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