~canonical-sysadmins/wordpress/4.9.1

« back to all changes in this revision

Viewing changes to wp-admin/js/editor.js

  • Committer: Barry Price
  • Date: 2017-11-17 04:49:02 UTC
  • mfrom: (1.1.30 upstream)
  • Revision ID: barry.price@canonical.com-20171117044902-5frux4ycbq6g9fyf
Merge WP4.9 from upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
99
99
 
100
100
                                editorHeight = parseInt( textarea.style.height, 10 ) || 0;
101
101
 
 
102
                                var keepSelection = false;
 
103
                                if ( editor ) {
 
104
                                        keepSelection = editor.getParam( 'wp_keep_scroll_position' );
 
105
                                } else {
 
106
                                        keepSelection = window.tinyMCEPreInit.mceInit[ id ] &&
 
107
                                                                        window.tinyMCEPreInit.mceInit[ id ].wp_keep_scroll_position;
 
108
                                }
 
109
 
 
110
                                if ( keepSelection ) {
 
111
                                        // Save the selection
 
112
                                        addHTMLBookmarkInTextAreaContent( $textarea );
 
113
                                }
 
114
 
102
115
                                if ( editor ) {
103
116
                                        editor.show();
104
117
 
112
125
                                                        editor.theme.resizeTo( null, editorHeight );
113
126
                                                }
114
127
                                        }
 
128
 
 
129
                                        if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
 
130
                                                // Restore the selection
 
131
                                                focusHTMLBookmarkInVisualEditor( editor );
 
132
                                        }
115
133
                                } else {
116
 
                                        tinymce.init( window.tinyMCEPreInit.mceInit[id] );
 
134
                                        tinymce.init( window.tinyMCEPreInit.mceInit[ id ] );
117
135
                                }
118
136
 
119
137
                                wrap.removeClass( 'html-active' ).addClass( 'tmce-active' );
143
161
                                                }
144
162
                                        }
145
163
 
 
164
                                        var selectionRange = null;
 
165
 
 
166
                                        if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
 
167
                                                selectionRange = findBookmarkedPosition( editor );
 
168
                                        }
 
169
 
146
170
                                        editor.hide();
 
171
 
 
172
                                        if ( selectionRange ) {
 
173
                                                selectTextInTextArea( editor, selectionRange );
 
174
                                        }
147
175
                                } else {
148
176
                                        // There is probably a JS error on the page. The TinyMCE editor instance doesn't exist. Show the textarea.
149
177
                                        $textarea.css({ 'display': '', 'visibility': '' });
156
184
                }
157
185
 
158
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 Check if a shortcode has Live Preview enabled for it.
 
285
                 *
 
286
                 * Previewable shortcodes here refers to shortcodes that have Live Preview enabled.
 
287
                 *
 
288
                 * These shortcodes get rewritten when the editor is in Visual mode, which means that
 
289
                 * we don't want to change anything inside them, i.e. inserting a selection marker
 
290
                 * inside the shortcode will break it :(
 
291
                 *
 
292
                 * @link wp-includes/js/mce-view.js
 
293
                 *
 
294
                 * @param {string} shortcode The shortcode to check.
 
295
                 * @return {boolean} If a shortcode has Live Preview or not
 
296
                 */
 
297
                function isShortcodePreviewable( shortcode ) {
 
298
                        var defaultPreviewableShortcodes = [ 'caption' ];
 
299
 
 
300
                        return (
 
301
                                defaultPreviewableShortcodes.indexOf( shortcode ) !== -1 ||
 
302
                                wp.mce.views.get( shortcode ) !== undefined
 
303
                        );
 
304
 
 
305
                }
 
306
 
 
307
                /**
 
308
                 * @summary Get all shortcodes and their positions in the content
 
309
                 *
 
310
                 * This function returns all the shortcodes that could be found in the textarea content
 
311
                 * along with their character positions and boundaries.
 
312
                 *
 
313
                 * This is used to check if the selection cursor is inside the boundaries of a shortcode
 
314
                 * and move it accordingly, to avoid breakage.
 
315
                 *
 
316
                 * @link adjustTextAreaSelectionCursors
 
317
                 *
 
318
                 * The information can also be used in other cases when we need to lookup shortcode data,
 
319
                 * as it's already structured!
 
320
                 *
 
321
                 * @param {string} content The content we want to scan for shortcodes
 
322
                 */
 
323
                function getShortCodePositionsInText( content ) {
 
324
                        var allShortcodes = getShortcodesInText( content ), shortcodeInfo;
 
325
 
 
326
                        if ( allShortcodes.length === 0 ) {
 
327
                                return [];
 
328
                        }
 
329
 
 
330
                        var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
 
331
                                shortcodeMatch, // Define local scope for the variable to be used in the loop below.
 
332
                                shortcodesDetails = [];
 
333
 
 
334
                        while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) {
 
335
                                /**
 
336
                                 * Check if the shortcode should be shown as plain text.
 
337
                                 *
 
338
                                 * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode
 
339
                                 * and just shows it as text.
 
340
                                 */
 
341
                                var showAsPlainText = shortcodeMatch[1] === '[';
 
342
 
 
343
                                /**
 
344
                                 * For more context check the docs for:
 
345
                                 *
 
346
                                 * @link isShortcodePreviewable
 
347
                                 *
 
348
                                 * In addition, if the shortcode will get rendered as plain text ( see above ),
 
349
                                 * we can treat it as text and use the selection markers in it.
 
350
                                 */
 
351
                                var isPreviewable = ! showAsPlainText && isShortcodePreviewable( shortcodeMatch[2] );
 
352
 
 
353
                                shortcodeInfo = {
 
354
                                        shortcodeName: shortcodeMatch[2],
 
355
                                        showAsPlainText: showAsPlainText,
 
356
                                        startIndex: shortcodeMatch.index,
 
357
                                        endIndex: shortcodeMatch.index + shortcodeMatch[0].length,
 
358
                                        length: shortcodeMatch[0].length,
 
359
                                        isPreviewable: isPreviewable
 
360
                                };
 
361
 
 
362
                                shortcodesDetails.push( shortcodeInfo );
 
363
                        }
 
364
 
 
365
                        /**
 
366
                         * Get all URL matches, and treat them as embeds.
 
367
                         *
 
368
                         * Since there isn't a good way to detect if a URL by itself on a line is a previewable
 
369
                         * object, it's best to treat all of them as such.
 
370
                         *
 
371
                         * This means that the selection will capture the whole URL, in a similar way shrotcodes
 
372
                         * are treated.
 
373
                         */
 
374
                        var urlRegexp = new RegExp(
 
375
                                '(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi'
 
376
                        );
 
377
 
 
378
                        while ( shortcodeMatch = urlRegexp.exec( content ) ) {
 
379
                                shortcodeInfo = {
 
380
                                        shortcodeName: 'url',
 
381
                                        showAsPlainText: false,
 
382
                                        startIndex: shortcodeMatch.index,
 
383
                                        endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length,
 
384
                                        length: shortcodeMatch[ 0 ].length,
 
385
                                        isPreviewable: true,
 
386
                                        urlAtStartOfContent: shortcodeMatch[ 1 ] === '',
 
387
                                        urlAtEndOfContent: shortcodeMatch[ 3 ] === ''
 
388
                                };
 
389
 
 
390
                                shortcodesDetails.push( shortcodeInfo );
 
391
                        }
 
392
 
 
393
                        return shortcodesDetails;
 
394
                }
 
395
 
 
396
                /**
 
397
                 * Generate a cursor marker element to be inserted in the content.
 
398
                 *
 
399
                 * `span` seems to be the least destructive element that can be used.
 
400
                 *
 
401
                 * Using DomQuery syntax to create it, since it's used as both text and as a DOM element.
 
402
                 *
 
403
                 * @param {Object} domLib DOM library instance.
 
404
                 * @param {string} content The content to insert into the cusror marker element.
 
405
                 */
 
406
                function getCursorMarkerSpan( domLib, content ) {
 
407
                        return domLib( '<span>' ).css( {
 
408
                                                display: 'inline-block',
 
409
                                                width: 0,
 
410
                                                overflow: 'hidden',
 
411
                                                'line-height': 0
 
412
                                        } )
 
413
                                        .html( content ? content : '' );
 
414
                }
 
415
 
 
416
                /**
 
417
                 * @summary Get adjusted selection cursor positions according to HTML tags/shortcodes
 
418
                 *
 
419
                 * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
 
420
                 * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
 
421
                 * to break the syntax and render the HTML tag or shortcode broken.
 
422
                 *
 
423
                 * @link getShortcodeWrapperInfo
 
424
                 *
 
425
                 * @param {string} content Textarea content that the cursors are in
 
426
                 * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions
 
427
                 *
 
428
                 * @return {{cursorStart: number, cursorEnd: number}}
 
429
                 */
 
430
                function adjustTextAreaSelectionCursors( content, cursorPositions ) {
 
431
                        var voidElements = [
 
432
                                'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
 
433
                                'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
 
434
                        ];
 
435
 
 
436
                        var cursorStart = cursorPositions.cursorStart,
 
437
                                cursorEnd = cursorPositions.cursorEnd,
 
438
                                // check if the cursor is in a tag and if so, adjust it
 
439
                                isCursorStartInTag = getContainingTagInfo( content, cursorStart );
 
440
 
 
441
                        if ( isCursorStartInTag ) {
 
442
                                /**
 
443
                                 * Only move to the start of the HTML tag (to select the whole element) if the tag
 
444
                                 * is part of the voidElements list above.
 
445
                                 *
 
446
                                 * This list includes tags that are self-contained and don't need a closing tag, according to the
 
447
                                 * HTML5 specification.
 
448
                                 *
 
449
                                 * This is done in order to make selection of text a bit more consistent when selecting text in
 
450
                                 * `<p>` tags or such.
 
451
                                 *
 
452
                                 * In cases where the tag is not a void element, the cursor is put to the end of the tag,
 
453
                                 * so it's either between the opening and closing tag elements or after the closing tag.
 
454
                                 */
 
455
                                if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
 
456
                                        cursorStart = isCursorStartInTag.ltPos;
 
457
                                } else {
 
458
                                        cursorStart = isCursorStartInTag.gtPos;
 
459
                                }
 
460
                        }
 
461
 
 
462
                        var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
 
463
                        if ( isCursorEndInTag ) {
 
464
                                cursorEnd = isCursorEndInTag.gtPos;
 
465
                        }
 
466
 
 
467
                        var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
 
468
                        if ( isCursorStartInShortcode && isCursorStartInShortcode.isPreviewable ) {
 
469
                                /**
 
470
                                 * If a URL is at the start or the end of the content,
 
471
                                 * the selection doesn't work, because it inserts a marker in the text,
 
472
                                 * which breaks the embedURL detection.
 
473
                                 *
 
474
                                 * The best way to avoid that and not modify the user content is to
 
475
                                 * adjust the cursor to either after or before URL.
 
476
                                 */
 
477
                                if ( isCursorStartInShortcode.urlAtStartOfContent ) {
 
478
                                        cursorStart = isCursorStartInShortcode.endIndex;
 
479
                                } else {
 
480
                                        cursorStart = isCursorStartInShortcode.startIndex;
 
481
                                }
 
482
                        }
 
483
 
 
484
                        var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
 
485
                        if ( isCursorEndInShortcode && isCursorEndInShortcode.isPreviewable ) {
 
486
                                if ( isCursorEndInShortcode.urlAtEndOfContent ) {
 
487
                                        cursorEnd = isCursorEndInShortcode.startIndex;
 
488
                                } else {
 
489
                                        cursorEnd = isCursorEndInShortcode.endIndex;
 
490
                                }
 
491
                        }
 
492
 
 
493
                        return {
 
494
                                cursorStart: cursorStart,
 
495
                                cursorEnd: cursorEnd
 
496
                        };
 
497
                }
 
498
 
 
499
                /**
 
500
                 * @summary Adds text selection markers in the editor textarea.
 
501
                 *
 
502
                 * Adds selection markers in the content of the editor `textarea`.
 
503
                 * The method directly manipulates the `textarea` content, to allow TinyMCE plugins
 
504
                 * to run after the markers are added.
 
505
                 *
 
506
                 * @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
 
507
                 */
 
508
                function addHTMLBookmarkInTextAreaContent( $textarea ) {
 
509
                        if ( ! $textarea || ! $textarea.length ) {
 
510
                                // If no valid $textarea object is provided, there's nothing we can do.
 
511
                                return;
 
512
                        }
 
513
 
 
514
                        var textArea = $textarea[0],
 
515
                                textAreaContent = textArea.value,
 
516
 
 
517
                                adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
 
518
                                        cursorStart: textArea.selectionStart,
 
519
                                        cursorEnd: textArea.selectionEnd
 
520
                                } ),
 
521
 
 
522
                                htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
 
523
                                htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
 
524
 
 
525
                                mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
 
526
 
 
527
                                selectedText = null,
 
528
                                cursorMarkerSkeleton = getCursorMarkerSpan( $$, '&#65279;' ).attr( 'data-mce-type','bookmark' );
 
529
 
 
530
                        if ( mode === 'range' ) {
 
531
                                var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
 
532
                                        bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
 
533
 
 
534
                                selectedText = [
 
535
                                        markedText,
 
536
                                        bookMarkEnd[0].outerHTML
 
537
                                ].join( '' );
 
538
                        }
 
539
 
 
540
                        textArea.value = [
 
541
                                textArea.value.slice( 0, htmlModeCursorStartPosition ), // text until the cursor/selection position
 
542
                                cursorMarkerSkeleton.clone()                                                    // cursor/selection start marker
 
543
                                        .addClass( 'mce_SELRES_start' )[0].outerHTML,
 
544
                                selectedText,                                                                                   // selected text with end cursor/position marker
 
545
                                textArea.value.slice( htmlModeCursorEndPosition )               // text from last cursor/selection position to end
 
546
                        ].join( '' );
 
547
                }
 
548
 
 
549
                /**
 
550
                 * @summary Focus the selection markers in Visual mode.
 
551
                 *
 
552
                 * The method checks for existing selection markers inside the editor DOM (Visual mode)
 
553
                 * and create a selection between the two nodes using the DOM `createRange` selection API
 
554
                 *
 
555
                 * If there is only a single node, select only the single node through TinyMCE's selection API
 
556
                 *
 
557
                 * @param {Object} editor TinyMCE editor instance.
 
558
                 */
 
559
                function focusHTMLBookmarkInVisualEditor( editor ) {
 
560
                        var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ),
 
561
                                endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 );
 
562
 
 
563
                        if ( startNode.length ) {
 
564
                                editor.focus();
 
565
 
 
566
                                if ( ! endNode.length ) {
 
567
                                        editor.selection.select( startNode[0] );
 
568
                                } else {
 
569
                                        var selection = editor.getDoc().createRange();
 
570
 
 
571
                                        selection.setStartAfter( startNode[0] );
 
572
                                        selection.setEndBefore( endNode[0] );
 
573
 
 
574
                                        editor.selection.setRng( selection );
 
575
                                }
 
576
                        }
 
577
 
 
578
                        if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
 
579
                                scrollVisualModeToStartElement( editor, startNode );
 
580
                        }
 
581
 
 
582
                        removeSelectionMarker( startNode );
 
583
                        removeSelectionMarker( endNode );
 
584
 
 
585
                        editor.save();
 
586
                }
 
587
 
 
588
                /**
 
589
                 * @summary Remove selection marker and the parent node if it is an empty paragraph.
 
590
                 *
 
591
                 * By default TinyMCE wraps loose inline tags in a `<p>`.
 
592
                 * When removing selection markers an empty `<p>` may be left behind, remove it.
 
593
                 *
 
594
                 * @param {object} $marker The marker to be removed from the editor DOM, wrapped in an instnce of `editor.$`
 
595
                 */
 
596
                function removeSelectionMarker( $marker ) {
 
597
                        var $markerParent = $marker.parent();
 
598
 
 
599
                        $marker.remove();
 
600
 
 
601
                        //Remove empty paragraph left over after removing the marker.
 
602
                        if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) {
 
603
                                $markerParent.remove();
 
604
                        }
 
605
                }
 
606
 
 
607
                /**
 
608
                 * @summary Scrolls the content to place the selected element in the center of the screen.
 
609
                 *
 
610
                 * Takes an element, that is usually the selection start element, selected in
 
611
                 * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly
 
612
                 * in the middle of the screen.
 
613
                 *
 
614
                 * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted
 
615
                 * from the window height, to get the proper viewport window, that the user sees.
 
616
                 *
 
617
                 * @param {Object} editor TinyMCE editor instance.
 
618
                 * @param {Object} element HTMLElement that should be scrolled into view.
 
619
                 */
 
620
                function scrollVisualModeToStartElement( editor, element ) {
 
621
                        var elementTop = editor.$( element ).offset().top,
 
622
                                TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
 
623
 
 
624
                                toolbarHeight = getToolbarHeight( editor ),
 
625
 
 
626
                                edTools = $( '#wp-content-editor-tools' ),
 
627
                                edToolsHeight = 0,
 
628
                                edToolsOffsetTop = 0,
 
629
 
 
630
                                $scrollArea;
 
631
 
 
632
                        if ( edTools.length ) {
 
633
                                edToolsHeight = edTools.height();
 
634
                                edToolsOffsetTop = edTools.offset().top;
 
635
                        }
 
636
 
 
637
                        var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
 
638
 
 
639
                                selectionPosition = TinyMCEContentAreaTop + elementTop,
 
640
                                visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
 
641
 
 
642
                        // There's no need to scroll if the selection is inside the visible area.
 
643
                        if ( selectionPosition < visibleAreaHeight ) {
 
644
                                return;
 
645
                        }
 
646
 
 
647
                        /**
 
648
                         * The minimum scroll height should be to the top of the editor, to offer a consistent
 
649
                         * experience.
 
650
                         *
 
651
                         * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and
 
652
                         * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
 
653
                         * the top of the viewport (under the Master Bar)
 
654
                         */
 
655
                        var adjustedScroll;
 
656
                        if ( editor.settings.wp_autoresize_on ) {
 
657
                                $scrollArea = $( 'html,body' );
 
658
                                adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
 
659
                        } else {
 
660
                                $scrollArea = $( editor.contentDocument ).find( 'html,body' );
 
661
                                adjustedScroll = elementTop;
 
662
                        }
 
663
 
 
664
                        $scrollArea.animate( {
 
665
                                scrollTop: parseInt( adjustedScroll, 10 )
 
666
                        }, 100 );
 
667
                }
 
668
 
 
669
                /**
 
670
                 * This method was extracted from the `SaveContent` hook in
 
671
                 * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`.
 
672
                 *
 
673
                 * It's needed here, since the method changes the content a bit, which confuses the cursor position.
 
674
                 *
 
675
                 * @param {Object} event TinyMCE event object.
 
676
                 */
 
677
                function fixTextAreaContent( event ) {
 
678
                        // Keep empty paragraphs :(
 
679
                        event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p>&nbsp;</p>' );
 
680
                }
 
681
 
 
682
                /**
 
683
                 * @summary Finds the current selection position in the Visual editor.
 
684
                 *
 
685
                 * Find the current selection in the Visual editor by inserting marker elements at the start
 
686
                 * and end of the selection.
 
687
                 *
 
688
                 * Uses the standard DOM selection API to achieve that goal.
 
689
                 *
 
690
                 * Check the notes in the comments in the code below for more information on some gotchas
 
691
                 * and why this solution was chosen.
 
692
                 *
 
693
                 * @param {Object} editor The editor where we must find the selection
 
694
                 * @returns {(null|Object)} The selection range position in the editor
 
695
                 */
 
696
                function findBookmarkedPosition( editor ) {
 
697
                        // Get the TinyMCE `window` reference, since we need to access the raw selection.
 
698
                        var TinyMCEWIndow = editor.getWin(),
 
699
                                selection = TinyMCEWIndow.getSelection();
 
700
 
 
701
                        if ( selection.rangeCount <= 0 ) {
 
702
                                // no selection, no need to continue.
 
703
                                return;
 
704
                        }
 
705
 
 
706
                        /**
 
707
                         * The ID is used to avoid replacing user generated content, that may coincide with the
 
708
                         * format specified below.
 
709
                         * @type {string}
 
710
                         */
 
711
                        var selectionID = 'SELRES_' + Math.random();
 
712
 
 
713
                        /**
 
714
                         * Create two marker elements that will be used to mark the start and the end of the range.
 
715
                         *
 
716
                         * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
 
717
                         * random content flickering in the editor when switching between modes.
 
718
                         */
 
719
                        var spanSkeleton = getCursorMarkerSpan( editor.$, selectionID ),
 
720
                                startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
 
721
                                endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
 
722
 
 
723
                        /**
 
724
                         * Inspired by:
 
725
                         * @link https://stackoverflow.com/a/17497803/153310
 
726
                         *
 
727
                         * Why do it this way and not with TinyMCE's bookmarks?
 
728
                         *
 
729
                         * TinyMCE's bookmarks are very nice when working with selections and positions, BUT
 
730
                         * there is no way to determine the precise position of the bookmark when switching modes, since
 
731
                         * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify
 
732
                         * HTML code and so on. In this process, the bookmark markup gets lost.
 
733
                         *
 
734
                         * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML
 
735
                         * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will
 
736
                         * throw off the positioning.
 
737
                         *
 
738
                         * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the
 
739
                         * selection.
 
740
                         *
 
741
                         * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates
 
742
                         * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to
 
743
                         * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the
 
744
                         * selection may start in the middle of one node and end in the middle of a completely different one. If we
 
745
                         * wrap the selection in another node, this will create artifacts in the content.
 
746
                         *
 
747
                         * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection.
 
748
                         * This helps us not break the content and also gives us the option to work with multi-node selections without
 
749
                         * breaking the markup.
 
750
                         */
 
751
                        var range = selection.getRangeAt( 0 ),
 
752
                                startNode = range.startContainer,
 
753
                                startOffset = range.startOffset,
 
754
                                boundaryRange = range.cloneRange();
 
755
 
 
756
                        /**
 
757
                         * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
 
758
                         * which we have to account for.
 
759
                         */
 
760
                        if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) {
 
761
                                startNode = editor.$( '[data-mce-selected]' )[0];
 
762
 
 
763
                                /**
 
764
                                 * Marking the start and end element with `data-mce-object-selection` helps
 
765
                                 * discern when the selected object is a Live Preview selection.
 
766
                                 *
 
767
                                 * This way we can adjust the selection to properly select only the content, ignoring
 
768
                                 * whitespace inserted around the selected object by the Editor.
 
769
                                 */
 
770
                                startElement.attr( 'data-mce-object-selection', 'true' );
 
771
                                endElement.attr( 'data-mce-object-selection', 'true' );
 
772
 
 
773
                                editor.$( startNode ).before( startElement[0] );
 
774
                                editor.$( startNode ).after( endElement[0] );
 
775
                        } else {
 
776
                                boundaryRange.collapse( false );
 
777
                                boundaryRange.insertNode( endElement[0] );
 
778
 
 
779
                                boundaryRange.setStart( startNode, startOffset );
 
780
                                boundaryRange.collapse( true );
 
781
                                boundaryRange.insertNode( startElement[0] );
 
782
 
 
783
                                range.setStartAfter( startElement[0] );
 
784
                                range.setEndBefore( endElement[0] );
 
785
                                selection.removeAllRanges();
 
786
                                selection.addRange( range );
 
787
                        }
 
788
 
 
789
                        /**
 
790
                         * Now the editor's content has the start/end nodes.
 
791
                         *
 
792
                         * Unfortunately the content goes through some more changes after this step, before it gets inserted
 
793
                         * in the `textarea`. This means that we have to do some minor cleanup on our own here.
 
794
                         */
 
795
                        editor.on( 'GetContent', fixTextAreaContent );
 
796
 
 
797
                        var content = removep( editor.getContent() );
 
798
 
 
799
                        editor.off( 'GetContent', fixTextAreaContent );
 
800
 
 
801
                        startElement.remove();
 
802
                        endElement.remove();
 
803
 
 
804
                        var startRegex = new RegExp(
 
805
                                '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
 
806
                        );
 
807
 
 
808
                        var endRegex = new RegExp(
 
809
                                '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
 
810
                        );
 
811
 
 
812
                        var startMatch = content.match( startRegex ),
 
813
                                endMatch = content.match( endRegex );
 
814
 
 
815
                        if ( ! startMatch ) {
 
816
                                return null;
 
817
                        }
 
818
 
 
819
                        var startIndex = startMatch.index,
 
820
                                startMatchLength = startMatch[0].length,
 
821
                                endIndex = null;
 
822
 
 
823
                        if (endMatch) {
 
824
                                /**
 
825
                                 * Adjust the selection index, if the selection contains a Live Preview object or not.
 
826
                                 *
 
827
                                 * Check where the `data-mce-object-selection` attribute is set above for more context.
 
828
                                 */
 
829
                                if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
 
830
                                        startMatchLength -= startMatch[1].length;
 
831
                                }
 
832
 
 
833
                                var endMatchIndex = endMatch.index;
 
834
 
 
835
                                if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
 
836
                                        endMatchIndex -= endMatch[1].length;
 
837
                                }
 
838
 
 
839
                                // We need to adjust the end position to discard the length of the range start marker
 
840
                                endIndex = endMatchIndex - startMatchLength;
 
841
                        }
 
842
 
 
843
                        return {
 
844
                                start: startIndex,
 
845
                                end: endIndex
 
846
                        };
 
847
                }
 
848
 
 
849
                /**
 
850
                 * @summary Selects text in the TinyMCE `textarea`.
 
851
                 *
 
852
                 * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
 
853
                 *
 
854
                 * For `selection` parameter:
 
855
                 * @link findBookmarkedPosition
 
856
                 *
 
857
                 * @param {Object} editor TinyMCE's editor instance.
 
858
                 * @param {Object} selection Selection data.
 
859
                 */
 
860
                function selectTextInTextArea( editor, selection ) {
 
861
                        // only valid in the text area mode and if we have selection
 
862
                        if ( ! selection ) {
 
863
                                return;
 
864
                        }
 
865
 
 
866
                        var textArea = editor.getElement(),
 
867
                                start = selection.start,
 
868
                                end = selection.end || selection.start;
 
869
 
 
870
                        if ( textArea.focus ) {
 
871
                                // Wait for the Visual editor to be hidden, then focus and scroll to the position
 
872
                                setTimeout( function() {
 
873
                                        textArea.setSelectionRange( start, end );
 
874
                                        if ( textArea.blur ) {
 
875
                                                // defocus before focusing
 
876
                                                textArea.blur();
 
877
                                        }
 
878
                                        textArea.focus();
 
879
                                }, 100 );
 
880
                        }
 
881
                }
 
882
 
 
883
                // Restore the selection when the editor is initialized. Needed when the Text editor is the default.
 
884
                $( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) {
 
885
                        if ( editor.$( '.mce_SELRES_start' ).length ) {
 
886
                                focusHTMLBookmarkInVisualEditor( editor );
 
887
                        }
 
888
                } );
 
889
 
 
890
                /**
159
891
                 * @summary Replaces <p> tags with two line breaks. "Opposite" of wpautop().
160
892
                 *
161
893
                 * Replaces <p> tags with two line breaks except where the <p> has attributes.
316
1048
                        // Normalize line breaks.
317
1049
                        text = text.replace( /\r\n|\r/g, '\n' );
318
1050
 
319
 
                        if ( text.indexOf( '\n' ) === -1 ) {
320
 
                                return text;
321
 
                        }
322
 
 
323
1051
                        // Remove line breaks from <object>.
324
1052
                        if ( text.indexOf( '<object' ) !== -1 ) {
325
1053
                                text = text.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
522
1250
         * Settings for both TinyMCE and Quicktags can be passed on initialization, and are "filtered"
523
1251
         * with custom jQuery events on the document element, wp-before-tinymce-init and wp-before-quicktags-init.
524
1252
         *
525
 
         * @since 4.8
 
1253
         * @since 4.8.0
526
1254
         *
527
1255
         * @param {string} id The HTML id of the textarea that is used for the editor.
528
1256
         *                    Has to be jQuery compliant. No brackets, special chars, etc.
562
1290
                // Add wrap and the Visual|Text tabs.
563
1291
                if ( settings.tinymce && settings.quicktags ) {
564
1292
                        var $textarea = $( '#' + id );
 
1293
 
565
1294
                        var $wrap = $( '<div>' ).attr( {
566
1295
                                        'class': 'wp-core-ui wp-editor-wrap tmce-active',
567
1296
                                        id: 'wp-' + id + '-wrap'
568
1297
                                } );
 
1298
 
569
1299
                        var $editorContainer = $( '<div class="wp-editor-container">' );
 
1300
 
570
1301
                        var $button = $( '<button>' ).attr( {
571
1302
                                        type: 'button',
572
1303
                                        'data-wp-editor-id': id
573
1304
                                } );
574
1305
 
 
1306
                        var $editorTools = $( '<div class="wp-editor-tools">' );
 
1307
 
 
1308
                        if ( settings.mediaButtons ) {
 
1309
                                var buttonText = 'Add Media';
 
1310
 
 
1311
                                if ( window._wpMediaViewsL10n && window._wpMediaViewsL10n.addMedia ) {
 
1312
                                        buttonText = window._wpMediaViewsL10n.addMedia;
 
1313
                                }
 
1314
 
 
1315
                                var $addMediaButton = $( '<button type="button" class="button insert-media add_media">' );
 
1316
 
 
1317
                                $addMediaButton.append( '<span class="wp-media-buttons-icon"></span>' );
 
1318
                                $addMediaButton.append( document.createTextNode( ' ' + buttonText ) );
 
1319
                                $addMediaButton.data( 'editor', id );
 
1320
 
 
1321
                                $editorTools.append(
 
1322
                                        $( '<div class="wp-media-buttons">' )
 
1323
                                                .append( $addMediaButton )
 
1324
                                );
 
1325
                        }
 
1326
 
575
1327
                        $wrap.append(
576
 
                                $( '<div class="wp-editor-tools">' )
 
1328
                                $editorTools
577
1329
                                        .append( $( '<div class="wp-editor-tabs">' )
578
1330
                                                .append( $button.clone().attr({
579
1331
                                                        id: id + '-tmce',
628
1380
         *
629
1381
         * Intended for use with editors that were initialized with wp.editor.initialize().
630
1382
         *
631
 
         * @since 4.8
 
1383
         * @since 4.8.0
632
1384
         *
633
1385
         * @param {string} id The HTML id of the editor textarea.
634
1386
         */
667
1419
         *
668
1420
         * Intended for use with editors that were initialized with wp.editor.initialize().
669
1421
         *
670
 
         * @since 4.8
 
1422
         * @since 4.8.0
671
1423
         *
672
1424
         * @param {string} id The HTML id of the editor textarea.
673
1425
         * @return The editor content.