| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/editor.js 3 */ 4 5 window.wp = window.wp || {}; 6 7 ( function( $, wp ) { 8 wp.editor = wp.editor || {}; 9 10 /** 11 * Utility functions for the editor. 12 * 13 * @since 2.5.0 14 * @return {Object} The editor utility functions. 15 */ 16 function SwitchEditors() { 17 var tinymce, $$, 18 exports = {}; 19 20 function init() { 21 if ( ! tinymce && window.tinymce ) { 22 tinymce = window.tinymce; 23 $$ = tinymce.$; 24 25 /** 26 * Handles onclick events for the Visual/Code tabs. 27 * 28 * @since 4.3.0 29 * 30 * @return {void} 31 */ 32 $$( document ).on( 'click', function( event ) { 33 var id, mode, 34 target = $$( event.target ); 35 36 if ( target.hasClass( 'wp-switch-editor' ) ) { 37 id = target.attr( 'data-wp-editor-id' ); 38 mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html'; 39 switchEditor( id, mode ); 40 } 41 }); 42 } 43 } 44 45 /** 46 * Returns the height of the editor toolbar(s) in px. 47 * 48 * @since 3.9.0 49 * 50 * @param {Object} editor The TinyMCE editor. 51 * @return {number} If the height is between 10 and 200 return the height, 52 * else return 30. 53 */ 54 function getToolbarHeight( editor ) { 55 var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0], 56 height = node && node.clientHeight; 57 58 if ( height && height > 10 && height < 200 ) { 59 return parseInt( height, 10 ); 60 } 61 62 return 30; 63 } 64 65 /** 66 * Switches the editor between Visual and Code mode. 67 * 68 * @since 2.5.0 69 * 70 * @memberof switchEditors 71 * 72 * @param {string} id The id of the editor you want to change the editor mode for. Default: `content`. 73 * @param {string} mode The mode you want to switch to. Default: `toggle`. 74 * @return {void} 75 */ 76 function switchEditor( id, mode ) { 77 id = id || 'content'; 78 mode = mode || 'toggle'; 79 80 var editorHeight, toolbarHeight, iframe, 81 editor = tinymce.get( id ), 82 wrap = $$( '#wp-' + id + '-wrap' ), 83 htmlSwitch = wrap.find( '.switch-tmce' ), 84 tmceSwitch = wrap.find( '.switch-html' ), 85 $textarea = $$( '#' + id ), 86 textarea = $textarea[0]; 87 88 if ( 'toggle' === mode ) { 89 if ( editor && ! editor.isHidden() ) { 90 mode = 'html'; 91 } else { 92 mode = 'tmce'; 93 } 94 } 95 96 if ( 'tmce' === mode || 'tinymce' === mode ) { 97 // If the editor is visible we are already in `tinymce` mode. 98 if ( editor && ! editor.isHidden() ) { 99 return false; 100 } 101 102 // Insert closing tags for any open tags in QuickTags. 103 if ( typeof( window.QTags ) !== 'undefined' ) { 104 window.QTags.closeAllTags( id ); 105 } 106 107 editorHeight = parseInt( textarea.style.height, 10 ) || 0; 108 109 addHTMLBookmarkInTextAreaContent( $textarea ); 110 111 if ( editor ) { 112 editor.show(); 113 114 // No point to resize the iframe in iOS. 115 if ( ! tinymce.Env.iOS && editorHeight ) { 116 toolbarHeight = getToolbarHeight( editor ); 117 editorHeight = editorHeight - toolbarHeight + 14; 118 119 // Sane limit for the editor height. 120 if ( editorHeight > 50 && editorHeight < 5000 ) { 121 editor.theme.resizeTo( null, editorHeight ); 122 } 123 } 124 125 focusHTMLBookmarkInVisualEditor( editor ); 126 } else { 127 tinymce.init( window.tinyMCEPreInit.mceInit[ id ] ); 128 } 129 130 wrap.removeClass( 'html-active' ).addClass( 'tmce-active' ); 131 tmceSwitch.attr( 'aria-pressed', false ); 132 htmlSwitch.attr( 'aria-pressed', true ); 133 $textarea.attr( 'aria-hidden', true ); 134 window.setUserSetting( 'editor', 'tinymce' ); 135 136 } else if ( 'html' === mode ) { 137 // If the editor is hidden (Quicktags is shown) we don't need to switch. 138 if ( editor && editor.isHidden() ) { 139 return false; 140 } 141 142 if ( editor ) { 143 // Don't resize the textarea in iOS. 144 // The iframe is forced to 100% height there, we shouldn't match it. 145 if ( ! tinymce.Env.iOS ) { 146 iframe = editor.iframeElement; 147 editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0; 148 149 if ( editorHeight ) { 150 toolbarHeight = getToolbarHeight( editor ); 151 editorHeight = editorHeight + toolbarHeight - 14; 152 153 // Sane limit for the textarea height. 154 if ( editorHeight > 50 && editorHeight < 5000 ) { 155 textarea.style.height = editorHeight + 'px'; 156 } 157 } 158 } 159 160 var selectionRange = null; 161 162 selectionRange = findBookmarkedPosition( editor ); 163 164 editor.hide(); 165 166 if ( selectionRange ) { 167 selectTextInTextArea( editor, selectionRange ); 168 } 169 } else { 170 // There is probably a JS error on the page. 171 // The TinyMCE editor instance doesn't exist. Show the textarea. 172 $textarea.css({ 'display': '', 'visibility': '' }); 173 } 174 175 wrap.removeClass( 'tmce-active' ).addClass( 'html-active' ); 176 tmceSwitch.attr( 'aria-pressed', true ); 177 htmlSwitch.attr( 'aria-pressed', false ); 178 $textarea.attr( 'aria-hidden', false ); 179 window.setUserSetting( 'editor', 'html' ); 180 } 181 } 182 183 /** 184 * Checks if a cursor is inside an HTML tag or comment. 185 * 186 * In order to prevent breaking HTML tags when selecting text, the cursor 187 * must be moved to either the start or end of the tag. 188 * 189 * This will prevent the selection marker to be inserted in the middle of an HTML tag. 190 * 191 * This function gives information whether the cursor is inside a tag or not, as well as 192 * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag, 193 * e.g. `[caption]<img.../>..`. 194 * 195 * @param {string} content The test content where the cursor is. 196 * @param {number} cursorPosition The cursor position inside the content. 197 * 198 * @return {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag. 199 */ 200 function getContainingTagInfo( content, cursorPosition ) { 201 var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ), 202 lastGtPos = content.lastIndexOf( '>', cursorPosition ); 203 204 if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) { 205 // Find what the tag is. 206 var tagContent = content.substr( lastLtPos ), 207 tagMatch = tagContent.match( /<\s*(\/)?(\w+|\!-{2}.*-{2})/ ); 208 209 if ( ! tagMatch ) { 210 return null; 211 } 212 213 var tagType = tagMatch[2], 214 closingGt = tagContent.indexOf( '>' ); 215 216 return { 217 ltPos: lastLtPos, 218 gtPos: lastLtPos + closingGt + 1, // Offset by one to get the position _after_ the character. 219 tagType: tagType, 220 isClosingTag: !! tagMatch[1] 221 }; 222 } 223 return null; 224 } 225 226 /** 227 * Checks if the cursor is inside a shortcode 228 * 229 * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to 230 * move the selection marker to before or after the shortcode. 231 * 232 * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the 233 * `<img/>` tag inside. 234 * 235 * `[caption]<span>ThisIsGone</span><img .../>[caption]` 236 * 237 * Moving the selection to before or after the short code is better, since it allows to select 238 * something, instead of just losing focus and going to the start of the content. 239 * 240 * @param {string} content The text content to check against. 241 * @param {number} cursorPosition The cursor position to check. 242 * 243 * @return {void|Object} Undefined if the cursor is not wrapped in a shortcode tag. 244 * Information about the wrapping shortcode tag if it's wrapped in one. 245 */ 246 function getShortcodeWrapperInfo( content, cursorPosition ) { 247 var contentShortcodes = getShortCodePositionsInText( content ); 248 249 for ( var i = 0; i < contentShortcodes.length; i++ ) { 250 var element = contentShortcodes[ i ]; 251 252 if ( cursorPosition >= element.startIndex && cursorPosition <= element.endIndex ) { 253 return element; 254 } 255 } 256 } 257 258 /** 259 * Gets a list of unique shortcodes or shortcode-lookalikes in the content. 260 * 261 * @param {string} content The content we want to scan for shortcodes. 262 * @return {string[]} An array of unique shortcodes found in the content. 263 */ 264 function getShortcodesInText( content ) { 265 var shortcodes = content.match( /\[+([\w_-])+/g ), 266 result = []; 267 268 if ( shortcodes ) { 269 for ( var i = 0; i < shortcodes.length; i++ ) { 270 var shortcode = shortcodes[ i ].replace( /^\[+/g, '' ); 271 272 if ( result.indexOf( shortcode ) === -1 ) { 273 result.push( shortcode ); 274 } 275 } 276 } 277 278 return result; 279 } 280 281 /** 282 * Gets all shortcodes and their positions in the content 283 * 284 * This function returns all the shortcodes that could be found in the textarea content 285 * along with their character positions and boundaries. 286 * 287 * This is used to check if the selection cursor is inside the boundaries of a shortcode 288 * and move it accordingly, to avoid breakage. 289 * 290 * @link adjustTextAreaSelectionCursors 291 * 292 * The information can also be used in other cases when we need to lookup shortcode data, 293 * as it's already structured! 294 * 295 * @param {string} content The content we want to scan for shortcodes. 296 * @return {Object[]} An array of objects with information about the shortcodes found in the content. 297 */ 298 function getShortCodePositionsInText( content ) { 299 var allShortcodes = getShortcodesInText( content ), shortcodeInfo; 300 301 if ( allShortcodes.length === 0 ) { 302 return []; 303 } 304 305 var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ), 306 shortcodeMatch, // Define local scope for the variable to be used in the loop below. 307 shortcodesDetails = []; 308 309 while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) { 310 /** 311 * Check if the shortcode should be shown as plain text. 312 * 313 * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode 314 * and just shows it as text. 315 */ 316 var showAsPlainText = shortcodeMatch[1] === '['; 317 318 shortcodeInfo = { 319 shortcodeName: shortcodeMatch[2], 320 showAsPlainText: showAsPlainText, 321 startIndex: shortcodeMatch.index, 322 endIndex: shortcodeMatch.index + shortcodeMatch[0].length, 323 length: shortcodeMatch[0].length 324 }; 325 326 shortcodesDetails.push( shortcodeInfo ); 327 } 328 329 /** 330 * Get all URL matches, and treat them as embeds. 331 * 332 * Since there isn't a good way to detect if a URL by itself on a line is a previewable 333 * object, it's best to treat all of them as such. 334 * 335 * This means that the selection will capture the whole URL, in a similar way shrotcodes 336 * are treated. 337 */ 338 var urlRegexp = new RegExp( 339 '(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi' 340 ); 341 342 while ( shortcodeMatch = urlRegexp.exec( content ) ) { 343 shortcodeInfo = { 344 shortcodeName: 'url', 345 showAsPlainText: false, 346 startIndex: shortcodeMatch.index, 347 endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length, 348 length: shortcodeMatch[ 0 ].length, 349 urlAtStartOfContent: shortcodeMatch[ 1 ] === '', 350 urlAtEndOfContent: shortcodeMatch[ 3 ] === '' 351 }; 352 353 shortcodesDetails.push( shortcodeInfo ); 354 } 355 356 return shortcodesDetails; 357 } 358 359 /** 360 * Generate a cursor marker element to be inserted in the content. 361 * 362 * `span` seems to be the least destructive element that can be used. 363 * 364 * Using DomQuery syntax to create it, since it's used as both text and as a DOM element. 365 * 366 * @param {Object} domLib DOM library instance. 367 * @param {string} content The content to insert into the cursor marker element. 368 * @return {Object} The cursor marker element. 369 */ 370 function getCursorMarkerSpan( domLib, content ) { 371 return domLib( '<span>' ).css( { 372 display: 'inline-block', 373 width: 0, 374 overflow: 'hidden', 375 'line-height': 0 376 } ) 377 .html( content ? content : '' ); 378 } 379 380 /** 381 * Gets adjusted selection cursor positions according to HTML tags, comments, and shortcodes. 382 * 383 * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render 384 * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible 385 * to break the syntax and render the HTML tag or shortcode broken. 386 * 387 * @link getShortcodeWrapperInfo 388 * 389 * @param {string} content Textarea content that the cursors are in 390 * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions 391 * 392 * @return {{cursorStart: number, cursorEnd: number}} Adjusted cursor positions with `cursorStart` and `cursorEnd` properties. 393 */ 394 function adjustTextAreaSelectionCursors( content, cursorPositions ) { 395 var voidElements = [ 396 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 397 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' 398 ]; 399 400 var cursorStart = cursorPositions.cursorStart, 401 cursorEnd = cursorPositions.cursorEnd, 402 // Check if the cursor is in a tag and if so, adjust it. 403 isCursorStartInTag = getContainingTagInfo( content, cursorStart ); 404 405 if ( isCursorStartInTag ) { 406 /** 407 * Only move to the start of the HTML tag (to select the whole element) if the tag 408 * is part of the voidElements list above. 409 * 410 * This list includes tags that are self-contained and don't need a closing tag, according to the 411 * HTML5 specification. 412 * 413 * This is done in order to make selection of text a bit more consistent when selecting text in 414 * `<p>` tags or such. 415 * 416 * In cases where the tag is not a void element, the cursor is put to the end of the tag, 417 * so it's either between the opening and closing tag elements or after the closing tag. 418 */ 419 if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) { 420 cursorStart = isCursorStartInTag.ltPos; 421 } else { 422 cursorStart = isCursorStartInTag.gtPos; 423 } 424 } 425 426 var isCursorEndInTag = getContainingTagInfo( content, cursorEnd ); 427 if ( isCursorEndInTag ) { 428 cursorEnd = isCursorEndInTag.gtPos; 429 } 430 431 var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart ); 432 if ( isCursorStartInShortcode && ! isCursorStartInShortcode.showAsPlainText ) { 433 /** 434 * If a URL is at the start or the end of the content, 435 * the selection doesn't work, because it inserts a marker in the text, 436 * which breaks the embedURL detection. 437 * 438 * The best way to avoid that and not modify the user content is to 439 * adjust the cursor to either after or before URL. 440 */ 441 if ( isCursorStartInShortcode.urlAtStartOfContent ) { 442 cursorStart = isCursorStartInShortcode.endIndex; 443 } else { 444 cursorStart = isCursorStartInShortcode.startIndex; 445 } 446 } 447 448 var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd ); 449 if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) { 450 if ( isCursorEndInShortcode.urlAtEndOfContent ) { 451 cursorEnd = isCursorEndInShortcode.startIndex; 452 } else { 453 cursorEnd = isCursorEndInShortcode.endIndex; 454 } 455 } 456 457 return { 458 cursorStart: cursorStart, 459 cursorEnd: cursorEnd 460 }; 461 } 462 463 /** 464 * Adds text selection markers in the editor textarea. 465 * 466 * Adds selection markers in the content of the editor `textarea`. 467 * The method directly manipulates the `textarea` content, to allow TinyMCE plugins 468 * to run after the markers are added. 469 * 470 * @param {Object} $textarea TinyMCE's textarea wrapped as a DomQuery object 471 */ 472 function addHTMLBookmarkInTextAreaContent( $textarea ) { 473 if ( ! $textarea || ! $textarea.length ) { 474 // If no valid $textarea object is provided, there's nothing we can do. 475 return; 476 } 477 478 var textArea = $textarea[0], 479 textAreaContent = textArea.value, 480 481 adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, { 482 cursorStart: textArea.selectionStart, 483 cursorEnd: textArea.selectionEnd 484 } ), 485 486 htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart, 487 htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd, 488 489 mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single', 490 491 selectedText = null, 492 cursorMarkerSkeleton = getCursorMarkerSpan( $$, '' ).attr( 'data-mce-type','bookmark' ); 493 494 if ( mode === 'range' ) { 495 var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ), 496 bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' ); 497 498 selectedText = [ 499 markedText, 500 bookMarkEnd[0].outerHTML 501 ].join( '' ); 502 } 503 504 textArea.value = [ 505 textArea.value.slice( 0, htmlModeCursorStartPosition ), // Text until the cursor/selection position. 506 cursorMarkerSkeleton.clone() // Cursor/selection start marker. 507 .addClass( 'mce_SELRES_start' )[0].outerHTML, 508 selectedText, // Selected text with end cursor/position marker. 509 textArea.value.slice( htmlModeCursorEndPosition ) // Text from last cursor/selection position to end. 510 ].join( '' ); 511 } 512 513 /** 514 * Focuses the selection markers in Visual mode. 515 * 516 * The method checks for existing selection markers inside the editor DOM (Visual mode) 517 * and create a selection between the two nodes using the DOM `createRange` selection API. 518 * 519 * If there is only a single node, select only the single node through TinyMCE's selection API 520 * 521 * @param {Object} editor TinyMCE editor instance. 522 */ 523 function focusHTMLBookmarkInVisualEditor( editor ) { 524 var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ), 525 endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 ); 526 527 if ( startNode.length ) { 528 editor.focus(); 529 530 if ( ! endNode.length ) { 531 editor.selection.select( startNode[0] ); 532 } else { 533 var selection = editor.getDoc().createRange(); 534 535 selection.setStartAfter( startNode[0] ); 536 selection.setEndBefore( endNode[0] ); 537 538 editor.selection.setRng( selection ); 539 } 540 } 541 542 scrollVisualModeToStartElement( editor, startNode ); 543 544 removeSelectionMarker( startNode ); 545 removeSelectionMarker( endNode ); 546 547 editor.save(); 548 } 549 550 /** 551 * Removes selection marker and the parent node if it is an empty paragraph. 552 * 553 * By default TinyMCE wraps loose inline tags in a `<p>`. 554 * When removing selection markers an empty `<p>` may be left behind, remove it. 555 * 556 * @param {Object} $marker The marker to be removed from the editor DOM, wrapped in an instance of `editor.$` 557 */ 558 function removeSelectionMarker( $marker ) { 559 var $markerParent = $marker.parent(); 560 561 $marker.remove(); 562 563 //Remove empty paragraph left over after removing the marker. 564 if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) { 565 $markerParent.remove(); 566 } 567 } 568 569 /** 570 * Scrolls the content to place the selected element in the center of the screen. 571 * 572 * Takes an element, that is usually the selection start element, selected in 573 * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly 574 * in the middle of the screen. 575 * 576 * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted 577 * from the window height, to get the proper viewport window, that the user sees. 578 * 579 * @param {Object} editor TinyMCE editor instance. 580 * @param {Object} element HTMLElement that should be scrolled into view. 581 */ 582 function scrollVisualModeToStartElement( editor, element ) { 583 var elementTop = editor.$( element ).offset().top, 584 TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top, 585 586 toolbarHeight = getToolbarHeight( editor ), 587 588 edTools = $( '#wp-content-editor-tools' ), 589 edToolsHeight = 0, 590 edToolsOffsetTop = 0, 591 592 $scrollArea; 593 594 if ( edTools.length ) { 595 edToolsHeight = edTools.height(); 596 edToolsOffsetTop = edTools.offset().top; 597 } 598 599 var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, 600 601 selectionPosition = TinyMCEContentAreaTop + elementTop, 602 visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight ); 603 604 // There's no need to scroll if the selection is inside the visible area. 605 if ( selectionPosition < visibleAreaHeight ) { 606 return; 607 } 608 609 /** 610 * The minimum scroll height should be to the top of the editor, to offer a consistent 611 * experience. 612 * 613 * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and 614 * subtracting the height. This gives the scroll position where the top of the editor tools aligns with 615 * the top of the viewport (under the Master Bar) 616 */ 617 var adjustedScroll; 618 if ( editor.settings.wp_autoresize_on ) { 619 $scrollArea = $( 'html,body' ); 620 adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight ); 621 } else { 622 $scrollArea = $( editor.contentDocument ).find( 'html,body' ); 623 adjustedScroll = elementTop; 624 } 625 626 $scrollArea.animate( { 627 scrollTop: parseInt( adjustedScroll, 10 ) 628 }, 100 ); 629 } 630 631 /** 632 * This method was extracted from the `SaveContent` hook in 633 * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`. 634 * 635 * It's needed here, since the method changes the content a bit, which confuses the cursor position. 636 * 637 * @param {Object} event TinyMCE event object. 638 */ 639 function fixTextAreaContent( event ) { 640 // Keep empty paragraphs :( 641 event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p> </p>' ); 642 } 643 644 /** 645 * Finds the current selection position in the Visual editor. 646 * 647 * Find the current selection in the Visual editor by inserting marker elements at the start 648 * and end of the selection. 649 * 650 * Uses the standard DOM selection API to achieve that goal. 651 * 652 * Check the notes in the comments in the code below for more information on some gotchas 653 * and why this solution was chosen. 654 * 655 * @param {Object} editor The editor where we must find the selection. 656 * @return {void|Object} The selection range position in the editor. 657 */ 658 function findBookmarkedPosition( editor ) { 659 // Get the TinyMCE `window` reference, since we need to access the raw selection. 660 var TinyMCEWindow = editor.getWin(), 661 selection = TinyMCEWindow.getSelection(); 662 663 if ( ! selection || selection.rangeCount < 1 ) { 664 // no selection, no need to continue. 665 return; 666 } 667 668 /** 669 * The ID is used to avoid replacing user generated content, that may coincide with the 670 * format specified below. 671 * @type {string} 672 */ 673 var selectionID = 'SELRES_' + Math.random(); 674 675 /** 676 * Create two marker elements that will be used to mark the start and the end of the range. 677 * 678 * The elements have hardcoded style that makes them invisible. This is done to avoid seeing 679 * random content flickering in the editor when switching between modes. 680 */ 681 var spanSkeleton = getCursorMarkerSpan( editor.$, selectionID ), 682 startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ), 683 endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' ); 684 685 /** 686 * Inspired by: 687 * @link https://stackoverflow.com/a/17497803/153310 688 * 689 * Why do it this way and not with TinyMCE's bookmarks? 690 * 691 * TinyMCE's bookmarks are very nice when working with selections and positions, BUT 692 * there is no way to determine the precise position of the bookmark when switching modes, since 693 * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify 694 * HTML code and so on. In this process, the bookmark markup gets lost. 695 * 696 * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML 697 * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will 698 * throw off the positioning. 699 * 700 * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the 701 * selection. 702 * 703 * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates 704 * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to 705 * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the 706 * selection may start in the middle of one node and end in the middle of a completely different one. If we 707 * wrap the selection in another node, this will create artifacts in the content. 708 * 709 * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection. 710 * This helps us not break the content and also gives us the option to work with multi-node selections without 711 * breaking the markup. 712 */ 713 var range = selection.getRangeAt( 0 ), 714 startNode = range.startContainer, 715 startOffset = range.startOffset, 716 boundaryRange = range.cloneRange(); 717 718 /** 719 * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup, 720 * which we have to account for. 721 */ 722 if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) { 723 startNode = editor.$( '[data-mce-selected]' )[0]; 724 725 /** 726 * Marking the start and end element with `data-mce-object-selection` helps 727 * discern when the selected object is a Live Preview selection. 728 * 729 * This way we can adjust the selection to properly select only the content, ignoring 730 * whitespace inserted around the selected object by the Editor. 731 */ 732 startElement.attr( 'data-mce-object-selection', 'true' ); 733 endElement.attr( 'data-mce-object-selection', 'true' ); 734 735 editor.$( startNode ).before( startElement[0] ); 736 editor.$( startNode ).after( endElement[0] ); 737 } else { 738 boundaryRange.collapse( false ); 739 boundaryRange.insertNode( endElement[0] ); 740 741 boundaryRange.setStart( startNode, startOffset ); 742 boundaryRange.collapse( true ); 743 boundaryRange.insertNode( startElement[0] ); 744 745 range.setStartAfter( startElement[0] ); 746 range.setEndBefore( endElement[0] ); 747 selection.removeAllRanges(); 748 selection.addRange( range ); 749 } 750 751 /** 752 * Now the editor's content has the start/end nodes. 753 * 754 * Unfortunately the content goes through some more changes after this step, before it gets inserted 755 * in the `textarea`. This means that we have to do some minor cleanup on our own here. 756 */ 757 editor.on( 'GetContent', fixTextAreaContent ); 758 759 var content = removep( editor.getContent() ); 760 761 editor.off( 'GetContent', fixTextAreaContent ); 762 763 startElement.remove(); 764 endElement.remove(); 765 766 var startRegex = new RegExp( 767 '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)' 768 ); 769 770 var endRegex = new RegExp( 771 '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' 772 ); 773 774 var startMatch = content.match( startRegex ), 775 endMatch = content.match( endRegex ); 776 777 if ( ! startMatch ) { 778 return null; 779 } 780 781 var startIndex = startMatch.index, 782 startMatchLength = startMatch[0].length, 783 endIndex = null; 784 785 if (endMatch) { 786 /** 787 * Adjust the selection index, if the selection contains a Live Preview object or not. 788 * 789 * Check where the `data-mce-object-selection` attribute is set above for more context. 790 */ 791 if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) { 792 startMatchLength -= startMatch[1].length; 793 } 794 795 var endMatchIndex = endMatch.index; 796 797 if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) { 798 endMatchIndex -= endMatch[1].length; 799 } 800 801 // We need to adjust the end position to discard the length of the range start marker. 802 endIndex = endMatchIndex - startMatchLength; 803 } 804 805 return { 806 start: startIndex, 807 end: endIndex 808 }; 809 } 810 811 /** 812 * Selects text in the TinyMCE `textarea`. 813 * 814 * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`. 815 * 816 * For `selection` parameter: 817 * @link findBookmarkedPosition 818 * 819 * @param {Object} editor TinyMCE's editor instance. 820 * @param {Object} selection Selection data. 821 */ 822 function selectTextInTextArea( editor, selection ) { 823 // Only valid in the text area mode and if we have selection. 824 if ( ! selection ) { 825 return; 826 } 827 828 var textArea = editor.getElement(), 829 start = selection.start, 830 end = selection.end || selection.start; 831 832 if ( textArea.focus ) { 833 // Wait for the Visual editor to be hidden, then focus and scroll to the position. 834 setTimeout( function() { 835 textArea.setSelectionRange( start, end ); 836 if ( textArea.blur ) { 837 // Defocus before focusing. 838 textArea.blur(); 839 } 840 textArea.focus(); 841 }, 100 ); 842 } 843 } 844 845 // Restore the selection when the editor is initialized. Needed when the Code editor is the default. 846 $( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) { 847 if ( editor.$( '.mce_SELRES_start' ).length ) { 848 focusHTMLBookmarkInVisualEditor( editor ); 849 } 850 } ); 851 852 /** 853 * Replaces <p> tags with two line breaks. "Opposite" of wpautop(). 854 * 855 * Replaces <p> tags with two line breaks except where the <p> has attributes. 856 * Unifies whitespace. 857 * Indents <li>, <dt> and <dd> for better readability. 858 * 859 * @since 2.5.0 860 * 861 * @memberof switchEditors 862 * 863 * @param {string} html The content from the editor. 864 * @return {string} The content with stripped paragraph tags. 865 */ 866 function removep( html ) { 867 var blocklist = 'blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure', 868 blocklist1 = blocklist + '|div|p', 869 blocklist2 = blocklist + '|pre', 870 preserve_linebreaks = false, 871 preserve_br = false, 872 preserve = []; 873 874 if ( ! html ) { 875 return ''; 876 } 877 878 // Protect script and style tags. 879 if ( html.indexOf( '<script' ) !== -1 || html.indexOf( '<style' ) !== -1 ) { 880 html = html.replace( /<(script|style)[^>]*>[\s\S]*?<\/\1>/g, function( match ) { 881 preserve.push( match ); 882 return '<wp-preserve>'; 883 } ); 884 } 885 886 // Protect pre tags. 887 if ( html.indexOf( '<pre' ) !== -1 ) { 888 preserve_linebreaks = true; 889 html = html.replace( /<pre[^>]*>[\s\S]+?<\/pre>/g, function( a ) { 890 a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' ); 891 a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' ); 892 return a.replace( /\r?\n/g, '<wp-line-break>' ); 893 }); 894 } 895 896 // Remove line breaks but keep <br> tags inside image captions. 897 if ( html.indexOf( '[caption' ) !== -1 ) { 898 preserve_br = true; 899 html = html.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) { 900 return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' ); 901 }); 902 } 903 904 // Normalize white space characters before and after block tags. 905 html = html.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' ); 906 html = html.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' ); 907 908 // Mark </p> if it has any attributes. 909 html = html.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' ); 910 911 // Preserve the first <p> inside a <div>. 912 html = html.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' ); 913 914 // Remove paragraph tags. 915 html = html.replace( /\s*<p>/gi, '' ); 916 html = html.replace( /\s*<\/p>\s*/gi, '\n\n' ); 917 918 // Normalize white space chars and remove multiple line breaks. 919 html = html.replace( /\n[\s\u00a0]+\n/g, '\n\n' ); 920 921 // Replace <br> tags with line breaks. 922 html = html.replace( /(\s*)<br ?\/?>\s*/gi, function( match, space ) { 923 if ( space && space.indexOf( '\n' ) !== -1 ) { 924 return '\n\n'; 925 } 926 927 return '\n'; 928 }); 929 930 // Fix line breaks around <div>. 931 html = html.replace( /\s*<div/g, '\n<div' ); 932 html = html.replace( /<\/div>\s*/g, '</div>\n' ); 933 934 // Fix line breaks around caption shortcodes. 935 html = html.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' ); 936 html = html.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' ); 937 938 // Pad block elements tags with a line break. 939 html = html.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' ); 940 html = html.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' ); 941 942 // Indent <li>, <dt> and <dd> tags. 943 html = html.replace( /<((li|dt|dd)[^>]*)>/g, ' \t<$1>' ); 944 945 // Fix line breaks around <select> and <option>. 946 if ( html.indexOf( '<option' ) !== -1 ) { 947 html = html.replace( /\s*<option/g, '\n<option' ); 948 html = html.replace( /\s*<\/select>/g, '\n</select>' ); 949 } 950 951 // Pad <hr> with two line breaks. 952 if ( html.indexOf( '<hr' ) !== -1 ) { 953 html = html.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' ); 954 } 955 956 // Remove line breaks in <object> tags. 957 if ( html.indexOf( '<object' ) !== -1 ) { 958 html = html.replace( /<object[\s\S]+?<\/object>/g, function( a ) { 959 return a.replace( /[\r\n]+/g, '' ); 960 }); 961 } 962 963 // Unmark special paragraph closing tags. 964 html = html.replace( /<\/p#>/g, '</p>\n' ); 965 966 // Pad remaining <p> tags whit a line break. 967 html = html.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' ); 968 969 // Trim. 970 html = html.replace( /^\s+/, '' ); 971 html = html.replace( /[\s\u00a0]+$/, '' ); 972 973 if ( preserve_linebreaks ) { 974 html = html.replace( /<wp-line-break>/g, '\n' ); 975 } 976 977 if ( preserve_br ) { 978 html = html.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' ); 979 } 980 981 // Restore preserved tags. 982 if ( preserve.length ) { 983 html = html.replace( /<wp-preserve>/g, function() { 984 return preserve.shift(); 985 } ); 986 } 987 988 return html; 989 } 990 991 /** 992 * Replaces two line breaks with a paragraph tag and one line break with a <br>. 993 * 994 * Similar to `wpautop()` in formatting.php. 995 * 996 * @since 2.5.0 997 * 998 * @memberof switchEditors 999 * 1000 * @param {string} text The text input. 1001 * @return {string} The formatted text. 1002 */ 1003 function autop( text ) { 1004 var preserve_linebreaks = false, 1005 preserve_br = false, 1006 blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' + 1007 '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' + 1008 '|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary'; 1009 1010 // Normalize line breaks. 1011 text = text.replace( /\r\n|\r/g, '\n' ); 1012 1013 // Remove line breaks from <object>. 1014 if ( text.indexOf( '<object' ) !== -1 ) { 1015 text = text.replace( /<object[\s\S]+?<\/object>/g, function( a ) { 1016 return a.replace( /\n+/g, '' ); 1017 }); 1018 } 1019 1020 // Remove line breaks from tags. 1021 text = text.replace( /<[^<>]+>/g, function( a ) { 1022 return a.replace( /[\n\t ]+/g, ' ' ); 1023 }); 1024 1025 // Preserve line breaks in <pre> and <script> tags. 1026 if ( text.indexOf( '<pre' ) !== -1 || text.indexOf( '<script' ) !== -1 ) { 1027 preserve_linebreaks = true; 1028 text = text.replace( /<(pre|script)[^>]*>[\s\S]*?<\/\1>/g, function( a ) { 1029 return a.replace( /\n/g, '<wp-line-break>' ); 1030 }); 1031 } 1032 1033 if ( text.indexOf( '<figcaption' ) !== -1 ) { 1034 text = text.replace( /\s*(<figcaption[^>]*>)/g, '$1' ); 1035 text = text.replace( /<\/figcaption>\s*/g, '</figcaption>' ); 1036 } 1037 1038 // Keep <br> tags inside captions. 1039 if ( text.indexOf( '[caption' ) !== -1 ) { 1040 preserve_br = true; 1041 1042 text = text.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) { 1043 a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ); 1044 1045 a = a.replace( /<[^<>]+>/g, function( b ) { 1046 return b.replace( /[\n\t ]+/, ' ' ); 1047 }); 1048 1049 return a.replace( /\s*\n\s*/g, '<wp-temp-br />' ); 1050 }); 1051 } 1052 1053 text = text + '\n\n'; 1054 text = text.replace( /<br \/>\s*<br \/>/gi, '\n\n' ); 1055 1056 // Pad block tags with two line breaks. 1057 text = text.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n\n$1' ); 1058 text = text.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' ); 1059 text = text.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' ); 1060 1061 // Remove white space chars around <option>. 1062 text = text.replace( /\s*<option/gi, '<option' ); 1063 text = text.replace( /<\/option>\s*/gi, '</option>' ); 1064 1065 // Normalize multiple line breaks and white space chars. 1066 text = text.replace( /\n\s*\n+/g, '\n\n' ); 1067 1068 // Convert two line breaks to a paragraph. 1069 text = text.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' ); 1070 1071 // Remove empty paragraphs. 1072 text = text.replace( /<p>\s*?<\/p>/gi, ''); 1073 1074 // Remove <p> tags that are around block tags. 1075 text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' ); 1076 text = text.replace( /<p>(<li.+?)<\/p>/gi, '$1'); 1077 1078 // Fix <p> in blockquotes. 1079 text = text.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>'); 1080 text = text.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>'); 1081 1082 // Remove <p> tags that are wrapped around block tags. 1083 text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' ); 1084 text = text.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' ); 1085 1086 text = text.replace( /(<br[^>]*>)\s*\n/gi, '$1' ); 1087 1088 // Add <br> tags. 1089 text = text.replace( /\s*\n/g, '<br />\n'); 1090 1091 // Remove <br> tags that are around block tags. 1092 text = text.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' ); 1093 text = text.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' ); 1094 1095 // Remove <p> and <br> around captions. 1096 text = text.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' ); 1097 1098 // Make sure there is <p> when there is </p> inside block tags that can contain other blocks. 1099 text = text.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) { 1100 if ( c.match( /<p( [^>]*)?>/ ) ) { 1101 return a; 1102 } 1103 1104 return b + '<p>' + c + '</p>'; 1105 }); 1106 1107 // Restore the line breaks in <pre> and <script> tags. 1108 if ( preserve_linebreaks ) { 1109 text = text.replace( /<wp-line-break>/g, '\n' ); 1110 } 1111 1112 // Restore the <br> tags in captions. 1113 if ( preserve_br ) { 1114 text = text.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' ); 1115 } 1116 1117 return text; 1118 } 1119 1120 /** 1121 * Fires custom jQuery events `beforePreWpautop` and `afterPreWpautop` when jQuery is available. 1122 * 1123 * @since 2.9.0 1124 * 1125 * @memberof switchEditors 1126 * 1127 * @param {string} html The content from the visual editor. 1128 * @return {string} the filtered content. 1129 */ 1130 function pre_wpautop( html ) { 1131 var obj = { o: exports, data: html, unfiltered: html }; 1132 1133 if ( $ ) { 1134 $( 'body' ).trigger( 'beforePreWpautop', [ obj ] ); 1135 } 1136 1137 obj.data = removep( obj.data ); 1138 1139 if ( $ ) { 1140 $( 'body' ).trigger( 'afterPreWpautop', [ obj ] ); 1141 } 1142 1143 return obj.data; 1144 } 1145 1146 /** 1147 * Fires custom jQuery events `beforeWpautop` and `afterWpautop` when jQuery is available. 1148 * 1149 * @since 2.9.0 1150 * 1151 * @memberof switchEditors 1152 * 1153 * @param {string} text The content from the text editor. 1154 * @return {string} filtered content. 1155 */ 1156 function wpautop( text ) { 1157 var obj = { o: exports, data: text, unfiltered: text }; 1158 1159 if ( $ ) { 1160 $( 'body' ).trigger( 'beforeWpautop', [ obj ] ); 1161 } 1162 1163 obj.data = autop( obj.data ); 1164 1165 if ( $ ) { 1166 $( 'body' ).trigger( 'afterWpautop', [ obj ] ); 1167 } 1168 1169 return obj.data; 1170 } 1171 1172 if ( $ ) { 1173 $( init ); 1174 } else if ( document.addEventListener ) { 1175 document.addEventListener( 'DOMContentLoaded', init, false ); 1176 window.addEventListener( 'load', init, false ); 1177 } else if ( window.attachEvent ) { 1178 window.attachEvent( 'onload', init ); 1179 document.attachEvent( 'onreadystatechange', function() { 1180 if ( 'complete' === document.readyState ) { 1181 init(); 1182 } 1183 } ); 1184 } 1185 1186 wp.editor.autop = wpautop; 1187 wp.editor.removep = pre_wpautop; 1188 1189 exports = { 1190 go: switchEditor, 1191 wpautop: wpautop, 1192 pre_wpautop: pre_wpautop, 1193 _wp_Autop: autop, 1194 _wp_Nop: removep 1195 }; 1196 1197 return exports; 1198 } 1199 1200 /** 1201 * Expose the switch editors to be used globally. 1202 * 1203 * @namespace switchEditors 1204 */ 1205 window.switchEditors = new SwitchEditors(); 1206 1207 /** 1208 * Initialize TinyMCE and/or Quicktags. For use with wp_enqueue_editor() (PHP). 1209 * 1210 * Intended for use with an existing textarea that will become the Code editor tab. 1211 * The editor width will be the width of the textarea container, height will be adjustable. 1212 * 1213 * Settings for both TinyMCE and Quicktags can be passed on initialization, and are "filtered" 1214 * with custom jQuery events on the document element, wp-before-tinymce-init and wp-before-quicktags-init. 1215 * 1216 * @since 4.8.0 1217 * 1218 * @param {string} id The HTML id of the textarea that is used for the editor. 1219 * Has to be jQuery compliant. No brackets, special chars, etc. 1220 * @param {Object} settings Example: 1221 * settings = { 1222 * // See https://www.tinymce.com/docs/configure/integration-and-setup/. 1223 * // Alternatively set to `true` to use the defaults. 1224 * tinymce: { 1225 * setup: function( editor ) { 1226 * console.log( 'Editor initialized', editor ); 1227 * } 1228 * } 1229 * 1230 * // Alternatively set to `true` to use the defaults. 1231 * quicktags: { 1232 * buttons: 'strong,em,link' 1233 * } 1234 * } 1235 */ 1236 wp.editor.initialize = function( id, settings ) { 1237 var init; 1238 var defaults; 1239 1240 if ( ! $ || ! id || ! wp.editor.getDefaultSettings ) { 1241 return; 1242 } 1243 1244 defaults = wp.editor.getDefaultSettings(); 1245 1246 // Initialize TinyMCE by default. 1247 if ( ! settings ) { 1248 settings = { 1249 tinymce: true 1250 }; 1251 } 1252 1253 // Add wrap and the Visual|Code tabs. 1254 if ( settings.tinymce && settings.quicktags ) { 1255 var $textarea = $( '#' + id ); 1256 1257 var $wrap = $( '<div>' ).attr( { 1258 'class': 'wp-core-ui wp-editor-wrap tmce-active', 1259 id: 'wp-' + id + '-wrap' 1260 } ); 1261 1262 var $editorContainer = $( '<div class="wp-editor-container">' ); 1263 1264 var $button = $( '<button>' ).attr( { 1265 type: 'button', 1266 'data-wp-editor-id': id 1267 } ); 1268 1269 var $editorTools = $( '<div class="wp-editor-tools">' ); 1270 1271 if ( settings.mediaButtons ) { 1272 var buttonText = 'Add Media'; 1273 1274 if ( window._wpMediaViewsL10n && window._wpMediaViewsL10n.addMedia ) { 1275 buttonText = window._wpMediaViewsL10n.addMedia; 1276 } 1277 1278 var $addMediaButton = $( '<button type="button" class="button insert-media add_media">' ); 1279 1280 $addMediaButton.append( '<span class="wp-media-buttons-icon" aria-hidden="true"></span>' ); 1281 $addMediaButton.append( document.createTextNode( ' ' + buttonText ) ); 1282 $addMediaButton.data( 'editor', id ); 1283 1284 $editorTools.append( 1285 $( '<div class="wp-media-buttons">' ) 1286 .append( $addMediaButton ) 1287 ); 1288 } 1289 1290 $wrap.append( 1291 $editorTools 1292 .append( $( '<div class="wp-editor-tabs">' ) 1293 .append( $button.clone().attr({ 1294 id: id + '-tmce', 1295 'class': 'wp-switch-editor switch-tmce' 1296 }).text( window.tinymce.translate( 'Visual' ) ) ) 1297 .append( $button.attr({ 1298 id: id + '-html', 1299 'class': 'wp-switch-editor switch-html' 1300 }).text( window.tinymce.translate( 'Code|tab' ) ) ) 1301 ).append( $editorContainer ) 1302 ); 1303 1304 $textarea.after( $wrap ); 1305 $editorContainer.append( $textarea ); 1306 } 1307 1308 if ( window.tinymce && settings.tinymce ) { 1309 if ( typeof settings.tinymce !== 'object' ) { 1310 settings.tinymce = {}; 1311 } 1312 1313 init = $.extend( {}, defaults.tinymce, settings.tinymce ); 1314 init.selector = '#' + id; 1315 1316 $( document ).trigger( 'wp-before-tinymce-init', init ); 1317 window.tinymce.init( init ); 1318 1319 if ( ! window.wpActiveEditor ) { 1320 window.wpActiveEditor = id; 1321 } 1322 } 1323 1324 if ( window.quicktags && settings.quicktags ) { 1325 if ( typeof settings.quicktags !== 'object' ) { 1326 settings.quicktags = {}; 1327 } 1328 1329 init = $.extend( {}, defaults.quicktags, settings.quicktags ); 1330 init.id = id; 1331 1332 $( document ).trigger( 'wp-before-quicktags-init', init ); 1333 window.quicktags( init ); 1334 1335 if ( ! window.wpActiveEditor ) { 1336 window.wpActiveEditor = init.id; 1337 } 1338 } 1339 }; 1340 1341 /** 1342 * Remove one editor instance. 1343 * 1344 * Intended for use with editors that were initialized with wp.editor.initialize(). 1345 * 1346 * @since 4.8.0 1347 * 1348 * @param {string} id The HTML id of the editor textarea. 1349 */ 1350 wp.editor.remove = function( id ) { 1351 var mceInstance, qtInstance, 1352 $wrap = $( '#wp-' + id + '-wrap' ); 1353 1354 if ( window.tinymce ) { 1355 mceInstance = window.tinymce.get( id ); 1356 1357 if ( mceInstance ) { 1358 if ( ! mceInstance.isHidden() ) { 1359 mceInstance.save(); 1360 } 1361 1362 mceInstance.remove(); 1363 } 1364 } 1365 1366 if ( window.quicktags ) { 1367 qtInstance = window.QTags.getInstance( id ); 1368 1369 if ( qtInstance ) { 1370 qtInstance.remove(); 1371 } 1372 } 1373 1374 if ( $wrap.length ) { 1375 $wrap.after( $( '#' + id ) ); 1376 $wrap.remove(); 1377 } 1378 }; 1379 1380 /** 1381 * Get the editor content. 1382 * 1383 * Intended for use with editors that were initialized with wp.editor.initialize(). 1384 * 1385 * @since 4.8.0 1386 * 1387 * @param {string} id The HTML id of the editor textarea. 1388 * @return {void|string} The editor content. 1389 */ 1390 wp.editor.getContent = function( id ) { 1391 var editor; 1392 1393 if ( ! $ || ! id ) { 1394 return; 1395 } 1396 1397 if ( window.tinymce ) { 1398 editor = window.tinymce.get( id ); 1399 1400 if ( editor && ! editor.isHidden() ) { 1401 editor.save(); 1402 } 1403 } 1404 1405 return $( '#' + id ).val(); 1406 }; 1407 1408 }( window.jQuery, window.wp ));
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Sep 8 08:20:28 2026 | Cross-referenced by PHPXref |