* @output wp-admin/js/editor.js
window.wp = window.wp || {};
wp.editor = wp.editor || {};
* Utility functions for the editor.
function SwitchEditors() {
if ( ! tinymce && window.tinymce ) {
tinymce = window.tinymce;
* Handles onclick events for the Visual/Text tabs.
$$( document ).on( 'click', function( event ) {
target = $$( event.target );
if ( target.hasClass( 'wp-switch-editor' ) ) {
id = target.attr( 'data-wp-editor-id' );
mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html';
switchEditor( id, mode );
* Returns the height of the editor toolbar(s) in px.
* @param {Object} editor The TinyMCE editor.
* @return {number} If the height is between 10 and 200 return the height,
function getToolbarHeight( editor ) {
var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0],
height = node && node.clientHeight;
if ( height && height > 10 && height < 200 ) {
return parseInt( height, 10 );
* Switches the editor between Visual and Text mode.
* @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`.
function switchEditor( id, mode ) {
var editorHeight, toolbarHeight, iframe,
editor = tinymce.get( id ),
wrap = $$( '#wp-' + id + '-wrap' ),
$textarea = $$( '#' + id ),
if ( 'toggle' === mode ) {
if ( editor && ! editor.isHidden() ) {
if ( 'tmce' === mode || 'tinymce' === mode ) {
// If the editor is visible we are already in `tinymce` mode.
if ( editor && ! editor.isHidden() ) {
// Insert closing tags for any open tags in QuickTags.
if ( typeof( window.QTags ) !== 'undefined' ) {
window.QTags.closeAllTags( id );
editorHeight = parseInt( textarea.style.height, 10 ) || 0;
var keepSelection = false;
keepSelection = editor.getParam( 'wp_keep_scroll_position' );
keepSelection = window.tinyMCEPreInit.mceInit[ id ] &&
window.tinyMCEPreInit.mceInit[ id ].wp_keep_scroll_position;
addHTMLBookmarkInTextAreaContent( $textarea );
// 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 ) {
editor.theme.resizeTo( null, editorHeight );
if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
// Restore the selection.
focusHTMLBookmarkInVisualEditor( editor );
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() ) {
// 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;
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 );
selectTextInTextArea( editor, selectionRange );
// 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' );
* Checks if a cursor is inside an HTML tag or comment.
* 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]<img.../>..`.
* @param {string} content The test content where the cursor is.
* @param {number} cursorPosition The cursor position inside the content.
* @return {(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 - 1 ),
lastGtPos = content.lastIndexOf( '>', cursorPosition );
if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
var tagContent = content.substr( lastLtPos ),
tagMatch = tagContent.match( /<\s*(\/)?(\w+|\!-{2}.*-{2})/ );
var tagType = tagMatch[2],
closingGt = tagContent.indexOf( '>' );
gtPos: lastLtPos + closingGt + 1, // Offset by one to get the position _after_ the character.
isClosingTag: !! tagMatch[1]
* Checks 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 or after the shortcode.
* For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
* `[caption]<span>ThisIsGone</span><img .../>[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 ) {
* 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 ),
for ( var i = 0; i < shortcodes.length; i++ ) {
var shortcode = shortcodes[ i ].replace( /^\[+/g, '' );
if ( result.indexOf( shortcode ) === -1 ) {
result.push( shortcode );
* Gets 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 ) {
var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
shortcodeMatch, // Define local scope for the variable to be used in the loop below.
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] === '[';
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
var urlRegexp = new RegExp(
'(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi'
while ( shortcodeMatch = urlRegexp.exec( content ) ) {
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 cursor marker element.
function getCursorMarkerSpan( domLib, content ) {
return domLib( '<span>' ).css( {
.html( content ? content : '' );
* Gets adjusted selection cursor positions according to HTML tags, comments, and 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 ) {
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'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.
isCursorStartInTag = getContainingTagInfo( content, cursorStart );
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
* This is done in order to make selection of text a bit more consistent when selecting text in
* 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;
cursorStart = isCursorStartInTag.gtPos;
var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
if ( isCursorEndInTag ) {
cursorEnd = isCursorEndInTag.gtPos;
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;
cursorStart = isCursorStartInShortcode.startIndex;
var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) {
if ( isCursorEndInShortcode.urlAtEndOfContent ) {
cursorEnd = isCursorEndInShortcode.startIndex;
cursorEnd = isCursorEndInShortcode.endIndex;
cursorStart: cursorStart,
* 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
function addHTMLBookmarkInTextAreaContent( $textarea ) {
if ( ! $textarea || ! $textarea.length ) {
// If no valid $textarea object is provided, there's nothing we can do.
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',
cursorMarkerSkeleton = getCursorMarkerSpan( $$, '' ).attr( 'data-mce-type','bookmark' );
if ( mode === 'range' ) {