WordPress.org

Make WordPress Core

Ticket #41962: 41962.diff

File 41962.diff, 21.5 KB (added by biskobe, 3 years ago)
  • src/wp-admin/js/editor.js

    diff --git a/src/wp-admin/js/editor.js b/src/wp-admin/js/editor.js
    index 91c8b9d2cf..0d73e418a7 100644
    a b window.wp = window.wp || {}; 
    9999
    100100                                editorHeight = parseInt( textarea.style.height, 10 ) || 0;
    101101
     102                                // Save the selection
     103                                addHTMLBookmarkInTextAreaContent( $textarea, $ );
     104
    102105                                if ( editor ) {
    103106                                        editor.show();
    104107
    window.wp = window.wp || {}; 
    112115                                                        editor.theme.resizeTo( null, editorHeight );
    113116                                                }
    114117                                        }
     118
     119                                        // Restore the selection
     120                                        focusHTMLBookmarkInVisualEditor( editor );
    115121                                } else {
    116                                         tinymce.init( window.tinyMCEPreInit.mceInit[id] );
     122                                        /**
     123                                         * TinyMCE is still not loaded. In order to restore the selection
     124                                         * when the editor loads, a `on('init')` event is added, that will
     125                                         * do the restoration.
     126                                         *
     127                                         * To achieve that, the initialization config is cloned and extended
     128                                         * to include the `setup` method, which makes it possible to add the
     129                                         * `on('init')` event.
     130                                         *
     131                                         * Cloning is used to prevent modification of the original init config,
     132                                         * which may cause unwanted side effects.
     133                                         */
     134                                        var tinyMCEConfig = $.extend(
     135                                                {},
     136                                                window.tinyMCEPreInit.mceInit[id],
     137                                                {
     138                                                        setup: function(editor) {
     139                                                                editor.on('init', function(event) {
     140                                                                        focusHTMLBookmarkInVisualEditor( event.target );
     141                                                                });
     142                                                        }
     143                                                }
     144                                        );
     145
     146                                        tinymce.init( tinyMCEConfig );
    117147                                }
    118148
    119149                                wrap.removeClass( 'html-active' ).addClass( 'tmce-active' );
    window.wp = window.wp || {}; 
    126156                                        return false;
    127157                                }
    128158
     159                                var selectionRange = null;
    129160                                if ( editor ) {
    130161                                        // Don't resize the textarea in iOS. The iframe is forced to 100% height there, we shouldn't match it.
    131162                                        if ( ! tinymce.Env.iOS ) {
    window.wp = window.wp || {}; 
    143174                                                }
    144175                                        }
    145176
     177                                        selectionRange = findBookmarkedPosition( editor );
     178
    146179                                        editor.hide();
     180
     181                                        if ( selectionRange ) {
     182                                                selectTextInTextArea( editor, selectionRange );
     183                                        }
    147184                                } else {
    148185                                        // There is probably a JS error on the page. The TinyMCE editor instance doesn't exist. Show the textarea.
    149186                                        $textarea.css({ 'display': '', 'visibility': '' });
    window.wp = window.wp || {}; 
    156193                }
    157194
    158195                /**
     196                 * @summary Checks if a cursor is inside an HTML tag.
     197                 *
     198                 * In order to prevent breaking HTML tags when selecting text, the cursor
     199                 * must be moved to either the start or end of the tag.
     200                 *
     201                 * This will prevent the selection marker to be inserted in the middle of an HTML tag.
     202                 *
     203                 * This function gives information whether the cursor is inside a tag or not, as well as
     204                 * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag,
     205                 * e.g. `[caption]<img.../>..`.
     206                 *
     207                 * @param {string} content The test content where the cursor is.
     208                 * @param {number} cursorPosition The cursor position inside the content.
     209                 *
     210                 * @returns {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag.
     211                 */
     212                function getContainingTagInfo( content, cursorPosition ) {
     213                        var lastLtPos = content.lastIndexOf( '<', cursorPosition ),
     214                                lastGtPos = content.lastIndexOf( '>', cursorPosition );
     215
     216                        if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
     217                                // find what the tag is
     218                                var tagContent = content.substr( lastLtPos );
     219                                var tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
     220                                if ( ! tagMatch ) {
     221                                        return null;
     222                                }
     223
     224                                var tagType = tagMatch[ 2 ];
     225                                var closingGt = tagContent.indexOf( '>' );
     226                                var isClosingTag = ! ! tagMatch[ 1 ];
     227                                var shortcodeWrapperInfo = getShortcodeWrapperInfo( content, lastLtPos );
     228
     229                                return {
     230                                        ltPos: lastLtPos,
     231                                        gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character,
     232                                        tagType: tagType,
     233                                        isClosingTag: isClosingTag,
     234                                        shortcodeTagInfo: shortcodeWrapperInfo
     235                                };
     236                        }
     237                        return null;
     238                }
     239
     240                /**
     241                 * @summary Check if a given HTML tag is enclosed in a shortcode tag
     242                 *
     243                 * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to
     244                 * move the selection marker to before the short tag.
     245                 *
     246                 * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
     247                 * `<img/>` tag inside.
     248                 *
     249                 *      `[caption]<span>ThisIsGone</span><img .../>[caption]`
     250                 *
     251                 *      Moving the selection to before the short code is better, since it allows to select
     252                 *      something, instead of just losing focus and going to the start of the content.
     253                 *
     254                 *      @param {string} content The text content to check against
     255                 *      @param {number} cursorPosition  The cursor position to check from. Usually this is the opening symbol of
     256                 *                                                                      an HTML tag.
     257                 *
     258                 * @return {(null|Object)}      Null if the oject is not wrapped in a shortcode tag.
     259                 *                                                      Information about the wrapping shortcode tag if it's wrapped in one.
     260                 */
     261                function getShortcodeWrapperInfo( content, cursorPosition ) {
     262                        if ( content.substr( cursorPosition - 1, 1 ) === ']' ) {
     263                                var shortTagStart = content.lastIndexOf( '[', cursorPosition );
     264                                var shortTagContent = content.substr(shortTagStart, cursorPosition - shortTagStart);
     265                                var shortTag = content.match( /\[\s*(\/)?(\w+)/ );
     266                                var tagType = shortTag[ 2 ];
     267                                var closingGt = shortTagContent.indexOf( '>' );
     268                                var isClosingTag = ! ! shortTag[ 1 ];
     269
     270                                return {
     271                                        openingBracket: shortTagStart,
     272                                        shortcode: tagType,
     273                                        closingBracket: closingGt,
     274                                        isClosingTag: isClosingTag
     275                                }
     276                        }
     277
     278                        return null;
     279                }
     280
     281                /**
     282                 * Generate a cursor marker element to be inserted in the content.
     283                 *
     284                 * `span` seems to be the least destructive element that can be used.
     285                 *
     286                 * Using DomQuery syntax to create it, since it's used as both text and as a DOM element.
     287                 *
     288                 * @param {Object} editor The TinyMCE editor instance.
     289                 * @param {string} content The content to insert into the cusror marker element.
     290                 */
     291                function getCursorMarkerSpan( editor, content ) {
     292                        return editor.$( '<span>' ).css( {
     293                                                display: 'inline-block',
     294                                                width: 0,
     295                                                overflow: 'hidden',
     296                                                'line-height': 0
     297                                        } )
     298                                        .html( content ? content : '' );
     299                }
     300
     301                /**
     302                 * @summary Adds text selection markers in the editor textarea.
     303                 *
     304                 * Adds selection markers in the content of the editor `textarea`.
     305                 * The method directly manipulates the `textarea` content, to allow TinyMCE plugins
     306                 * to run after the markers are added.
     307                 *
     308                 * @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
     309                 * @param {object} jQuery A jQuery instance
     310                 */
     311                function addHTMLBookmarkInTextAreaContent( $textarea, jQuery ) {
     312                        var textArea = $textarea[ 0 ], // TODO add error checking
     313                                htmlModeCursorStartPosition = textArea.selectionStart,
     314                                htmlModeCursorEndPosition = textArea.selectionEnd;
     315
     316                        var voidElements = [
     317                                'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
     318                                'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
     319                        ];
     320
     321                        // check if the cursor is in a tag and if so, adjust it
     322                        var isCursorStartInTag = getContainingTagInfo( textArea.value, htmlModeCursorStartPosition );
     323                        if ( isCursorStartInTag ) {
     324                                /**
     325                                 * Only move to the start of the HTML tag (to select the whole element) if the tag
     326                                 * is part of the voidElements list above.
     327                                 *
     328                                 * This list includes tags that are self-contained and don't need a closing tag, according to the
     329                                 * HTML5 specification.
     330                                 *
     331                                 * This is done in order to make selection of text a bit more consistent when selecting text in
     332                                 * `<p>` tags or such.
     333                                 *
     334                                 * In cases where the tag is not a void element, the cursor is put to the end of the tag,
     335                                 * so it's either between the opening and closing tag elements or after the closing tag.
     336                                 */
     337                                if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== - 1 ) {
     338                                        htmlModeCursorStartPosition = isCursorStartInTag.ltPos;
     339                                }
     340                                else {
     341                                        htmlModeCursorStartPosition = isCursorStartInTag.gtPos;
     342                                }
     343                        }
     344
     345                        var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition );
     346                        if ( isCursorEndInTag ) {
     347                                htmlModeCursorEndPosition = isCursorEndInTag.gtPos;
     348                        }
     349
     350                        var mode =
     351                                htmlModeCursorStartPosition !== htmlModeCursorEndPosition
     352                                        ? 'range'
     353                                        : 'single';
     354
     355                        var selectedText = null;
     356                        var cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
     357
     358                        if ( mode === 'range' ) {
     359                                var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition );
     360
     361                                /**
     362                                 * Since the shortcodes convert the tags in them a bit, we need to mark the tag itself,
     363                                 * and not rely on the cursor marker.
     364                                 *
     365                                 * @see getShortcodeWrapperInfo
     366                                 */
     367                                if ( isCursorStartInTag && isCursorStartInTag.shortcodeTagInfo ) {
     368                                        // Get the tag on the cursor start
     369                                        var tagEndPosition = isCursorStartInTag.gtPos - isCursorStartInTag.ltPos;
     370                                        var tagContent = markedText.slice( 0, tagEndPosition );
     371
     372                                        // Check if the tag already has a `class` attribute.
     373                                        var classMatch = /class=(['"])([^$1]*?)\1/;
     374
     375                                        /**
     376                                         * Add a marker class to the selected tag, to be used later.
     377                                         *
     378                                         * @see focusHTMLBookmarkInVisualEditor
     379                                         */
     380                                        if ( tagContent.match( classMatch ) ) {
     381                                                tagContent = tagContent.replace( classMatch, 'class=$1$2 mce_SELRES_start_target$1' )
     382                                        }
     383                                        else {
     384                                                tagContent = tagContent.replace( /(<\w+)/, '$1 class="mce_SELRES_start_target" ' )
     385                                        }
     386
     387                                        // Update the selected text content with the marked tag above
     388                                        markedText = [
     389                                                tagContent,
     390                                                markedText.substr( tagEndPosition )
     391                                        ].join( '' );
     392                                }
     393
     394                                var bookMarkEnd = cursorMarkerSkeleton.clone()
     395                                        .addClass( 'mce_SELRES_end' )[ 0 ].outerHTML;
     396
     397                                /**
     398                                 * A small workaround when selecting just a single HTML tag inside a shortcode.
     399                                 *
     400                                 * This removes the end selection marker, to make sure the HTML tag is the only selected
     401                                 * thing. This prevents the selection to appear like it contains multiple items in it (i.e.
     402                                 * all highlighted blue)
     403                                 */
     404                                if (
     405                                        isCursorStartInTag
     406                                        && isCursorStartInTag.shortcodeTagInfo
     407                                        && isCursorEndInTag
     408                                        && isCursorStartInTag.ltPos === isCursorEndInTag.ltPos
     409                                ) {
     410                                        bookMarkEnd = '';
     411                                }
     412
     413                                selectedText = [
     414                                        markedText,
     415                                        bookMarkEnd
     416                                ].join( '' );
     417                        }
     418
     419                        textArea.value = [
     420                                textArea.value.slice( 0, htmlModeCursorStartPosition ), // text until the cursor/selection position
     421                                cursorMarkerSkeleton.clone()                                                    // cursor/selection start marker
     422                                        .addClass( 'mce_SELRES_start')[0].outerHTML,
     423                                selectedText,                                                                                   // selected text with end cursor/position marker
     424                                textArea.value.slice( htmlModeCursorEndPosition )               // text from last cursor/selection position to end
     425                        ].join( '' );
     426                }
     427
     428                /**
     429                 * @summary Focus the selection markers in Visual mode.
     430                 *
     431                 * The method checks for existing selection markers inside the editor DOM (Visual mode)
     432                 * and create a selection between the two nodes using the DOM `createRange` selection API
     433                 *
     434                 * If there is only a single node, select only the single node through TinyMCE's selection API
     435                 *
     436                 * @param {Object} editor TinyMCE editor instance.
     437                 */
     438                function focusHTMLBookmarkInVisualEditor( editor ) {
     439                        var startNode = editor.$( '.mce_SELRES_start' ),
     440                                endNode = editor.$( '.mce_SELRES_end' );
     441
     442                        if ( ! startNode.length ) {
     443                                startNode = editor.$( '.mce_SELRES_start_target' );
     444                        }
     445
     446                        if ( startNode.length ) {
     447                                editor.focus();
     448
     449                                if ( ! endNode.length ) {
     450                                        editor.selection.select( startNode[ 0 ] );
     451                                } else {
     452                                        const selection = editor.getDoc().createRange();
     453
     454                                        selection.setStartAfter( startNode[ 0 ] );
     455                                        selection.setEndBefore( endNode[ 0 ] );
     456
     457                                        editor.selection.setRng( selection );
     458                                }
     459
     460                                scrollVisualModeToStartElement( editor, startNode );
     461                        }
     462
     463                        if ( startNode.hasClass( 'mce_SELRES_start_target' ) ) {
     464                                startNode.removeClass( 'mce_SELRES_start_target' );
     465                        }
     466                        else {
     467                                startNode.remove();
     468                        }
     469                        endNode.remove();
     470                }
     471
     472                /**
     473                 * @summary Scrolls the content to place the selected element in the center of the screen.
     474                 *
     475                 * Takes an element, that is usually the selection start element, selected in
     476                 * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly
     477                 * in the middle of the screen.
     478                 *
     479                 * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted
     480                 * from the window height, to get the proper viewport window, that the user sees.
     481                 *
     482                 * @param {Object} editor TinyMCE editor instance.
     483                 * @param {Object} element HTMLElement that should be scrolled into view.
     484                 */
     485                function scrollVisualModeToStartElement( editor, element ) {
     486                        /**
     487                         * TODO:
     488                         *  * Decide if we should animate the transition or not ( motion sickness/accessibility )
     489                         */
     490                        var elementTop = editor.$( element ).offset().top;
     491                        var TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top;
     492
     493                        var edTools = $('#wp-content-editor-tools');
     494                        var edToolsHeight = edTools.height();
     495                        var edToolsOffsetTop = edTools.offset().top;
     496
     497                        var toolbarHeight = getToolbarHeight( editor );
     498
     499                        var windowHeight = window.innerHeight
     500                                || document.documentElement.clientHeight
     501                                || document.body.clientHeight;
     502
     503                        var selectionPosition = TinyMCEContentAreaTop + elementTop;
     504                        var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
     505
     506                        /**
     507                         * The minimum scroll height should be to the top of the editor, to offer a consistent
     508                         * experience.
     509                         *
     510                         * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and
     511                         * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
     512                         * the top of the viewport (under the Master Bar)
     513                         */
     514                        var adjustedScroll = Math.max(selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight);
     515
     516
     517                        $( 'body' ).animate( {
     518                                scrollTop: parseInt( adjustedScroll, 10 )
     519                        }, 100 );
     520                }
     521
     522                /**
     523                 * This method was extracted from the `SaveContent` hook in
     524                 * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`.
     525                 *
     526                 * It's needed here, since the method changes the content a bit, which confuses the cursor position.
     527                 *
     528                 * @param {Object} event TinyMCE event object.
     529                 */
     530                function fixTextAreaContent( event ) {
     531                        // Keep empty paragraphs :(
     532                        event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p>&nbsp;</p>' );
     533                }
     534
     535                /**
     536                 * @summary Finds the current selection position in the Visual editor.
     537                 *
     538                 * Find the current selection in the Visual editor by inserting marker elements at the start
     539                 * and end of the selection.
     540                 *
     541                 * Uses the standard DOM selection API to achieve that goal.
     542                 *
     543                 * Check the notes in the comments in the code below for more information on some gotchas
     544                 * and why this solution was chosen.
     545                 *
     546                 * @param {Object} editor The editor where we must find the selection
     547                 * @returns {(null|Object)} The selection range position in the editor
     548                 */
     549                function findBookmarkedPosition( editor ) {
     550                        // Get the TinyMCE `window` reference, since we need to access the raw selection.
     551                        var TinyMCEWIndow = editor.getWin(),
     552                                selection = TinyMCEWIndow.getSelection();
     553
     554                        if ( selection.rangeCount <= 0 ) {
     555                                // no selection, no need to continue.
     556                                return;
     557                        }
     558
     559                        /**
     560                         * The ID is used to avoid replacing user generated content, that may coincide with the
     561                         * format specified below.
     562                         * @type {string}
     563                         */
     564                        var selectionID = 'SELRES_' + Math.random();
     565
     566                        /**
     567                         * Create two marker elements that will be used to mark the start and the end of the range.
     568                         *
     569                         * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
     570                         * random content flickering in the editor when switching between modes.
     571                         */
     572                        var spanSkeleton = getCursorMarkerSpan(editor, selectionID);
     573
     574                        var startElement = spanSkeleton.clone().addClass('mce_SELRES_start');
     575                        var endElement = spanSkeleton.clone().addClass('mce_SELRES_end');
     576
     577                        /**
     578                         * Inspired by:
     579                         * @link https://stackoverflow.com/a/17497803/153310
     580                         *
     581                         * Why do it this way and not with TinyMCE's bookmarks?
     582                         *
     583                         * TinyMCE's bookmarks are very nice when working with selections and positions, BUT
     584                         * there is no way to determine the precise position of the bookmark when switching modes, since
     585                         * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify
     586                         * HTML code and so on. In this process, the bookmark markup gets lost.
     587                         *
     588                         * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML
     589                         * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will
     590                         * throw off the positioning.
     591                         *
     592                         * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the
     593                         * selection.
     594                         *
     595                         * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates
     596                         * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to
     597                         * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the
     598                         * selection may start in the middle of one node and end in the middle of a completely different one. If we
     599                         * wrap the selection in another node, this will create artifacts in the content.
     600                         *
     601                         * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection.
     602                         * This helps us not break the content and also gives us the option to work with multi-node selections without
     603                         * breaking the markup.
     604                         */
     605                        var range = selection.getRangeAt( 0 ),
     606                                startNode = range.startContainer,
     607                                startOffset = range.startOffset,
     608                                boundaryRange = range.cloneRange();
     609
     610                        boundaryRange.collapse( false );
     611                        boundaryRange.insertNode( endElement[0] );
     612
     613                        /**
     614                         * Sometimes the selection starts at the `<img>` tag, which makes the
     615                         * boundary range `insertNode` insert `startElement` inside the `<img>` tag itself, i.e.:
     616                         *
     617                         * `<img><span class="mce_SELRES_start"...>...</span></img>`
     618                         *
     619                         * As this is an invalid syntax, it breaks the selection.
     620                         *
     621                         * The conditional below checks if `startNode` is a tag that suffer from that and
     622                         * manually inserts the selection start maker before it.
     623                         *
     624                         * In the future this will probably include a list of tags, not just `<img>`, depending on the needs.
     625                         */
     626                        if ( startNode && startNode.tagName && startNode.tagName.toLowerCase() === 'img' ) {
     627                                editor.$( startNode ).before( startElement[ 0 ] );
     628                        }
     629                        else {
     630                                boundaryRange.setStart( startNode, startOffset );
     631                                boundaryRange.collapse( true );
     632                                boundaryRange.insertNode( startElement[ 0 ] );
     633                        }
     634
     635
     636                        range.setStartAfter( startElement[0] );
     637                        range.setEndBefore( endElement[0] );
     638                        selection.removeAllRanges();
     639                        selection.addRange( range );
     640
     641                        /**
     642                         * Now the editor's content has the start/end nodes.
     643                         *
     644                         * Unfortunately the content goes through some more changes after this step, before it gets inserted
     645                         * in the `textarea`. This means that we have to do some minor cleanup on our own here.
     646                         */
     647                        editor.on( 'GetContent', fixTextAreaContent );
     648
     649                        var content = removep( editor.getContent() );
     650
     651                        editor.off( 'GetContent', fixTextAreaContent );
     652
     653                        startElement.remove();
     654                        endElement.remove();
     655
     656                        var startRegex = new RegExp(
     657                                '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
     658                        );
     659
     660                        var endRegex = new RegExp(
     661                                '<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
     662                        );
     663
     664                        var startMatch = content.match( startRegex );
     665                        var endMatch = content.match( endRegex );
     666                        if ( ! startMatch ) {
     667                                return null;
     668                        }
     669
     670                        return {
     671                                start: startMatch.index,
     672
     673                                // We need to adjust the end position to discard the length of the range start marker
     674                                end: endMatch
     675                                        ? endMatch.index - startMatch[ 0 ].length
     676                                        : null
     677                        };
     678                }
     679
     680                /**
     681                 * @summary Selects text in the TinyMCE `textarea`.
     682                 *
     683                 * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
     684                 *
     685                 * For `selection` parameter:
     686                 * @see findBookmarkedPosition
     687                 *
     688                 * @param {Object} editor TinyMCE's editor instance.
     689                 * @param {Object} selection Selection data.
     690                 */
     691                function selectTextInTextArea( editor, selection ) {
     692                        // only valid in the text area mode and if we have selection
     693                        if ( ! selection ) {
     694                                return;
     695                        }
     696
     697                        var textArea = editor.getElement(),
     698                                start = selection.start,
     699                                end = selection.end || selection.start;
     700
     701                        if ( textArea.focus ) {
     702                                // focus and scroll to the position
     703                                setTimeout( function() {
     704                                        if ( textArea.blur ) {
     705                                                // defocus before focusing
     706                                                textArea.blur();
     707                                        }
     708                                        textArea.focus();
     709                                }, 100 );
     710
     711                                textArea.focus();
     712                        }
     713
     714                        textArea.setSelectionRange( start, end );
     715                }
     716
     717                /**
    159718                 * @summary Replaces <p> tags with two line breaks. "Opposite" of wpautop().
    160719                 *
    161720                 * Replaces <p> tags with two line breaks except where the <p> has attributes.