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

Props biskobe.
See #42029.

git-svn-id: https://develop.svn.wordpress.org/trunk@41645 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Andrew Ozz 2017-09-29 17:49:43 +00:00
parent cc08743ec5
commit 6b72687e10
1 changed files with 306 additions and 171 deletions

View File

@ -133,10 +133,10 @@ window.wp = window.wp || {};
*/ */
var tinyMCEConfig = $.extend( var tinyMCEConfig = $.extend(
{}, {},
window.tinyMCEPreInit.mceInit[id], window.tinyMCEPreInit.mceInit[ id ],
{ {
setup: function(editor) { setup: function( editor ) {
editor.on('init', function(event) { editor.on( 'init', function( event ) {
focusHTMLBookmarkInVisualEditor( event.target ); focusHTMLBookmarkInVisualEditor( event.target );
}); });
} }
@ -210,72 +210,156 @@ window.wp = window.wp || {};
* @returns {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag. * @returns {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag.
*/ */
function getContainingTagInfo( content, cursorPosition ) { function getContainingTagInfo( content, cursorPosition ) {
var lastLtPos = content.lastIndexOf( '<', cursorPosition ), var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ),
lastGtPos = content.lastIndexOf( '>', cursorPosition ); lastGtPos = content.lastIndexOf( '>', cursorPosition );
if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) { if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
// find what the tag is // find what the tag is
var tagContent = content.substr( lastLtPos ); var tagContent = content.substr( lastLtPos ),
var tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ ); tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
if ( ! tagMatch ) { if ( ! tagMatch ) {
return null; return null;
} }
var tagType = tagMatch[ 2 ]; var tagType = tagMatch[2],
var closingGt = tagContent.indexOf( '>' ); closingGt = tagContent.indexOf( '>' );
var isClosingTag = ! ! tagMatch[ 1 ];
var shortcodeWrapperInfo = getShortcodeWrapperInfo( content, lastLtPos );
return { return {
ltPos: lastLtPos, ltPos: lastLtPos,
gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character, gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character,
tagType: tagType, tagType: tagType,
isClosingTag: isClosingTag, isClosingTag: !! tagMatch[1]
shortcodeTagInfo: shortcodeWrapperInfo
}; };
} }
return null; return null;
} }
/** /**
* @summary Check if a given HTML tag is enclosed in a shortcode tag * @summary Check if the cursor is inside a shortcode
* *
* If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to
* move the selection marker to before the short tag. * move the selection marker to before or after the shortcode.
* *
* For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
* `<img/>` tag inside. * `<img/>` tag inside.
* *
* `[caption]<span>ThisIsGone</span><img .../>[caption]` * `[caption]<span>ThisIsGone</span><img .../>[caption]`
* *
* Moving the selection to before the short code is better, since it allows to select * Moving the selection to before or after the short code is better, since it allows to select
* something, instead of just losing focus and going to the start of the content. * something, instead of just losing focus and going to the start of the content.
* *
* @param {string} content The text content to check against * @param {string} content The text content to check against.
* @param {number} cursorPosition The cursor position to check from. Usually this is the opening symbol of * @param {number} cursorPosition The cursor position to check.
* an HTML tag.
* *
* @return {(null|Object)} Null if the oject is not wrapped in a shortcode tag. * @return {(undefined|Object)} Undefined if the cursor is not wrapped in a shortcode tag.
* Information about the wrapping shortcode tag if it's wrapped in one. * Information about the wrapping shortcode tag if it's wrapped in one.
*/ */
function getShortcodeWrapperInfo( content, cursorPosition ) { function getShortcodeWrapperInfo( content, cursorPosition ) {
if ( content.substr( cursorPosition - 1, 1 ) === ']' ) { var contentShortcodes = getShortCodePositionsInText( content );
var shortTagStart = content.lastIndexOf( '[', cursorPosition );
var shortTagContent = content.substr(shortTagStart, cursorPosition - shortTagStart);
var shortTag = content.match( /\[\s*(\/)?(\w+)/ );
var tagType = shortTag[ 2 ];
var closingGt = shortTagContent.indexOf( '>' );
var isClosingTag = ! ! shortTag[ 1 ];
return { return _.find( contentShortcodes, function( element ) {
openingBracket: shortTagStart, return cursorPosition >= element.startIndex && cursorPosition <= element.endIndex;
shortcode: tagType, } );
closingBracket: closingGt,
isClosingTag: isClosingTag
};
} }
return null; /**
* Gets a list of unique shortcodes or shortcode-look-alikes in the content.
*
* @param {string} content The content we want to scan for shortcodes.
*/
function getShortcodesInText( content ) {
var shortcodes = content.match( /\[+([\w_-])+/g );
return _.uniq(
_.map( shortcodes, function( element ) {
return element.replace( /^\[+/g, '' );
} )
);
}
/**
* @summary Check if a shortcode has Live Preview enabled for it.
*
* Previewable shortcodes here refers to shortcodes that have Live Preview enabled.
*
* These shortcodes get rewritten when the editor is in Visual mode, which means that
* we don't want to change anything inside them, i.e. inserting a selection marker
* inside the shortcode will break it :(
*
* @link wp-includes/js/mce-view.js
*
* @param {string} shortcode The shortcode to check.
* @return {boolean} If a shortcode has Live Preview or not
*/
function isShortcodePreviewable( shortcode ) {
var defaultPreviewableShortcodes = [ 'caption' ];
return (
defaultPreviewableShortcodes.indexOf( shortcode ) !== -1 ||
wp.mce.views.get( shortcode ) !== undefined
);
}
/**
* @summary Get all shortcodes and their positions in the content
*
* This function returns all the shortcodes that could be found in the textarea content
* along with their character positions and boundaries.
*
* This is used to check if the selection cursor is inside the boundaries of a shortcode
* and move it accordingly, to avoid breakage.
*
* @link adjustTextAreaSelectionCursors
*
* The information can also be used in other cases when we need to lookup shortcode data,
* as it's already structured!
*
* @param {string} content The content we want to scan for shortcodes
*/
function getShortCodePositionsInText( content ) {
var allShortcodes = getShortcodesInText( content );
if ( allShortcodes.length === 0 ) {
return [];
}
var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
shortcodeMatch, // Define local scope for the variable to be used in the loop below.
shortcodesDetails = [];
while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) {
/**
* Check if the shortcode should be shown as plain text.
*
* This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode
* and just shows it as text.
*/
var showAsPlainText = shortcodeMatch[1] === '[';
/**
* For more context check the docs for:
*
* @link isShortcodePreviewable
*
* In addition, if the shortcode will get rendered as plain text ( see above ),
* we can treat it as text and use the selection markers in it.
*/
var isPreviewable = ! showAsPlainText && isShortcodePreviewable( shortcodeMatch[2] ),
shortcodeInfo = {
shortcodeName: shortcodeMatch[2],
showAsPlainText: showAsPlainText,
startIndex: shortcodeMatch.index,
endIndex: shortcodeMatch.index + shortcodeMatch[0].length,
length: shortcodeMatch[0].length,
isPreviewable: isPreviewable
};
shortcodesDetails.push( shortcodeInfo );
}
return shortcodesDetails;
} }
/** /**
@ -299,27 +383,30 @@ window.wp = window.wp || {};
} }
/** /**
* @summary Adds text selection markers in the editor textarea. * @summary Get adjusted selection cursor positions according to HTML tags/shortcodes
* *
* Adds selection markers in the content of the editor `textarea`. * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
* The method directly manipulates the `textarea` content, to allow TinyMCE plugins * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
* to run after the markers are added. * to break the syntax and render the HTML tag or shortcode broken.
* *
* @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object * @link getShortcodeWrapperInfo
* @param {object} jQuery A jQuery instance *
* @param {string} content Textarea content that the cursors are in
* @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions
*
* @return {{cursorStart: number, cursorEnd: number}}
*/ */
function addHTMLBookmarkInTextAreaContent( $textarea, jQuery ) { function adjustTextAreaSelectionCursors( content, cursorPositions ) {
var textArea = $textarea[ 0 ], // TODO add error checking
htmlModeCursorStartPosition = textArea.selectionStart,
htmlModeCursorEndPosition = textArea.selectionEnd;
var voidElements = [ var voidElements = [
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
]; ];
var cursorStart = cursorPositions.cursorStart,
cursorEnd = cursorPositions.cursorEnd,
// check if the cursor is in a tag and if so, adjust it // check if the cursor is in a tag and if so, adjust it
var isCursorStartInTag = getContainingTagInfo( textArea.value, htmlModeCursorStartPosition ); isCursorStartInTag = getContainingTagInfo( content, cursorStart );
if ( isCursorStartInTag ) { if ( isCursorStartInTag ) {
/** /**
* Only move to the start of the HTML tag (to select the whole element) if the tag * Only move to the start of the HTML tag (to select the whole element) if the tag
@ -334,78 +421,74 @@ window.wp = window.wp || {};
* In cases where the tag is not a void element, the cursor is put to the end of the tag, * In cases where the tag is not a void element, the cursor is put to the end of the tag,
* so it's either between the opening and closing tag elements or after the closing tag. * so it's either between the opening and closing tag elements or after the closing tag.
*/ */
if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== - 1 ) { if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
htmlModeCursorStartPosition = isCursorStartInTag.ltPos; cursorStart = isCursorStartInTag.ltPos;
} }
else { else {
htmlModeCursorStartPosition = isCursorStartInTag.gtPos; cursorStart = isCursorStartInTag.gtPos;
} }
} }
var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition ); var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
if ( isCursorEndInTag ) { if ( isCursorEndInTag ) {
htmlModeCursorEndPosition = isCursorEndInTag.gtPos; cursorEnd = isCursorEndInTag.gtPos;
} }
var mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single'; var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
if ( isCursorStartInShortcode && isCursorStartInShortcode.isPreviewable ) {
cursorStart = isCursorStartInShortcode.startIndex;
}
var selectedText = null; var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
var cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' ); if ( isCursorEndInShortcode && isCursorEndInShortcode.isPreviewable ) {
cursorEnd = isCursorEndInShortcode.endIndex;
}
return {
cursorStart: cursorStart,
cursorEnd: cursorEnd
};
}
/**
* @summary Adds text selection markers in the editor textarea.
*
* Adds selection markers in the content of the editor `textarea`.
* The method directly manipulates the `textarea` content, to allow TinyMCE plugins
* to run after the markers are added.
*
* @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
* @param {object} jQuery A jQuery instance
*/
function addHTMLBookmarkInTextAreaContent( $textarea, jQuery ) {
if ( ! $textarea || ! $textarea.length ) {
// If no valid $textarea object is provided, there's nothing we can do.
return;
}
var textArea = $textarea[0],
textAreaContent = textArea.value,
adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
cursorStart: textArea.selectionStart,
cursorEnd: textArea.selectionEnd
} ),
htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
selectedText = null,
cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '&#65279;' );
if ( mode === 'range' ) { if ( mode === 'range' ) {
var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ); var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
/**
* Since the shortcodes convert the tags in them a bit, we need to mark the tag itself,
* and not rely on the cursor marker.
*
* @see getShortcodeWrapperInfo
*/
if ( isCursorStartInTag && isCursorStartInTag.shortcodeTagInfo ) {
// Get the tag on the cursor start
var tagEndPosition = isCursorStartInTag.gtPos - isCursorStartInTag.ltPos;
var tagContent = markedText.slice( 0, tagEndPosition );
// Check if the tag already has a `class` attribute.
var classMatch = /class=(['"])([^$1]*?)\1/;
/**
* Add a marker class to the selected tag, to be used later.
*
* @see focusHTMLBookmarkInVisualEditor
*/
if ( tagContent.match( classMatch ) ) {
tagContent = tagContent.replace( classMatch, 'class=$1$2 mce_SELRES_start_target$1' );
}
else {
tagContent = tagContent.replace( /(<\w+)/, '$1 class="mce_SELRES_start_target" ' );
}
// Update the selected text content with the marked tag above
markedText = [
tagContent,
markedText.substr( tagEndPosition )
].join( '' );
}
var bookMarkEnd = cursorMarkerSkeleton.clone()
.addClass( 'mce_SELRES_end' )[ 0 ].outerHTML;
/**
* A small workaround when selecting just a single HTML tag inside a shortcode.
*
* This removes the end selection marker, to make sure the HTML tag is the only selected
* thing. This prevents the selection to appear like it contains multiple items in it (i.e.
* all highlighted blue)
*/
if ( isCursorStartInTag && isCursorStartInTag.shortcodeTagInfo && isCursorEndInTag &&
isCursorStartInTag.ltPos === isCursorEndInTag.ltPos ) {
bookMarkEnd = '';
}
selectedText = [ selectedText = [
markedText, markedText,
bookMarkEnd bookMarkEnd[0].outerHTML
].join( '' ); ].join( '' );
} }
@ -432,34 +515,66 @@ window.wp = window.wp || {};
var startNode = editor.$( '.mce_SELRES_start' ), var startNode = editor.$( '.mce_SELRES_start' ),
endNode = editor.$( '.mce_SELRES_end' ); endNode = editor.$( '.mce_SELRES_end' );
if ( ! startNode.length ) {
startNode = editor.$( '.mce_SELRES_start_target' );
}
if ( startNode.length ) { if ( startNode.length ) {
editor.focus(); editor.focus();
if ( ! endNode.length ) { if ( ! endNode.length ) {
editor.selection.select( startNode[ 0 ] ); editor.selection.select( startNode[0] );
} else { } else {
var selection = editor.getDoc().createRange(); var selection = editor.getDoc().createRange();
selection.setStartAfter( startNode[ 0 ] ); selection.setStartAfter( startNode[0] );
selection.setEndBefore( endNode[ 0 ] ); selection.setEndBefore( endNode[0] );
editor.selection.setRng( selection ); editor.selection.setRng( selection );
} }
}
scrollVisualModeToStartElement( editor, startNode ); scrollVisualModeToStartElement( editor, startNode );
removeSelectionMarker( editor, startNode );
removeSelectionMarker( editor, endNode );
} }
if ( startNode.hasClass( 'mce_SELRES_start_target' ) ) { /**
startNode.removeClass( 'mce_SELRES_start_target' ); * @summary Remove selection marker with optional `<p>` parent.
*
* By default TinyMCE puts every inline node at the main level in a `<p>` wrapping tag.
*
* In the case with selection markers, when removed they leave an empty `<p>` behind,
* which adds an empty paragraph line with `&nbsp;` when switched to Text mode.
*
* In order to prevent that the wrapping `<p>` needs to be removed when removing the
* selection marker.
*
* @param {object} editor The TinyMCE Editor instance
* @param {object} marker The marker to be removed from the editor DOM
*/
function removeSelectionMarker( editor, marker ) {
var markerParent = editor.$( marker ).parent();
if (
! markerParent.length ||
markerParent.prop('tagName').toLowerCase() !== 'p' ||
markerParent[0].childNodes.length > 1 ||
! markerParent.prop('outerHTML').match(/^<p>/)
) {
/**
* The selection marker is not self-contained in a <p>.
* In this case only the selection marker is removed, since
* it will affect the content.
*/
marker.remove();
} }
else { else {
startNode.remove(); /**
* The marker is self-contained in an blank `<p>` tag.
*
* This is usually inserted by TinyMCE
*/
markerParent.remove();
} }
endNode.remove();
} }
/** /**
@ -476,23 +591,19 @@ window.wp = window.wp || {};
* @param {Object} element HTMLElement that should be scrolled into view. * @param {Object} element HTMLElement that should be scrolled into view.
*/ */
function scrollVisualModeToStartElement( editor, element ) { function scrollVisualModeToStartElement( editor, element ) {
/** var elementTop = editor.$( element ).offset().top,
* TODO: TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
* * Decide if we should animate the transition or not ( motion sickness/accessibility )
*/
var elementTop = editor.$( element ).offset().top;
var TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top;
var edTools = $('#wp-content-editor-tools'); edTools = $( '#wp-content-editor-tools' ),
var edToolsHeight = edTools.height(); edToolsHeight = edTools.height(),
var edToolsOffsetTop = edTools.offset().top; edToolsOffsetTop = edTools.offset().top,
var toolbarHeight = getToolbarHeight( editor ); toolbarHeight = getToolbarHeight( editor ),
var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
var selectionPosition = TinyMCEContentAreaTop + elementTop; selectionPosition = TinyMCEContentAreaTop + elementTop,
var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight ); visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
/** /**
* The minimum scroll height should be to the top of the editor, to offer a consistent * The minimum scroll height should be to the top of the editor, to offer a consistent
@ -502,10 +613,9 @@ window.wp = window.wp || {};
* subtracting the height. This gives the scroll position where the top of the editor tools aligns with * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
* the top of the viewport (under the Master Bar) * the top of the viewport (under the Master Bar)
*/ */
var adjustedScroll = Math.max(selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight); var adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
$( 'html,body' ).animate( {
$( 'body' ).animate( {
scrollTop: parseInt( adjustedScroll, 10 ) scrollTop: parseInt( adjustedScroll, 10 )
}, 100 ); }, 100 );
} }
@ -560,10 +670,9 @@ window.wp = window.wp || {};
* The elements have hardcoded style that makes them invisible. This is done to avoid seeing * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
* random content flickering in the editor when switching between modes. * random content flickering in the editor when switching between modes.
*/ */
var spanSkeleton = getCursorMarkerSpan(editor, selectionID); var spanSkeleton = getCursorMarkerSpan( editor, selectionID ),
startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
var startElement = spanSkeleton.clone().addClass('mce_SELRES_start'); endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
var endElement = spanSkeleton.clone().addClass('mce_SELRES_end');
/** /**
* Inspired by: * Inspired by:
@ -598,36 +707,39 @@ window.wp = window.wp || {};
startOffset = range.startOffset, startOffset = range.startOffset,
boundaryRange = range.cloneRange(); boundaryRange = range.cloneRange();
/**
* If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
* which we have to account for.
*/
if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) {
startNode = editor.$( '[data-mce-selected]' )[0];
/**
* Marking the start and end element with `data-mce-object-selection` helps
* discern when the selected object is a Live Preview selection.
*
* This way we can adjust the selection to properly select only the content, ignoring
* whitespace inserted around the selected object by the Editor.
*/
startElement.attr('data-mce-object-selection', 'true');
endElement.attr('data-mce-object-selection', 'true');
editor.$( startNode ).before( startElement[0] );
editor.$( startNode ).after( endElement[0] );
}
else {
boundaryRange.collapse( false ); boundaryRange.collapse( false );
boundaryRange.insertNode( endElement[0] ); boundaryRange.insertNode( endElement[0] );
/**
* Sometimes the selection starts at the `<img>` tag, which makes the
* boundary range `insertNode` insert `startElement` inside the `<img>` tag itself, i.e.:
*
* `<img><span class="mce_SELRES_start"...>...</span></img>`
*
* As this is an invalid syntax, it breaks the selection.
*
* The conditional below checks if `startNode` is a tag that suffer from that and
* manually inserts the selection start maker before it.
*
* In the future this will probably include a list of tags, not just `<img>`, depending on the needs.
*/
if ( startNode && startNode.tagName && startNode.tagName.toLowerCase() === 'img' ) {
editor.$( startNode ).before( startElement[ 0 ] );
}
else {
boundaryRange.setStart( startNode, startOffset ); boundaryRange.setStart( startNode, startOffset );
boundaryRange.collapse( true ); boundaryRange.collapse( true );
boundaryRange.insertNode( startElement[ 0 ] ); boundaryRange.insertNode( startElement[0] );
}
range.setStartAfter( startElement[0] ); range.setStartAfter( startElement[0] );
range.setEndBefore( endElement[0] ); range.setEndBefore( endElement[0] );
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange( range ); selection.addRange( range );
}
/** /**
* Now the editor's content has the start/end nodes. * Now the editor's content has the start/end nodes.
@ -645,24 +757,47 @@ window.wp = window.wp || {};
endElement.remove(); endElement.remove();
var startRegex = new RegExp( var startRegex = new RegExp(
'<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
); );
var endRegex = new RegExp( var endRegex = new RegExp(
'<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
); );
var startMatch = content.match( startRegex ); var startMatch = content.match( startRegex ),
var endMatch = content.match( endRegex ); endMatch = content.match( endRegex );
if ( ! startMatch ) { if ( ! startMatch ) {
return null; return null;
} }
return { var startIndex = startMatch.index,
start: startMatch.index, startMatchLength = startMatch[0].length,
endIndex = null;
if (endMatch) {
/**
* Adjust the selection index, if the selection contains a Live Preview object or not.
*
* Check where the `data-mce-object-selection` attribute is set above for more context.
*/
if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
startMatchLength -= startMatch[1].length;
}
var endMatchIndex = endMatch.index;
if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
endMatchIndex -= endMatch[1].length;
}
// We need to adjust the end position to discard the length of the range start marker // We need to adjust the end position to discard the length of the range start marker
end: endMatch ? endMatch.index - startMatch[ 0 ].length : null endIndex = endMatchIndex - startMatchLength;
}
return {
start: startIndex,
end: endIndex
}; };
} }
@ -672,7 +807,7 @@ window.wp = window.wp || {};
* Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`. * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
* *
* For `selection` parameter: * For `selection` parameter:
* @see findBookmarkedPosition * @link findBookmarkedPosition
* *
* @param {Object} editor TinyMCE's editor instance. * @param {Object} editor TinyMCE's editor instance.
* @param {Object} selection Selection data. * @param {Object} selection Selection data.