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