` 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 ) { htmlModeCursorStartPosition = isCursorStartInTag.ltPos; } else { htmlModeCursorStartPosition = isCursorStartInTag.gtPos; } } var isCursorEndInTag = getContainingTagInfo( textArea.value, htmlModeCursorEndPosition ); if ( isCursorEndInTag ) { htmlModeCursorEndPosition = isCursorEndInTag.gtPos; } var mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single'; var selectedText = null; var 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 = ''; } selectedText = [ markedText, bookMarkEnd ].join( '' ); } textArea.value = [ 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 ].join( '' ); } /** * @summary Focus 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' ), 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 ] ); } else { var selection = editor.getDoc().createRange(); 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' ); } else { startNode.remove(); } endNode.remove(); } /** * @summary 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 ) { /** * 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 edTools = $('#wp-content-editor-tools'); var edToolsHeight = edTools.height(); var edToolsOffsetTop = edTools.offset().top; var toolbarHeight = getToolbarHeight( editor ); var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; var selectionPosition = TinyMCEContentAreaTop + elementTop; var visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight ); /** * The minimum scroll height should be to the top of the editor, to offer a consistent * experience. * * 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) */ var adjustedScroll = Math.max(selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight); $( 'body' ).animate( { scrollTop: parseInt( adjustedScroll, 10 ) }, 100 ); } /** * 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( /
(?:
|\u00a0|\uFEFF| )*<\/p>/g, '
' ); } /** * @summary 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 * @returns {(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.rangeCount <= 0 ) { // no selection, no need to continue. return; } /** * The ID is used to avoid replacing user generated content, that may coincide with the * format specified below. * @type {string} */ 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); var startElement = spanSkeleton.clone().addClass('mce_SELRES_start'); var endElement = spanSkeleton.clone().addClass('mce_SELRES_end'); /** * Inspired by: * @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 * selection. * * 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 * breaking the markup. */ var range = selection.getRangeAt( 0 ), startNode = range.startContainer, 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 ( startNode && startNode.tagName && startNode.tagName.toLowerCase() === 'img' ) { editor.$( startNode ).before( startElement[ 0 ] ); } else { 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 ); startElement.remove(); endElement.remove(); var startRegex = new RegExp( ']*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' ); var endRegex = new RegExp( ']*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' ); var startMatch = content.match( startRegex ); var endMatch = content.match( endRegex ); if ( ! startMatch ) { return null; } return { start: startMatch.index, // We need to adjust the end position to discard the length of the range start marker end: endMatch ? endMatch.index - startMatch[ 0 ].length : null }; } /** * @summary Selects text in the TinyMCE `textarea`. * * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`. * * For `selection` parameter: * @see 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 if ( ! selection ) { return; } var textArea = editor.getElement(), start = selection.start, end = selection.end || selection.start; if ( textArea.focus ) { // focus and scroll to the position setTimeout( function() { if ( textArea.blur ) { // defocus before focusing textArea.blur(); } textArea.focus(); }, 100 ); textArea.focus(); } textArea.setSelectionRange( start, end ); } /** * @summary Replaces
tags with two line breaks. "Opposite" of wpautop(). * * Replaces
tags with two line breaks except where the
has attributes. * Unifies whitespace. * Indents