diff -r 490d5cc509ed -r cf61fcea0001 wp/wp-admin/js/editor.js
--- a/wp/wp-admin/js/editor.js Tue Jun 09 11:14:17 2015 +0000
+++ b/wp/wp-admin/js/editor.js Mon Oct 14 17:39:30 2019 +0200
@@ -1,38 +1,53 @@
-/* global tinymce, tinyMCEPreInit, QTags, setUserSetting */
+window.wp = window.wp || {};
-window.switchEditors = {
+( function( $, wp ) {
+ wp.editor = wp.editor || {};
- switchto: function( el ) {
- var aid = el.id,
- l = aid.length,
- id = aid.substr( 0, l - 5 ),
- mode = aid.substr( l - 4 );
-
- this.go( id, mode );
- },
+ /**
+ * @summary Utility functions for the editor.
+ *
+ * @since 2.5.0
+ */
+ function SwitchEditors() {
+ var tinymce, $$,
+ exports = {};
- // mode can be 'html', 'tmce', or 'toggle'; 'html' is used for the 'Text' editor tab.
- go: function( id, mode ) {
- var t = this, ed, wrap_id, txtarea_el, iframe, editorHeight, toolbarHeight,
- DOM = tinymce.DOM; //DOMUtils outside the editor iframe
-
- id = id || 'content';
- mode = mode || 'toggle';
+ function init() {
+ if ( ! tinymce && window.tinymce ) {
+ tinymce = window.tinymce;
+ $$ = tinymce.$;
- ed = tinymce.get( id );
- wrap_id = 'wp-' + id + '-wrap';
- txtarea_el = DOM.get( id );
+ /**
+ * @summary Handles onclick events for the Visual/Text tabs.
+ *
+ * @since 4.3.0
+ *
+ * @returns {void}
+ */
+ $$( document ).on( 'click', function( event ) {
+ var id, mode,
+ target = $$( event.target );
- if ( 'toggle' === mode ) {
- if ( ed && ! ed.isHidden() ) {
- mode = 'html';
- } else {
- mode = 'tmce';
+ if ( target.hasClass( 'wp-switch-editor' ) ) {
+ id = target.attr( 'data-wp-editor-id' );
+ mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html';
+ switchEditor( id, mode );
+ }
+ });
}
}
- function getToolbarHeight() {
- var node = DOM.select( '.mce-toolbar-grp', ed.getContainer() )[0],
+ /**
+ * @summary Returns the height of the editor toolbar(s) in px.
+ *
+ * @since 3.9.0
+ *
+ * @param {Object} editor The TinyMCE editor.
+ * @returns {number} If the height is between 10 and 200 return the height,
+ * else return 30.
+ */
+ function getToolbarHeight( editor ) {
+ var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0],
height = node && node.clientHeight;
if ( height && height > 10 && height < 200 ) {
@@ -42,283 +57,1353 @@
return 30;
}
- if ( 'tmce' === mode || 'tinymce' === mode ) {
- if ( ed && ! ed.isHidden() ) {
- return false;
- }
-
- if ( typeof( QTags ) !== 'undefined' ) {
- QTags.closeAllTags( id );
- }
-
- editorHeight = txtarea_el ? parseInt( txtarea_el.style.height, 10 ) : 0;
-
- if ( tinyMCEPreInit.mceInit[ id ] && tinyMCEPreInit.mceInit[ id ].wpautop ) {
- txtarea_el.value = t.wpautop( txtarea_el.value );
- }
+ /**
+ * @summary Switches the editor between Visual and Text mode.
+ *
+ * @since 2.5.0
+ *
+ * @memberof switchEditors
+ *
+ * @param {string} id The id of the editor you want to change the editor mode for. Default: `content`.
+ * @param {string} mode The mode you want to switch to. Default: `toggle`.
+ * @returns {void}
+ */
+ function switchEditor( id, mode ) {
+ id = id || 'content';
+ mode = mode || 'toggle';
- if ( ed ) {
- ed.show();
-
- // No point resizing the iframe in iOS
- if ( ! tinymce.Env.iOS && editorHeight ) {
- toolbarHeight = getToolbarHeight();
- editorHeight = editorHeight - toolbarHeight + 14;
+ var editorHeight, toolbarHeight, iframe,
+ editor = tinymce.get( id ),
+ wrap = $$( '#wp-' + id + '-wrap' ),
+ $textarea = $$( '#' + id ),
+ textarea = $textarea[0];
- // height cannot be under 50 or over 5000
- if ( editorHeight > 50 && editorHeight < 5000 ) {
- ed.theme.resizeTo( null, editorHeight );
- }
+ if ( 'toggle' === mode ) {
+ if ( editor && ! editor.isHidden() ) {
+ mode = 'html';
+ } else {
+ mode = 'tmce';
}
- } else {
- tinymce.init( tinyMCEPreInit.mceInit[id] );
}
- DOM.removeClass( wrap_id, 'html-active' );
- DOM.addClass( wrap_id, 'tmce-active' );
- DOM.setAttrib( txtarea_el, 'aria-hidden', true );
- setUserSetting( 'editor', 'tinymce' );
+ if ( 'tmce' === mode || 'tinymce' === mode ) {
+ // If the editor is visible we are already in `tinymce` mode.
+ if ( editor && ! editor.isHidden() ) {
+ return false;
+ }
- } else if ( 'html' === mode ) {
+ // Insert closing tags for any open tags in QuickTags.
+ if ( typeof( window.QTags ) !== 'undefined' ) {
+ window.QTags.closeAllTags( id );
+ }
- if ( ed && ed.isHidden() ) {
- return false;
- }
+ editorHeight = parseInt( textarea.style.height, 10 ) || 0;
- if ( ed ) {
- if ( ! tinymce.Env.iOS ) {
- iframe = DOM.get( id + '_ifr' );
- editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0;
+ var keepSelection = false;
+ if ( editor ) {
+ keepSelection = editor.getParam( 'wp_keep_scroll_position' );
+ } else {
+ keepSelection = window.tinyMCEPreInit.mceInit[ id ] &&
+ window.tinyMCEPreInit.mceInit[ id ].wp_keep_scroll_position;
+ }
- if ( editorHeight ) {
- toolbarHeight = getToolbarHeight();
- editorHeight = editorHeight + toolbarHeight - 14;
+ if ( keepSelection ) {
+ // Save the selection
+ addHTMLBookmarkInTextAreaContent( $textarea );
+ }
+
+ if ( editor ) {
+ editor.show();
- // height cannot be under 50 or over 5000
+ // No point to resize the iframe in iOS.
+ if ( ! tinymce.Env.iOS && editorHeight ) {
+ toolbarHeight = getToolbarHeight( editor );
+ editorHeight = editorHeight - toolbarHeight + 14;
+
+ // Sane limit for the editor height.
if ( editorHeight > 50 && editorHeight < 5000 ) {
- txtarea_el.style.height = editorHeight + 'px';
+ editor.theme.resizeTo( null, editorHeight );
}
}
+
+ if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
+ // Restore the selection
+ focusHTMLBookmarkInVisualEditor( editor );
+ }
+ } else {
+ tinymce.init( window.tinyMCEPreInit.mceInit[ id ] );
+ }
+
+ wrap.removeClass( 'html-active' ).addClass( 'tmce-active' );
+ $textarea.attr( 'aria-hidden', true );
+ window.setUserSetting( 'editor', 'tinymce' );
+
+ } else if ( 'html' === mode ) {
+ // If the editor is hidden (Quicktags is shown) we don't need to switch.
+ if ( editor && editor.isHidden() ) {
+ return false;
}
- ed.hide();
- } else {
- // The TinyMCE instance doesn't exist, run the content through 'pre_wpautop()' and show the textarea
- if ( tinyMCEPreInit.mceInit[ id ] && tinyMCEPreInit.mceInit[ id ].wpautop ) {
- txtarea_el.value = t.pre_wpautop( txtarea_el.value );
+ if ( editor ) {
+ // Don't resize the textarea in iOS. The iframe is forced to 100% height there, we shouldn't match it.
+ if ( ! tinymce.Env.iOS ) {
+ iframe = editor.iframeElement;
+ editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0;
+
+ if ( editorHeight ) {
+ toolbarHeight = getToolbarHeight( editor );
+ editorHeight = editorHeight + toolbarHeight - 14;
+
+ // Sane limit for the textarea height.
+ if ( editorHeight > 50 && editorHeight < 5000 ) {
+ textarea.style.height = editorHeight + 'px';
+ }
+ }
+ }
+
+ var selectionRange = null;
+
+ if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
+ selectionRange = findBookmarkedPosition( editor );
+ }
+
+ editor.hide();
+
+ if ( selectionRange ) {
+ selectTextInTextArea( editor, selectionRange );
+ }
+ } else {
+ // There is probably a JS error on the page. The TinyMCE editor instance doesn't exist. Show the textarea.
+ $textarea.css({ 'display': '', 'visibility': '' });
+ }
+
+ wrap.removeClass( 'tmce-active' ).addClass( 'html-active' );
+ $textarea.attr( 'aria-hidden', false );
+ window.setUserSetting( 'editor', 'html' );
+ }
+ }
+
+ /**
+ * @summary Checks if a cursor is inside an HTML tag.
+ *
+ * In order to prevent breaking HTML tags when selecting text, the cursor
+ * must be moved to either the start or end of the tag.
+ *
+ * This will prevent the selection marker to be inserted in the middle of an HTML tag.
+ *
+ * This function gives information whether the cursor is inside a tag or not, as well as
+ * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag,
+ * e.g. `[caption]` tag inside.
+ *
+ * `[caption]ThisIsGone
[caption]`
+ *
+ * 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.
+ *
+ * @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 ) {
+ var contentShortcodes = getShortCodePositionsInText( content );
+
+ for ( var i = 0; i < contentShortcodes.length; i++ ) {
+ var element = contentShortcodes[ i ];
+
+ if ( cursorPosition >= element.startIndex && cursorPosition <= element.endIndex ) {
+ return element;
+ }
+ }
+ }
+
+ /**
+ * 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 ),
+ result = [];
+
+ if ( shortcodes ) {
+ for ( var i = 0; i < shortcodes.length; i++ ) {
+ var shortcode = shortcodes[ i ].replace( /^\[+/g, '' );
+
+ if ( result.indexOf( shortcode ) === -1 ) {
+ result.push( shortcode );
+ }
+ }
}
- DOM.removeClass( wrap_id, 'tmce-active' );
- DOM.addClass( wrap_id, 'html-active' );
- DOM.setAttrib( txtarea_el, 'aria-hidden', false );
- setUserSetting( 'editor', 'html' );
- }
- return false;
- },
-
- _wp_Nop: function( content ) {
- var blocklist1, blocklist2,
- preserve_linebreaks = false,
- preserve_br = false;
-
- // Protect pre|script tags
- if ( content.indexOf( '
]*>[\s\S]+?<\/\1>/g, function( a ) { - a = a.replace( /
(\r\n|\n)?/g, '' ); - a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, ' ' ); - return a.replace( /\r?\n/g, ' ' ); - }); + return result; } - // keep
tags inside captions and remove line breaks - if ( content.indexOf( '[caption' ) !== -1 ) { - preserve_br = true; - content = content.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) { - return a.replace( /
]*)>/g, '' ).replace( /[\r\n\t]+/, '' ); - }); + /** + * @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 ), shortcodeInfo; + + 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] === '['; + + shortcodeInfo = { + shortcodeName: shortcodeMatch[2], + showAsPlainText: showAsPlainText, + startIndex: shortcodeMatch.index, + endIndex: shortcodeMatch.index + shortcodeMatch[0].length, + length: shortcodeMatch[0].length + }; + + shortcodesDetails.push( shortcodeInfo ); + } + + /** + * Get all URL matches, and treat them as embeds. + * + * Since there isn't a good way to detect if a URL by itself on a line is a previewable + * object, it's best to treat all of them as such. + * + * This means that the selection will capture the whole URL, in a similar way shrotcodes + * are treated. + */ + var urlRegexp = new RegExp( + '(^|[\\n\\r][\\n\\r]| )(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi' + ); + + while ( shortcodeMatch = urlRegexp.exec( content ) ) { + shortcodeInfo = { + shortcodeName: 'url', + showAsPlainText: false, + startIndex: shortcodeMatch.index, + endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length, + length: shortcodeMatch[ 0 ].length, + urlAtStartOfContent: shortcodeMatch[ 1 ] === '', + urlAtEndOfContent: shortcodeMatch[ 3 ] === '' + }; + + shortcodesDetails.push( shortcodeInfo ); + } + + return shortcodesDetails; + } + + /** + * Generate a cursor marker element to be inserted in the content. + * + * `span` seems to be the least destructive element that can be used. + * + * Using DomQuery syntax to create it, since it's used as both text and as a DOM element. + * + * @param {Object} domLib DOM library instance. + * @param {string} content The content to insert into the cusror marker element. + */ + function getCursorMarkerSpan( domLib, content ) { + return domLib( '' ).css( { + display: 'inline-block', + width: 0, + overflow: 'hidden', + 'line-height': 0 + } ) + .html( content ? content : '' ); } - // Pretty it up for the source editor - blocklist1 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|div|h[1-6]|p|fieldset'; - content = content.replace( new RegExp( '\\s*(' + blocklist1 + ')>\\s*', 'g' ), '$1>\n' ); - content = content.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' ); + /** + * @summary Get adjusted selection cursor positions according to HTML tags/shortcodes + * + * 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. + * + * @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 adjustTextAreaSelectionCursors( content, cursorPositions ) { + var voidElements = [ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]; - // Mark
if it has any attributes. - content = content.replace( /(]+>.*?)<\/p>/g, '$1
' ); - - // Separatecontaining- content = content.replace( /
]*)?>\s*/gi, '
\n\n' ); + var cursorStart = cursorPositions.cursorStart, + cursorEnd = cursorPositions.cursorEnd, + // check if the cursor is in a tag and if so, adjust it + isCursorStartInTag = getContainingTagInfo( content, cursorStart ); - // Removeand
- content = content.replace( /\s*/gi, '' ); - content = content.replace( /\s*<\/p>\s*/gi, '\n\n' ); - content = content.replace( /\n[\s\u00a0]+\n/g, '\n\n' ); - content = content.replace( /\s*
\s*/gi, '\n' ); + if ( isCursorStartInTag ) { + /** + * Only move to the start of the HTML tag (to select the whole element) if the tag + * is part of the voidElements list above. + * + * This list includes tags that are self-contained and don't need a closing tag, according to the + * HTML5 specification. + * + * This is done in order to make selection of text a bit more consistent when selecting text in + * `` tags or such. + * + * 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 ) { + cursorStart = isCursorStartInTag.ltPos; + } else { + cursorStart = isCursorStartInTag.gtPos; + } + } + + var isCursorEndInTag = getContainingTagInfo( content, cursorEnd ); + if ( isCursorEndInTag ) { + cursorEnd = isCursorEndInTag.gtPos; + } - // Fix some block element newline issues - content = content.replace( /\s*
\s*/g, '\n' ); - content = content.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' ); - content = content.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' ); + var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart ); + if ( isCursorStartInShortcode && ! isCursorStartInShortcode.showAsPlainText ) { + /** + * If a URL is at the start or the end of the content, + * the selection doesn't work, because it inserts a marker in the text, + * which breaks the embedURL detection. + * + * The best way to avoid that and not modify the user content is to + * adjust the cursor to either after or before URL. + */ + if ( isCursorStartInShortcode.urlAtStartOfContent ) { + cursorStart = isCursorStartInShortcode.endIndex; + } else { + cursorStart = isCursorStartInShortcode.startIndex; + } + } - blocklist2 = 'blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|h[1-6]|pre|fieldset'; - content = content.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' ); - content = content.replace( new RegExp('\\s*(' + blocklist2 + ')>\\s*', 'g' ), '$1>\n' ); - content = content.replace( /]*)>/g, '\t ' ); + var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd ); + if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) { + if ( isCursorEndInShortcode.urlAtEndOfContent ) { + cursorEnd = isCursorEndInShortcode.startIndex; + } else { + cursorEnd = isCursorEndInShortcode.endIndex; + } + } - if ( content.indexOf( '