Make WordPress Core

Changeset 41645


Ignore:
Timestamp:
09/29/2017 05:49:43 PM (7 years ago)
Author:
azaozz
Message:

Editor: Improve keeping text selection when switching between Visual and Text modes.

Props biskobe.
See #42029.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/editor.js

    r41630 r41645  
    134134                    var tinyMCEConfig = $.extend(
    135135                        {},
    136                         window.tinyMCEPreInit.mceInit[id],
     136                        window.tinyMCEPreInit.mceInit[ id ],
    137137                        {
    138                             setup: function(editor) {
    139                                 editor.on('init', function(event) {
     138                            setup: function( editor ) {
     139                                editor.on( 'init', function( event ) {
    140140                                    focusHTMLBookmarkInVisualEditor( event.target );
    141141                                });
     
    211211         */
    212212        function getContainingTagInfo( content, cursorPosition ) {
    213             var lastLtPos = content.lastIndexOf( '<', cursorPosition ),
     213            var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ),
    214214                lastGtPos = content.lastIndexOf( '>', cursorPosition );
    215215
    216216            if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
    217217                // find what the tag is
    218                 var tagContent = content.substr( lastLtPos );
    219                 var tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
     218                var tagContent = content.substr( lastLtPos ),
     219                    tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
     220
    220221                if ( ! tagMatch ) {
    221222                    return null;
    222223                }
    223224
    224                 var tagType = tagMatch[ 2 ];
    225                 var closingGt = tagContent.indexOf( '>' );
    226                 var isClosingTag = ! ! tagMatch[ 1 ];
    227                 var shortcodeWrapperInfo = getShortcodeWrapperInfo( content, lastLtPos );
     225                var tagType = tagMatch[2],
     226                    closingGt = tagContent.indexOf( '>' );
    228227
    229228                return {
     
    231230                    gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character,
    232231                    tagType: tagType,
    233                     isClosingTag: isClosingTag,
    234                     shortcodeTagInfo: shortcodeWrapperInfo
     232                    isClosingTag: !! tagMatch[1]
    235233                };
    236234            }
     
    239237
    240238        /**
    241          * @summary Check if a given HTML tag is enclosed in a shortcode tag
     239         * @summary Check if the cursor is inside a shortcode
    242240         *
    243241         * 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.
     242         * move the selection marker to before or after the shortcode.
    245243         *
    246244         * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
    247245         * `<img/>` tag inside.
    248246         *
    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.
     247         * `[caption]<span>ThisIsGone</span><img .../>[caption]`
     248         *
     249         * Moving the selection to before or after the short code is better, since it allows to select
     250         * something, instead of just losing focus and going to the start of the content.
     251         *
     252         * @param {string} content The text content to check against.
     253         * @param {number} cursorPosition    The cursor position to check.
     254         *
     255         * @return {(undefined|Object)} Undefined if the cursor is not wrapped in a shortcode tag.
     256         *                                Information about the wrapping shortcode tag if it's wrapped in one.
    260257         */
    261258        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;
     259            var contentShortcodes = getShortCodePositionsInText( content );
     260
     261            return _.find( contentShortcodes, function( element ) {
     262                return cursorPosition >= element.startIndex && cursorPosition <= element.endIndex;
     263            } );
     264        }
     265
     266        /**
     267         * Gets a list of unique shortcodes or shortcode-look-alikes in the content.
     268         *
     269         * @param {string} content The content we want to scan for shortcodes.
     270         */
     271        function getShortcodesInText( content ) {
     272            var shortcodes = content.match( /\[+([\w_-])+/g );
     273
     274            return _.uniq(
     275                _.map( shortcodes, function( element ) {
     276                    return element.replace( /^\[+/g, '' );
     277                } )
     278            );
     279        }
     280
     281        /**
     282         * @summary Check if a shortcode has Live Preview enabled for it.
     283         *
     284         * Previewable shortcodes here refers to shortcodes that have Live Preview enabled.
     285         *
     286         * These shortcodes get rewritten when the editor is in Visual mode, which means that
     287         * we don't want to change anything inside them, i.e. inserting a selection marker
     288         * inside the shortcode will break it :(
     289         *
     290         * @link wp-includes/js/mce-view.js
     291         *
     292         * @param {string} shortcode The shortcode to check.
     293         * @return {boolean} If a shortcode has Live Preview or not
     294         */
     295        function isShortcodePreviewable( shortcode ) {
     296            var defaultPreviewableShortcodes = [ 'caption' ];
     297
     298            return (
     299                defaultPreviewableShortcodes.indexOf( shortcode ) !== -1 ||
     300                wp.mce.views.get( shortcode ) !== undefined
     301            );
     302
     303        }
     304
     305        /**
     306         * @summary Get all shortcodes and their positions in the content
     307         *
     308         * This function returns all the shortcodes that could be found in the textarea content
     309         * along with their character positions and boundaries.
     310         *
     311         * This is used to check if the selection cursor is inside the boundaries of a shortcode
     312         * and move it accordingly, to avoid breakage.
     313         *
     314         * @link adjustTextAreaSelectionCursors
     315         *
     316         * The information can also be used in other cases when we need to lookup shortcode data,
     317         * as it's already structured!
     318         *
     319         * @param {string} content The content we want to scan for shortcodes
     320         */
     321        function getShortCodePositionsInText( content ) {
     322            var allShortcodes = getShortcodesInText( content );
     323
     324            if ( allShortcodes.length === 0 ) {
     325                return [];
     326            }
     327
     328            var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
     329                shortcodeMatch, // Define local scope for the variable to be used in the loop below.
     330                shortcodesDetails = [];
     331
     332            while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) {
     333                /**
     334                 * Check if the shortcode should be shown as plain text.
     335                 *
     336                 * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode
     337                 * and just shows it as text.
     338                 */
     339                var showAsPlainText = shortcodeMatch[1] === '[';
     340
     341                /**
     342                 * For more context check the docs for:
     343                 *
     344                 * @link isShortcodePreviewable
     345                 *
     346                 * In addition, if the shortcode will get rendered as plain text ( see above ),
     347                 * we can treat it as text and use the selection markers in it.
     348                 */
     349                var isPreviewable = ! showAsPlainText && isShortcodePreviewable( shortcodeMatch[2] ),
     350                    shortcodeInfo = {
     351                        shortcodeName: shortcodeMatch[2],
     352                        showAsPlainText: showAsPlainText,
     353                        startIndex: shortcodeMatch.index,
     354                        endIndex: shortcodeMatch.index + shortcodeMatch[0].length,
     355                        length: shortcodeMatch[0].length,
     356                        isPreviewable: isPreviewable
     357                    };
     358
     359                shortcodesDetails.push( shortcodeInfo );
     360            }
     361
     362            return shortcodesDetails;
    279363        }
    280364
     
    300384
    301385        /**
    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 
     386         * @summary Get adjusted selection cursor positions according to HTML tags/shortcodes
     387         *
     388         * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
     389         * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
     390         * to break the syntax and render the HTML tag or shortcode broken.
     391         *
     392         * @link getShortcodeWrapperInfo
     393         *
     394         * @param {string} content Textarea content that the cursors are in
     395         * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions
     396         *
     397         * @return {{cursorStart: number, cursorEnd: number}}
     398         */
     399        function adjustTextAreaSelectionCursors( content, cursorPositions ) {
    316400            var voidElements = [
    317401                'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
     
    319403            ];
    320404
    321             // check if the cursor is in a tag and if so, adjust it
    322             var isCursorStartInTag = getContainingTagInfo( textArea.value, htmlModeCursorStartPosition );
     405            var cursorStart = cursorPositions.cursorStart,
     406                cursorEnd = cursorPositions.cursorEnd,
     407                // check if the cursor is in a tag and if so, adjust it
     408                isCursorStartInTag = getContainingTagInfo( content, cursorStart );
     409
    323410            if ( isCursorStartInTag ) {
    324411                /**
     
    335422                 * so it's either between the opening and closing tag elements or after the closing tag.
    336423                 */
    337                 if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== - 1 ) {
    338                     htmlModeCursorStartPosition = isCursorStartInTag.ltPos;
     424                if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
     425                    cursorStart = isCursorStartInTag.ltPos;
    339426                }
    340427                else {
    341                     htmlModeCursorStartPosition = isCursorStartInTag.gtPos;
    342                 }
    343             }
    344 
    345             var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition );
     428                    cursorStart = isCursorStartInTag.gtPos;
     429                }
     430            }
     431
     432            var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
    346433            if ( isCursorEndInTag ) {
    347                 htmlModeCursorEndPosition = isCursorEndInTag.gtPos;
    348             }
    349 
    350             var mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single';
    351 
    352             var selectedText = null;
    353             var cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
     434                cursorEnd = isCursorEndInTag.gtPos;
     435            }
     436
     437            var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
     438            if ( isCursorStartInShortcode && isCursorStartInShortcode.isPreviewable ) {
     439                cursorStart = isCursorStartInShortcode.startIndex;
     440            }
     441
     442            var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
     443            if ( isCursorEndInShortcode && isCursorEndInShortcode.isPreviewable ) {
     444                cursorEnd = isCursorEndInShortcode.endIndex;
     445            }
     446
     447            return {
     448                cursorStart: cursorStart,
     449                cursorEnd: cursorEnd
     450            };
     451        }
     452
     453        /**
     454         * @summary Adds text selection markers in the editor textarea.
     455         *
     456         * Adds selection markers in the content of the editor `textarea`.
     457         * The method directly manipulates the `textarea` content, to allow TinyMCE plugins
     458         * to run after the markers are added.
     459         *
     460         * @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
     461         * @param {object} jQuery A jQuery instance
     462         */
     463        function addHTMLBookmarkInTextAreaContent( $textarea, jQuery ) {
     464            if ( ! $textarea || ! $textarea.length ) {
     465                // If no valid $textarea object is provided, there's nothing we can do.
     466                return;
     467            }
     468
     469            var textArea = $textarea[0],
     470                textAreaContent = textArea.value,
     471
     472                adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
     473                    cursorStart: textArea.selectionStart,
     474                    cursorEnd: textArea.selectionEnd
     475                } ),
     476
     477                htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
     478                htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
     479
     480                mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
     481
     482                selectedText = null,
     483                cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
    354484
    355485            if ( mode === 'range' ) {
    356                 var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition );
    357 
    358                 /**
    359                  * Since the shortcodes convert the tags in them a bit, we need to mark the tag itself,
    360                  * and not rely on the cursor marker.
    361                  *
    362                  * @see getShortcodeWrapperInfo
    363                  */
    364                 if ( isCursorStartInTag && isCursorStartInTag.shortcodeTagInfo ) {
    365                     // Get the tag on the cursor start
    366                     var tagEndPosition = isCursorStartInTag.gtPos - isCursorStartInTag.ltPos;
    367                     var tagContent = markedText.slice( 0, tagEndPosition );
    368 
    369                     // Check if the tag already has a `class` attribute.
    370                     var classMatch = /class=(['"])([^$1]*?)\1/;
    371 
    372                     /**
    373                      * Add a marker class to the selected tag, to be used later.
    374                      *
    375                      * @see focusHTMLBookmarkInVisualEditor
    376                      */
    377                     if ( tagContent.match( classMatch ) ) {
    378                         tagContent = tagContent.replace( classMatch, 'class=$1$2 mce_SELRES_start_target$1' );
    379                     }
    380                     else {
    381                         tagContent = tagContent.replace( /(<\w+)/, '$1 class="mce_SELRES_start_target" ' );
    382                     }
    383 
    384                     // Update the selected text content with the marked tag above
    385                     markedText = [
    386                         tagContent,
    387                         markedText.substr( tagEndPosition )
    388                     ].join( '' );
    389                 }
    390 
    391                 var bookMarkEnd = cursorMarkerSkeleton.clone()
    392                     .addClass( 'mce_SELRES_end' )[ 0 ].outerHTML;
    393 
    394                 /**
    395                  * A small workaround when selecting just a single HTML tag inside a shortcode.
    396                  *
    397                  * This removes the end selection marker, to make sure the HTML tag is the only selected
    398                  * thing. This prevents the selection to appear like it contains multiple items in it (i.e.
    399                  * all highlighted blue)
    400                  */
    401                 if ( isCursorStartInTag && isCursorStartInTag.shortcodeTagInfo && isCursorEndInTag &&
    402                         isCursorStartInTag.ltPos === isCursorEndInTag.ltPos ) {
    403                     bookMarkEnd = '';
    404                 }
     486                var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
     487                    bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
    405488
    406489                selectedText = [
    407490                    markedText,
    408                     bookMarkEnd
     491                    bookMarkEnd[0].outerHTML
    409492                ].join( '' );
    410493            }
     
    433516                endNode = editor.$( '.mce_SELRES_end' );
    434517
    435             if ( ! startNode.length ) {
    436                 startNode = editor.$( '.mce_SELRES_start_target' );
    437             }
    438 
    439518            if ( startNode.length ) {
    440519                editor.focus();
    441520
    442521                if ( ! endNode.length ) {
    443                     editor.selection.select( startNode[ 0 ] );
     522                    editor.selection.select( startNode[0] );
    444523                } else {
    445524                    var selection = editor.getDoc().createRange();
    446525
    447                     selection.setStartAfter( startNode[ 0 ] );
    448                     selection.setEndBefore( endNode[ 0 ] );
     526                    selection.setStartAfter( startNode[0] );
     527                    selection.setEndBefore( endNode[0] );
    449528
    450529                    editor.selection.setRng( selection );
    451530                }
    452 
    453                 scrollVisualModeToStartElement( editor, startNode );
    454             }
    455 
    456             if ( startNode.hasClass( 'mce_SELRES_start_target' ) ) {
    457                 startNode.removeClass( 'mce_SELRES_start_target' );
     531            }
     532
     533            scrollVisualModeToStartElement( editor, startNode );
     534
     535
     536            removeSelectionMarker( editor, startNode );
     537            removeSelectionMarker( editor, endNode );
     538        }
     539
     540        /**
     541         * @summary Remove selection marker with optional `<p>` parent.
     542         *
     543         * By default TinyMCE puts every inline node at the main level in a `<p>` wrapping tag.
     544         *
     545         * In the case with selection markers, when removed they leave an empty `<p>` behind,
     546         * which adds an empty paragraph line with `&nbsp;` when switched to Text mode.
     547         *
     548         * In order to prevent that the wrapping `<p>` needs to be removed when removing the
     549         * selection marker.
     550         *
     551         * @param {object} editor The TinyMCE Editor instance
     552         * @param {object} marker The marker to be removed from the editor DOM
     553         */
     554        function removeSelectionMarker( editor, marker ) {
     555            var markerParent = editor.$( marker ).parent();
     556
     557            if (
     558                ! markerParent.length ||
     559                markerParent.prop('tagName').toLowerCase() !== 'p' ||
     560                markerParent[0].childNodes.length > 1 ||
     561                ! markerParent.prop('outerHTML').match(/^<p>/)
     562            ) {
     563                /**
     564                 * The selection marker is not self-contained in a <p>.
     565                 * In this case only the selection marker is removed, since
     566                 * it will affect the content.
     567                 */
     568                marker.remove();
    458569            }
    459570            else {
    460                 startNode.remove();
    461             }
    462             endNode.remove();
     571                /**
     572                 * The marker is self-contained in an blank `<p>` tag.
     573                 *
     574                 * This is usually inserted by TinyMCE
     575                 */
     576                markerParent.remove();
     577            }
    463578        }
    464579
     
    477592         */
    478593        function scrollVisualModeToStartElement( editor, element ) {
    479             /**
    480              * TODO:
    481              *  * Decide if we should animate the transition or not ( motion sickness/accessibility )
    482              */
    483             var elementTop = editor.$( element ).offset().top;
    484             var TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top;
    485 
    486             var edTools = $('#wp-content-editor-tools');
    487             var edToolsHeight = edTools.height();
    488             var edToolsOffsetTop = edTools.offset().top;
    489 
    490             var toolbarHeight = getToolbarHeight( editor );
    491 
    492             var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
    493 
    494             var selectionPosition = TinyMCEContentAreaTop + elementTop;
    495             var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
     594            var elementTop = editor.$( element ).offset().top,
     595                TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
     596
     597                edTools = $( '#wp-content-editor-tools' ),
     598                edToolsHeight = edTools.height(),
     599                edToolsOffsetTop = edTools.offset().top,
     600
     601                toolbarHeight = getToolbarHeight( editor ),
     602
     603                windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
     604
     605                selectionPosition = TinyMCEContentAreaTop + elementTop,
     606                visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
    496607
    497608            /**
     
    503614             * the top of the viewport (under the Master Bar)
    504615             */
    505             var adjustedScroll = Math.max(selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight);
    506 
    507 
    508             $( 'body' ).animate( {
     616            var adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
     617
     618            $( 'html,body' ).animate( {
    509619                scrollTop: parseInt( adjustedScroll, 10 )
    510620            }, 100 );
     
    561671             * random content flickering in the editor when switching between modes.
    562672             */
    563             var spanSkeleton = getCursorMarkerSpan(editor, selectionID);
    564 
    565             var startElement = spanSkeleton.clone().addClass('mce_SELRES_start');
    566             var endElement = spanSkeleton.clone().addClass('mce_SELRES_end');
     673            var spanSkeleton = getCursorMarkerSpan( editor, selectionID ),
     674                startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
     675                endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
    567676
    568677            /**
     
    599708                boundaryRange = range.cloneRange();
    600709
    601             boundaryRange.collapse( false );
    602             boundaryRange.insertNode( endElement[0] );
    603 
    604710            /**
    605              * Sometimes the selection starts at the `<img>` tag, which makes the
    606              * boundary range `insertNode` insert `startElement` inside the `<img>` tag itself, i.e.:
    607              *
    608              * `<img><span class="mce_SELRES_start"...>...</span></img>`
    609              *
    610              * As this is an invalid syntax, it breaks the selection.
    611              *
    612              * The conditional below checks if `startNode` is a tag that suffer from that and
    613              * manually inserts the selection start maker before it.
    614              *
    615              * In the future this will probably include a list of tags, not just `<img>`, depending on the needs.
     711             * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
     712             * which we have to account for.
    616713             */
    617             if ( startNode && startNode.tagName && startNode.tagName.toLowerCase() === 'img' ) {
    618                 editor.$( startNode ).before( startElement[ 0 ] );
     714            if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) {
     715                startNode = editor.$( '[data-mce-selected]' )[0];
     716
     717                /**
     718                 * Marking the start and end element with `data-mce-object-selection` helps
     719                 * discern when the selected object is a Live Preview selection.
     720                 *
     721                 * This way we can adjust the selection to properly select only the content, ignoring
     722                 * whitespace inserted around the selected object by the Editor.
     723                 */
     724                startElement.attr('data-mce-object-selection', 'true');
     725                endElement.attr('data-mce-object-selection', 'true');
     726
     727                editor.$( startNode ).before( startElement[0] );
     728                editor.$( startNode ).after( endElement[0] );
    619729            }
    620730            else {
     731                boundaryRange.collapse( false );
     732                boundaryRange.insertNode( endElement[0] );
     733
    621734                boundaryRange.setStart( startNode, startOffset );
    622735                boundaryRange.collapse( true );
    623                 boundaryRange.insertNode( startElement[ 0 ] );
    624             }
    625 
    626 
    627             range.setStartAfter( startElement[0] );
    628             range.setEndBefore( endElement[0] );
    629             selection.removeAllRanges();
    630             selection.addRange( range );
     736                boundaryRange.insertNode( startElement[0] );
     737
     738                range.setStartAfter( startElement[0] );
     739                range.setEndBefore( endElement[0] );
     740                selection.removeAllRanges();
     741                selection.addRange( range );
     742            }
    631743
    632744            /**
     
    646758
    647759            var startRegex = new RegExp(
    648                 '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
     760                '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
    649761            );
    650762
    651763            var endRegex = new RegExp(
    652                 '<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
     764                '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
    653765            );
    654766
    655             var startMatch = content.match( startRegex );
    656             var endMatch = content.match( endRegex );
     767            var startMatch = content.match( startRegex ),
     768                endMatch = content.match( endRegex );
     769
    657770            if ( ! startMatch ) {
    658771                return null;
    659772            }
    660773
     774            var startIndex = startMatch.index,
     775                startMatchLength = startMatch[0].length,
     776                endIndex = null;
     777
     778            if (endMatch) {
     779                /**
     780                 * Adjust the selection index, if the selection contains a Live Preview object or not.
     781                 *
     782                 * Check where the `data-mce-object-selection` attribute is set above for more context.
     783                 */
     784                if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
     785                    startMatchLength -= startMatch[1].length;
     786                }
     787
     788                var endMatchIndex = endMatch.index;
     789
     790                if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
     791                    endMatchIndex -= endMatch[1].length;
     792                }
     793
     794                // We need to adjust the end position to discard the length of the range start marker
     795                endIndex = endMatchIndex - startMatchLength;
     796            }
     797
    661798            return {
    662                 start: startMatch.index,
    663 
    664                 // We need to adjust the end position to discard the length of the range start marker
    665                 end: endMatch ? endMatch.index - startMatch[ 0 ].length : null
     799                start: startIndex,
     800                end: endIndex
    666801            };
    667802        }
     
    673808         *
    674809         * For `selection` parameter:
    675          * @see findBookmarkedPosition
     810         * @link findBookmarkedPosition
    676811         *
    677812         * @param {Object} editor TinyMCE's editor instance.
Note: See TracChangeset for help on using the changeset viewer.