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:
parent
cc08743ec5
commit
6b72687e10
|
@ -133,10 +133,10 @@ window.wp = window.wp || {};
|
|||
*/
|
||||
var tinyMCEConfig = $.extend(
|
||||
{},
|
||||
window.tinyMCEPreInit.mceInit[id],
|
||||
window.tinyMCEPreInit.mceInit[ id ],
|
||||
{
|
||||
setup: function(editor) {
|
||||
editor.on('init', function(event) {
|
||||
setup: function( editor ) {
|
||||
editor.on( 'init', function( event ) {
|
||||
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.
|
||||
*/
|
||||
function getContainingTagInfo( content, cursorPosition ) {
|
||||
var lastLtPos = content.lastIndexOf( '<', cursorPosition ),
|
||||
var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ),
|
||||
lastGtPos = content.lastIndexOf( '>', cursorPosition );
|
||||
|
||||
if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
|
||||
// find what the tag is
|
||||
var tagContent = content.substr( lastLtPos );
|
||||
var tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
|
||||
var tagContent = content.substr( lastLtPos ),
|
||||
tagMatch = tagContent.match( /<\s*(\/)?(\w+)/ );
|
||||
|
||||
if ( ! tagMatch ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var tagType = tagMatch[ 2 ];
|
||||
var closingGt = tagContent.indexOf( '>' );
|
||||
var isClosingTag = ! ! tagMatch[ 1 ];
|
||||
var shortcodeWrapperInfo = getShortcodeWrapperInfo( content, lastLtPos );
|
||||
var tagType = tagMatch[2],
|
||||
closingGt = tagContent.indexOf( '>' );
|
||||
|
||||
return {
|
||||
ltPos: lastLtPos,
|
||||
gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character,
|
||||
tagType: tagType,
|
||||
isClosingTag: isClosingTag,
|
||||
shortcodeTagInfo: shortcodeWrapperInfo
|
||||
isClosingTag: !! tagMatch[1]
|
||||
};
|
||||
}
|
||||
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
|
||||
* 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
|
||||
* `<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
|
||||
* something, instead of just losing focus and going to the start of the content.
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
* an HTML tag.
|
||||
* @param {string} content The text content to check against.
|
||||
* @param {number} cursorPosition The cursor position to check.
|
||||
*
|
||||
* @return {(null|Object)} Null if the oject is not wrapped in a shortcode tag.
|
||||
* Information about the wrapping shortcode tag if it's wrapped in one.
|
||||
* @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.
|
||||
*/
|
||||
function getShortcodeWrapperInfo( content, cursorPosition ) {
|
||||
if ( content.substr( cursorPosition - 1, 1 ) === ']' ) {
|
||||
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 ];
|
||||
var contentShortcodes = getShortCodePositionsInText( content );
|
||||
|
||||
return {
|
||||
openingBracket: shortTagStart,
|
||||
shortcode: tagType,
|
||||
closingBracket: closingGt,
|
||||
isClosingTag: isClosingTag
|
||||
};
|
||||
return _.find( contentShortcodes, function( element ) {
|
||||
return cursorPosition >= element.startIndex && cursorPosition <= element.endIndex;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
|
||||
return null;
|
||||
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`.
|
||||
* The method directly manipulates the `textarea` content, to allow TinyMCE plugins
|
||||
* to run after the markers are added.
|
||||
* Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
|
||||
* content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
|
||||
* to break the syntax and render the HTML tag or shortcode broken.
|
||||
*
|
||||
* @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
|
||||
* @param {object} jQuery A jQuery instance
|
||||
* @link getShortcodeWrapperInfo
|
||||
*
|
||||
* @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 ) {
|
||||
var textArea = $textarea[ 0 ], // TODO add error checking
|
||||
htmlModeCursorStartPosition = textArea.selectionStart,
|
||||
htmlModeCursorEndPosition = textArea.selectionEnd;
|
||||
|
||||
function adjustTextAreaSelectionCursors( content, cursorPositions ) {
|
||||
var voidElements = [
|
||||
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
||||
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
||||
];
|
||||
|
||||
// check if the cursor is in a tag and if so, adjust it
|
||||
var isCursorStartInTag = getContainingTagInfo( textArea.value, htmlModeCursorStartPosition );
|
||||
var cursorStart = cursorPositions.cursorStart,
|
||||
cursorEnd = cursorPositions.cursorEnd,
|
||||
// check if the cursor is in a tag and if so, adjust it
|
||||
isCursorStartInTag = getContainingTagInfo( content, cursorStart );
|
||||
|
||||
if ( isCursorStartInTag ) {
|
||||
/**
|
||||
* 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,
|
||||
* so it's either between the opening and closing tag elements or after the closing tag.
|
||||
*/
|
||||
if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== - 1 ) {
|
||||
htmlModeCursorStartPosition = isCursorStartInTag.ltPos;
|
||||
if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
|
||||
cursorStart = isCursorStartInTag.ltPos;
|
||||
}
|
||||
else {
|
||||
htmlModeCursorStartPosition = isCursorStartInTag.gtPos;
|
||||
cursorStart = isCursorStartInTag.gtPos;
|
||||
}
|
||||
}
|
||||
|
||||
var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition );
|
||||
var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
|
||||
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 cursorMarkerSkeleton = getCursorMarkerSpan( { $: jQuery }, '' );
|
||||
var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
|
||||
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 }, '' );
|
||||
|
||||
if ( mode === 'range' ) {
|
||||
var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition );
|
||||
|
||||
/**
|
||||
* 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 = '';
|
||||
}
|
||||
var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
|
||||
bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
|
||||
|
||||
selectedText = [
|
||||
markedText,
|
||||
bookMarkEnd
|
||||
bookMarkEnd[0].outerHTML
|
||||
].join( '' );
|
||||
}
|
||||
|
||||
|
@ -432,34 +515,66 @@ window.wp = window.wp || {};
|
|||
var startNode = editor.$( '.mce_SELRES_start' ),
|
||||
endNode = editor.$( '.mce_SELRES_end' );
|
||||
|
||||
if ( ! startNode.length ) {
|
||||
startNode = editor.$( '.mce_SELRES_start_target' );
|
||||
}
|
||||
|
||||
if ( startNode.length ) {
|
||||
editor.focus();
|
||||
|
||||
if ( ! endNode.length ) {
|
||||
editor.selection.select( startNode[ 0 ] );
|
||||
editor.selection.select( startNode[0] );
|
||||
} else {
|
||||
var selection = editor.getDoc().createRange();
|
||||
|
||||
selection.setStartAfter( startNode[ 0 ] );
|
||||
selection.setEndBefore( endNode[ 0 ] );
|
||||
selection.setStartAfter( startNode[0] );
|
||||
selection.setEndBefore( endNode[0] );
|
||||
|
||||
editor.selection.setRng( selection );
|
||||
}
|
||||
|
||||
scrollVisualModeToStartElement( editor, startNode );
|
||||
}
|
||||
|
||||
if ( startNode.hasClass( 'mce_SELRES_start_target' ) ) {
|
||||
startNode.removeClass( 'mce_SELRES_start_target' );
|
||||
scrollVisualModeToStartElement( editor, startNode );
|
||||
|
||||
|
||||
removeSelectionMarker( editor, startNode );
|
||||
removeSelectionMarker( editor, endNode );
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ` ` 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 {
|
||||
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.
|
||||
*/
|
||||
function scrollVisualModeToStartElement( editor, element ) {
|
||||
/**
|
||||
* TODO:
|
||||
* * 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 elementTop = editor.$( element ).offset().top,
|
||||
TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
|
||||
|
||||
var edTools = $('#wp-content-editor-tools');
|
||||
var edToolsHeight = edTools.height();
|
||||
var edToolsOffsetTop = edTools.offset().top;
|
||||
edTools = $( '#wp-content-editor-tools' ),
|
||||
edToolsHeight = edTools.height(),
|
||||
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;
|
||||
var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
|
||||
selectionPosition = TinyMCEContentAreaTop + elementTop,
|
||||
visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 );
|
||||
|
||||
|
||||
$( 'body' ).animate( {
|
||||
$( 'html,body' ).animate( {
|
||||
scrollTop: parseInt( adjustedScroll, 10 )
|
||||
}, 100 );
|
||||
}
|
||||
|
@ -560,10 +670,9 @@ window.wp = window.wp || {};
|
|||
* 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.
|
||||
*/
|
||||
var spanSkeleton = getCursorMarkerSpan(editor, selectionID);
|
||||
|
||||
var startElement = spanSkeleton.clone().addClass('mce_SELRES_start');
|
||||
var endElement = spanSkeleton.clone().addClass('mce_SELRES_end');
|
||||
var spanSkeleton = getCursorMarkerSpan( editor, selectionID ),
|
||||
startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
|
||||
endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
|
||||
|
||||
/**
|
||||
* Inspired by:
|
||||
|
@ -598,37 +707,40 @@ window.wp = window.wp || {};
|
|||
startOffset = range.startOffset,
|
||||
boundaryRange = range.cloneRange();
|
||||
|
||||
boundaryRange.collapse( false );
|
||||
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 the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
|
||||
* which we have to account for.
|
||||
*/
|
||||
if ( startNode && startNode.tagName && startNode.tagName.toLowerCase() === 'img' ) {
|
||||
editor.$( startNode ).before( startElement[ 0 ] );
|
||||
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.insertNode( endElement[0] );
|
||||
|
||||
boundaryRange.setStart( startNode, startOffset );
|
||||
boundaryRange.collapse( true );
|
||||
boundaryRange.insertNode( startElement[ 0 ] );
|
||||
boundaryRange.insertNode( startElement[0] );
|
||||
|
||||
range.setStartAfter( startElement[0] );
|
||||
range.setEndBefore( endElement[0] );
|
||||
selection.removeAllRanges();
|
||||
selection.addRange( range );
|
||||
}
|
||||
|
||||
|
||||
range.setStartAfter( startElement[0] );
|
||||
range.setEndBefore( endElement[0] );
|
||||
selection.removeAllRanges();
|
||||
selection.addRange( range );
|
||||
|
||||
/**
|
||||
* Now the editor's content has the start/end nodes.
|
||||
*
|
||||
|
@ -645,24 +757,47 @@ window.wp = window.wp || {};
|
|||
endElement.remove();
|
||||
|
||||
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(
|
||||
'<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 endMatch = content.match( endRegex );
|
||||
var startMatch = content.match( startRegex ),
|
||||
endMatch = content.match( endRegex );
|
||||
|
||||
if ( ! startMatch ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
start: startMatch.index,
|
||||
var startIndex = 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
|
||||
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`.
|
||||
*
|
||||
* For `selection` parameter:
|
||||
* @see findBookmarkedPosition
|
||||
* @link findBookmarkedPosition
|
||||
*
|
||||
* @param {Object} editor TinyMCE's editor instance.
|
||||
* @param {Object} selection Selection data.
|
||||
|
|
Loading…
Reference in New Issue