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