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(
|
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 }, '' );
|
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' ) {
|
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 ` ` 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.
|
||||||
|
|
Loading…
Reference in New Issue