diff --git a/wp-admin/js/editor.js b/wp-admin/js/editor.js index e34d208396..6d94c03112 100644 --- a/wp-admin/js/editor.js +++ b/wp-admin/js/editor.js @@ -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 * `` tag inside. * - * `[caption]ThisIsGone[caption]` + * `[caption]ThisIsGone[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 `
` parent. + * + * By default TinyMCE puts every inline node at the main level in a `
` wrapping tag. + * + * In the case with selection markers, when removed they leave an empty `
` behind, + * which adds an empty paragraph line with ` ` when switched to Text mode. + * + * In order to prevent that the wrapping `
` 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(/^
/) + ) { + /** + * The selection marker is not self-contained in a
. + * 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 `
` 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 `` tag, which makes the
- * boundary range `insertNode` insert `startElement` inside the `` tag itself, i.e.:
- *
- * `...`
- *
- * 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 ``, 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(
- ']*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
+ ']*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
);
var endRegex = new RegExp(
- ']*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
+ '(\\s*)]*\\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.
diff --git a/wp-admin/js/editor.min.js b/wp-admin/js/editor.min.js
index 4bfc7b4fea..30108fbc62 100644
--- a/wp-admin/js/editor.min.js
+++ b/wp-admin/js/editor.min.js
@@ -1 +1 @@
-window.wp=window.wp||{},function(a,b){function c(){function c(){!s&&window.tinymce&&(s=window.tinymce,t=s.$,t(document).on("click",function(a){var b,c,d=t(a.target);d.hasClass("wp-switch-editor")&&(b=d.attr("data-wp-editor-id"),c=d.hasClass("switch-tmce")?"tmce":"html",e(b,c))}))}function d(a){var b=t(".mce-toolbar-grp",a.getContainer())[0],c=b&&b.clientHeight;return c&&c>10&&c<200?parseInt(c,10):30}function e(b,c){b=b||"content",c=c||"toggle";var e,f,g,h=s.get(b),k=t("#wp-"+b+"-wrap"),l=t("#"+b),o=l[0];if("toggle"===c&&(c=h&&!h.isHidden()?"html":"tmce"),"tmce"===c||"tinymce"===c){if(h&&!h.isHidden())return!1;if("undefined"!=typeof window.QTags&&window.QTags.closeAllTags(b),e=parseInt(o.style.height,10)||0,i(l,a),h)h.show(),!s.Env.iOS&&e&&(f=d(h),e=e-f+14,e>50&&e<5e3&&h.theme.resizeTo(null,e)),j(h);else{var p=a.extend({},window.tinyMCEPreInit.mceInit[b],{setup:function(a){a.on("init",function(a){j(a.target)})}});s.init(p)}k.removeClass("html-active").addClass("tmce-active"),l.attr("aria-hidden",!0),window.setUserSetting("editor","tinymce")}else if("html"===c){if(h&&h.isHidden())return!1;var q=null;h?(s.Env.iOS||(g=h.iframeElement,e=g?parseInt(g.style.height,10):0,e&&(f=d(h),e=e+f-14,e>50&&e<5e3&&(o.style.height=e+"px"))),q=m(h),h.hide(),q&&n(h,q)):l.css({display:"",visibility:""}),k.removeClass("tmce-active").addClass("html-active"),l.attr("aria-hidden",!1),window.setUserSetting("editor","html")}}function f(a,b){var c=a.lastIndexOf("<",b),d=a.lastIndexOf(">",b);if(c>d||">"===a.substr(b,1)){var e=a.substr(c),f=e.match(/<\s*(\/)?(\w+)/);if(!f)return null;var h=f[2],i=e.indexOf(">"),j=!!f[1],k=g(a,c);return{ltPos:c,gtPos:c+i+1,tagType:h,isClosingTag:j,shortcodeTagInfo:k}}return null}function g(a,b){if("]"===a.substr(b-1,1)){var c=a.lastIndexOf("[",b),d=a.substr(c,b-c),e=a.match(/\[\s*(\/)?(\w+)/),f=e[2],g=d.indexOf(">"),h=!!e[1];return{openingBracket:c,shortcode:f,closingBracket:g,isClosingTag:h}}return null}function h(a,b){return a.$("").css({display:"inline-block",width:0,overflow:"hidden","line-height":0}).html(b?b:"")}function i(a,b){var c=a[0],d=c.selectionStart,e=c.selectionEnd,g=["area","base","br","col","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"],i=f(c.value,d);i&&(d=g.indexOf(i.tagType)!==-1?i.ltPos:i.gtPos);var j=f(c.value,e);j&&(e=j.gtPos);var k=d!==e?"range":"single",l=null,m=h({$:b},"");if("range"===k){var n=c.value.slice(d,e);if(i&&i.shortcodeTagInfo){var o=i.gtPos-i.ltPos,p=n.slice(0,o),q=/class=(['"])([^$1]*?)\1/;p=p.match(q)?p.replace(q,"class=$1$2 mce_SELRES_start_target$1"):p.replace(/(<\w+)/,'$1 class="mce_SELRES_start_target" '),n=[p,n.substr(o)].join("")}var r=m.clone().addClass("mce_SELRES_end")[0].outerHTML;i&&i.shortcodeTagInfo&&j&&i.ltPos===j.ltPos&&(r=""),l=[n,r].join("")}c.value=[c.value.slice(0,d),m.clone().addClass("mce_SELRES_start")[0].outerHTML,l,c.value.slice(e)].join("")}function j(a){var b=a.$(".mce_SELRES_start"),c=a.$(".mce_SELRES_end");if(b.length||(b=a.$(".mce_SELRES_start_target")),b.length){if(a.focus(),c.length){var d=a.getDoc().createRange();d.setStartAfter(b[0]),d.setEndBefore(c[0]),a.selection.setRng(d)}else a.selection.select(b[0]);k(a,b)}b.hasClass("mce_SELRES_start_target")?b.removeClass("mce_SELRES_start_target"):b.remove(),c.remove()}function k(b,c){var e=b.$(c).offset().top,f=b.$(b.getContentAreaContainer()).offset().top,g=a("#wp-content-editor-tools"),h=g.height(),i=g.offset().top,j=d(b),k=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,l=f+e,m=k-(h+j),n=Math.max(l-m/2,i-h);a("body").animate({scrollTop:parseInt(n,10)},100)}function l(a){a.content=a.content.replace(/ (?:
|\u00a0|\uFEFF| )*<\/p>/g,"