: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
textArea.value.slice( 0, htmlModeCursorStartPosition ), // Text until the cursor/selection position.
cursorMarkerSkeleton.clone() // Cursor/selection start marker.
.addClass( 'mce_SELRES_start' )[0].outerHTML,
selectedText, // Selected text with end cursor/position marker.
textArea.value.slice( htmlModeCursorEndPosition ) // Text from last cursor/selection position to end.
* Focuses the selection markers in Visual mode.
* The method checks for existing selection markers inside the editor DOM (Visual mode)
* and create a selection between the two nodes using the DOM `createRange` selection API
* If there is only a single node, select only the single node through TinyMCE's selection API
* @param {Object} editor TinyMCE editor instance.
function focusHTMLBookmarkInVisualEditor( editor ) {
var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ),
endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 );
if ( startNode.length ) {
if ( ! endNode.length ) {
editor.selection.select( startNode[0] );
var selection = editor.getDoc().createRange();
selection.setStartAfter( startNode[0] );
selection.setEndBefore( endNode[0] );
editor.selection.setRng( selection );
if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
scrollVisualModeToStartElement( editor, startNode );
removeSelectionMarker( startNode );
removeSelectionMarker( endNode );
* Removes selection marker and the parent node if it is an empty paragraph.
* By default TinyMCE wraps loose inline tags in a `<p>`.
* When removing selection markers an empty `<p>` may be left behind, remove it.
* @param {Object} $marker The marker to be removed from the editor DOM, wrapped in an instance of `editor.$`
function removeSelectionMarker( $marker ) {
var $markerParent = $marker.parent();
//Remove empty paragraph left over after removing the marker.
if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) {
* Scrolls the content to place the selected element in the center of the screen.
* Takes an element, that is usually the selection start element, selected in
* `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly
* in the middle of the screen.
* I order to achieve the proper positioning, the editor media bar and toolbar are subtracted
* from the window height, to get the proper viewport window, that the user sees.
* @param {Object} editor TinyMCE editor instance.
* @param {Object} element HTMLElement that should be scrolled into view.
function scrollVisualModeToStartElement( editor, element ) {
var elementTop = editor.$( element ).offset().top,
TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
toolbarHeight = getToolbarHeight( editor ),
edTools = $( '#wp-content-editor-tools' ),
edToolsHeight = edTools.height();
edToolsOffsetTop = edTools.offset().top;
var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
selectionPosition = TinyMCEContentAreaTop + elementTop,
visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
// There's no need to scroll if the selection is inside the visible area.
if ( selectionPosition < visibleAreaHeight ) {
* The minimum scroll height should be to the top of the editor, to offer a consistent
* In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and
* 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)
if ( editor.settings.wp_autoresize_on ) {
$scrollArea = $( 'html,body' );
adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
$scrollArea = $( editor.contentDocument ).find( 'html,body' );
adjustedScroll = elementTop;
scrollTop: parseInt( adjustedScroll, 10 )
* This method was extracted from the `SaveContent` hook in
* `wp-includes/js/tinymce/plugins/wordpress/plugin.js`.
* It's needed here, since the method changes the content a bit, which confuses the cursor position.
* @param {Object} event TinyMCE event object.
function fixTextAreaContent( event ) {
// Keep empty paragraphs :(
event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p> </p>' );
* Finds the current selection position in the Visual editor.
* Find the current selection in the Visual editor by inserting marker elements at the start
* and end of the selection.
* Uses the standard DOM selection API to achieve that goal.
* Check the notes in the comments in the code below for more information on some gotchas
* and why this solution was chosen.
* @param {Object} editor The editor where we must find the selection.
* @return {(null|Object)} The selection range position in the editor.
function findBookmarkedPosition( editor ) {
// Get the TinyMCE `window` reference, since we need to access the raw selection.
var TinyMCEWindow = editor.getWin(),
selection = TinyMCEWindow.getSelection();
if ( ! selection || selection.rangeCount < 1 ) {
// no selection, no need to continue.
* The ID is used to avoid replacing user generated content, that may coincide with the
* format specified below.
var selectionID = 'SELRES_' + Math.random();
* Create two marker elements that will be used to mark the start and the end of the range.
* 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 ),
startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
* @link https://stackoverflow.com/a/17497803/153310
* Why do it this way and not with TinyMCE's bookmarks?
* TinyMCE's bookmarks are very nice when working with selections and positions, BUT
* there is no way to determine the precise position of the bookmark when switching modes, since
* TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify
* HTML code and so on. In this process, the bookmark markup gets lost.
* If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML
* in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will
* throw off the positioning.
* To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the
* Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates
* a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to
* full nodes. Unfortunately in our case, the user can select whatever they like, which means that the
* selection may start in the middle of one node and end in the middle of a completely different one. If we
* wrap the selection in another node, this will create artifacts in the content.
* Using the method below, we insert the custom `span` nodes at the start and at the end of the selection.
* This helps us not break the content and also gives us the option to work with multi-node selections without
var range = selection.getRangeAt( 0 ),
startNode = range.startContainer,
startOffset = range.startOffset,
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] );
boundaryRange.collapse( false );
boundaryRange.insertNode( endElement[0] );
boundaryRange.setStart( startNode, startOffset );
boundaryRange.collapse( true );
boundaryRange.insertNode( startElement[0] );
range.setStartAfter( startElement[0] );
range.setEndBefore( endElement[0] );
selection.removeAllRanges();
selection.addRange( range );
* Now the editor's content has the start/end nodes.
* Unfortunately the content goes through some more changes after this step, before it gets inserted
* in the `textarea`. This means that we have to do some minor cleanup on our own here.
editor.on( 'GetContent', fixTextAreaContent );
var content = removep( editor.getContent() );
editor.off( 'GetContent', fixTextAreaContent );
var startRegex = new RegExp(
'<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
var endRegex = new RegExp(
'(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
var startMatch = content.match( startRegex ),
endMatch = content.match( endRegex );
var startIndex = startMatch.index,
startMatchLength = startMatch[0].length,
* 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.
endIndex = endMatchIndex - startMatchLength;
* Selects text in the TinyMCE `textarea`.
* Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
* For `selection` parameter:
* @link findBookmarkedPosition
* @param {Object} editor TinyMCE's editor instance.
* @param {Object} selection Selection data.
function selectTextInTextArea( editor, selection ) {
// Only valid in the text area mode and if we have selection.
var textArea = editor.getElement(),
end = selection.end || selection.start;
// Wait for the Visual editor to be hidden, then focus and scroll to the position.
textArea.setSelectionRange( start, end );
// Defocus before focusing.
// Restore the selection when the editor is initialized. Needed when the Text editor is the default.
$( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) {
if ( editor.$( '.mce_SELRES_start' ).length ) {
focusHTMLBookmarkInVisualEditor( editor );
* Replaces <p> tags with two line breaks. "Opposite" of wpautop().
* Replaces <p> tags with two line breaks except where the <p> has attributes.
* Indents <li>, <dt> and <dd> for better readability.
* @memberof switchEditors
* @param {string} html The content from the editor.
* @return {string} The content with stripped paragraph tags.
function removep( html ) {
var blocklist = 'blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure',
blocklist1 = blocklist + '|div|p',
blocklist2 = blocklist + '|pre',
preserve_linebreaks = false,
// Protect script and style tags.
if ( html.indexOf( '<script' ) !== -1 || html.indexOf( '<style' ) !== -1 ) {
html = html.replace( /<(script|style)[^>]*>[\s\S]*?<\/\1>/g, function( match ) {
if ( html.indexOf( '<pre' ) !== -1 ) {
preserve_linebreaks = true;
html = html.replace( /<pre[^>]*>[\s\S]+?<\/pre>/g, function( a ) {
a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' );
a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' );
return a.replace( /\r?\n/g, '<wp-line-break>' );
// Remove line breaks but keep <br> tags inside image captions.
if ( html.indexOf( '[caption' ) !== -1 ) {
html = html.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' );
// Normalize white space characters before and after block tags.
html = html.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' );
html = html.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' );
// Mark </p> if it has any attributes.
html = html.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' );
// Preserve the first <p> inside a <div>.
html = html.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' );
// Remove paragraph tags.
html = html.replace( /\s*<p>/gi, '' );
html = html.replace( /\s*<\/p>\s*/gi, '\n\n' );
// Normalize white space chars and remove multiple line breaks.
html = html.replace( /\n[\s\u00a0]+\n/g, '\n\n' );
// Replace <br> tags with line breaks.
html = html.replace( /(\s*)<br ?\/?>\s*/gi, function( match, space ) {
if ( space && space.indexOf( '\n' ) !== -1 ) {
// Fix line breaks around <div>.
html = html.replace( /\s*<div/g, '\n<div' );
html = html.replace( /<\/div>\s*/g, '</div>\n' );
// Fix line breaks around caption shortcodes.
html = html.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' );
html = html.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' );
// Pad block elements tags with a line break.
html = html.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' );
html = html.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' );
// Indent <li>, <dt> and <dd> tags.
html = html.replace( /<((li|dt|dd)[^>]*)>/g, ' \t<$1>' );
// Fix line breaks around <select> and <option>.
if ( html.indexOf( '<option' ) !== -1 ) {
html = html.replace( /\s*<option/g, '\n<option' );
html = html.replace( /\s*<\/select>/g, '\n</select>' );
// Pad <hr> with two line breaks.
if ( html.indexOf( '<hr' ) !== -1 ) {
html = html.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' );
// Remove line breaks in <object> tags.
if ( html.indexOf( '<object' ) !== -1 ) {
html = html.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
return a.replace( /[\r\n]+/g, '' );
// Unmark special paragraph closing tags.
html = html.replace( /<\/p#>/g, '</p>\n' );
// Pad remaining <p> tags whit a line break.
html = html.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' );
html = html.replace( /^\s+/, '' );
html = html.replace( /[\s\u00a0]+$/, '' );
if ( preserve_linebreaks ) {
html = html.replace( /<wp-line-break>/g, '\n' );
html = html.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
// Restore preserved tags.
html = html.replace( /<wp-preserve>/g, function() {
* Replaces two line breaks with a paragraph tag and one line break with a <br>.