Make WordPress Core

Ticket #42029: 42029.diff

File 42029.diff, 24.5 KB (added by biskobe, 8 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 e34d208396..6d94c03112 100644
    a b window.wp = window.wp || {}; 
    133133                                         */
    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                                                                });
    142142                                                        }
    window.wp = window.wp || {}; 
    210210                 * @returns {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag.
    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 {
    230229                                        ltPos: lastLtPos,
    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                        }
    237235                        return null;
    238236                }
    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]`
     247                 * `[caption]<span>ThisIsGone</span><img .../>[caption]`
    250248                 *
    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.
     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.
    253251                 *
    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.
     252                 * @param {string} content The text content to check against.
     253                 * @param {number} cursorPosition    The cursor position to check.
    257254                 *
    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.
     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 ];
     259                        var contentShortcodes = getShortCodePositionsInText( content );
    269260
    270                                 return {
    271                                         openingBracket: shortTagStart,
    272                                         shortcode: tagType,
    273                                         closingBracket: closingGt,
    274                                         isClosingTag: isClosingTag
    275                                 };
     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 [];
    276326                        }
    277327
    278                         return null;
     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
    281365                /**
    window.wp = window.wp || {}; 
    299383                }
    300384
    301385                /**
    302                  * @summary Adds text selection markers in the editor textarea.
     386                 * @summary Get adjusted selection cursor positions according to HTML tags/shortcodes
    303387                 *
    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.
     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.
    307391                 *
    308                  * @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
    309                  * @param {object} jQuery A jQuery instance
     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}}
    310398                 */
    311                 function addHTMLBookmarkInTextAreaContent( $textarea, jQuery ) {
    312                         var textArea = $textarea[ 0 ], // TODO add error checking
    313                                 htmlModeCursorStartPosition = textArea.selectionStart,
    314                                 htmlModeCursorEndPosition = textArea.selectionEnd;
    315 
     399                function adjustTextAreaSelectionCursors( content, cursorPositions ) {
    316400                        var voidElements = [
    317401                                'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
    318402                                'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
    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                                /**
    325412                                 * Only move to the start of the HTML tag (to select the whole element) if the tag
    window.wp = window.wp || {}; 
    334421                                 * In cases where the tag is not a void element, the cursor is put to the end of the tag,
    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;
     428                                        cursorStart = isCursorStartInTag.gtPos;
    342429                                }
    343430                        }
    344431
    345                         var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition );
     432                        var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
    346433                        if ( isCursorEndInTag ) {
    347                                 htmlModeCursorEndPosition = isCursorEndInTag.gtPos;
     434                                cursorEnd = isCursorEndInTag.gtPos;
    348435                        }
    349436
    350                         var mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single';
     437                        var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
     438                        if ( isCursorStartInShortcode && isCursorStartInShortcode.isPreviewable ) {
     439                                cursorStart = isCursorStartInShortcode.startIndex;
     440                        }
    351441
    352                         var selectedText = null;
    353                         var cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
     442                        var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
     443                        if ( isCursorEndInShortcode && isCursorEndInShortcode.isPreviewable ) {
     444                                cursorEnd = isCursorEndInShortcode.endIndex;
     445                        }
    354446
    355                         if ( mode === 'range' ) {
    356                                 var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition );
     447                        return {
     448                                cursorStart: cursorStart,
     449                                cursorEnd: cursorEnd
     450                        };
     451                }
    357452
    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 );
     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                        }
    368468
    369                                         // Check if the tag already has a `class` attribute.
    370                                         var classMatch = /class=(['"])([^$1]*?)\1/;
     469                        var textArea = $textarea[0],
     470                                textAreaContent = textArea.value,
    371471
    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                                         }
     472                                adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
     473                                        cursorStart: textArea.selectionStart,
     474                                        cursorEnd: textArea.selectionEnd
     475                                } ),
    383476
    384                                         // Update the selected text content with the marked tag above
    385                                         markedText = [
    386                                                 tagContent,
    387                                                 markedText.substr( tagEndPosition )
    388                                         ].join( '' );
    389                                 }
     477                                htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
     478                                htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
    390479
    391                                 var bookMarkEnd = cursorMarkerSkeleton.clone()
    392                                         .addClass( 'mce_SELRES_end' )[ 0 ].outerHTML;
     480                                mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
    393481
    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                                 }
     482                                selectedText = null,
     483                                cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
     484
     485                        if ( mode === 'range' ) {
     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                        }
    411494
    window.wp = window.wp || {}; 
    432515                        var startNode = editor.$( '.mce_SELRES_start' ),
    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 );
    454531                        }
    455532
    456                         if ( startNode.hasClass( 'mce_SELRES_start_target' ) ) {
    457                                 startNode.removeClass( 'mce_SELRES_start_target' );
     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();
     571                                /**
     572                                 * The marker is self-contained in an blank `<p>` tag.
     573                                 *
     574                                 * This is usually inserted by TinyMCE
     575                                 */
     576                                markerParent.remove();
    461577                        }
    462                         endNode.remove();
    463578                }
    464579
    465580                /**
    window.wp = window.wp || {}; 
    476591                 * @param {Object} element HTMLElement that should be scrolled into view.
    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;
     594                        var elementTop = editor.$( element ).offset().top,
     595                                TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
    485596
    486                         var edTools = $('#wp-content-editor-tools');
    487                         var edToolsHeight = edTools.height();
    488                         var edToolsOffsetTop = edTools.offset().top;
     597                                edTools = $( '#wp-content-editor-tools' ),
     598                                edToolsHeight = edTools.height(),
     599                                edToolsOffsetTop = edTools.offset().top,
    489600
    490                         var toolbarHeight = getToolbarHeight( editor );
     601                                toolbarHeight = getToolbarHeight( editor ),
    491602
    492                         var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
     603                                windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
    493604
    494                         var selectionPosition = TinyMCEContentAreaTop + elementTop;
    495                         var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
     605                                selectionPosition = TinyMCEContentAreaTop + elementTop,
     606                                visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
    496607
    497608                        /**
    498609                         * The minimum scroll height should be to the top of the editor, to offer a consistent
    window.wp = window.wp || {}; 
    502613                         * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
    503614                         * the top of the viewport (under the Master Bar)
    504615                         */
    505                         var adjustedScroll = Math.max(selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight);
     616                        var adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
    506617
    507 
    508                         $( 'body' ).animate( {
     618                        $( 'html,body' ).animate( {
    509619                                scrollTop: parseInt( adjustedScroll, 10 )
    510620                        }, 100 );
    511621                }
    window.wp = window.wp || {}; 
    560670                         * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
    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                        /**
    569678                         * Inspired by:
    window.wp = window.wp || {}; 
    598707                                startOffset = range.startOffset,
    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 
     736                                boundaryRange.insertNode( startElement[0] );
    626737
    627                         range.setStartAfter( startElement[0] );
    628                         range.setEndBefore( endElement[0] );
    629                         selection.removeAllRanges();
    630                         selection.addRange( range );
     738                                range.setStartAfter( startElement[0] );
     739                                range.setEndBefore( endElement[0] );
     740                                selection.removeAllRanges();
     741                                selection.addRange( range );
     742                        }
    631743
    632744                        /**
    633745                         * Now the editor's content has the start/end nodes.
    window.wp = window.wp || {}; 
    645757                        endElement.remove();
    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
    661                         return {
    662                                 start: startMatch.index,
     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                                }
    663793
    664794                                // 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
     795                                endIndex = endMatchIndex - startMatchLength;
     796                        }
     797
     798                        return {
     799                                start: startIndex,
     800                                end: endIndex
    666801                        };
    667802                }
    668803
    window.wp = window.wp || {}; 
    672807                 * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
    673808                 *
    674809                 * For `selection` parameter:
    675                  * @see findBookmarkedPosition
     810                 * @link findBookmarkedPosition
    676811                 *
    677812                 * @param {Object} editor TinyMCE's editor instance.
    678813                 * @param {Object} selection Selection data.