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