[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> editor.js (source)

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


Generated : Wed Oct 7 08:20:33 2026 Cross-referenced by PHPXref