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