Make WordPress Core

Ticket #31412: 31412.5.patch

File 31412.5.patch, 43.8 KB (added by iseulde, 10 years ago)
  • src/wp-includes/js/mce-view.js

     
    11/* global tinymce */
    2 /**
     2
     3window.wp = window.wp || {};
     4
     5/*
     6 * The TinyMCE view API.
     7 *
    38 * Note: this API is "experimental" meaning that it will probably change
    49 * in the next few releases based on feedback from 3.9.0.
    510 * If you decide to use it, please follow the development closely.
     11 *
     12 * Diagram
     13 *
     14 * |- registered view constructor (type)
     15 * |  |- view instance (unique text)
     16 * |  |  |- editor 1
     17 * |  |  |  |- view node
     18 * |  |  |  |- view node
     19 * |  |  |  |- ...
     20 * |  |  |- editor 2
     21 * |  |  |  |- ...
     22 * |  |- view instance
     23 * |  |  |- ...
     24 * |- registered view
     25 * |  |- ...
    626 */
    7 
    8 // Ensure the global `wp` object exists.
    9 window.wp = window.wp || {};
    10 
    11 ( function( $ ) {
     27( function( window, wp, $ ) {
    1228        'use strict';
    1329
    1430        var views = {},
    15                 instances = {},
    16                 media = wp.media,
    17                 mediaWindows = [],
    18                 windowIdx = 0,
    19                 waitInterval = 50,
    20                 viewOptions = ['encodedText'];
     31                instances = {};
    2132
    22         // Create the `wp.mce` object if necessary.
    2333        wp.mce = wp.mce || {};
    2434
    2535        /**
    26          * wp.mce.View
    27          *
    28          * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
    29          * that the TinyMCE View is not tied to a particular DOM node.
    30          *
    31          * @param {Object} [options={}]
    32          */
    33         wp.mce.View = function( options ) {
    34                 options = options || {};
    35                 this.type = options.type;
    36                 _.extend( this, _.pick( options, viewOptions ) );
    37                 this.initialize.apply( this, arguments );
    38         };
    39 
    40         _.extend( wp.mce.View.prototype, {
    41                 initialize: function() {},
    42                 getHtml: function() {
    43                         return '';
    44                 },
    45                 loadingPlaceholder: function() {
    46                         return '' +
    47                                 '<div class="loading-placeholder">' +
    48                                         '<div class="dashicons dashicons-admin-media"></div>' +
    49                                         '<div class="wpview-loading"><ins></ins></div>' +
    50                                 '</div>';
    51                 },
    52                 render: function( force ) {
    53                         if ( force || ! this.rendered() ) {
    54                                 this.unbind();
    55 
    56                                 this.setContent(
    57                                         '<p class="wpview-selection-before">\u00a0</p>' +
    58                                         '<div class="wpview-body" contenteditable="false">' +
    59                                                 '<div class="toolbar mce-arrow-down">' +
    60                                                         ( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +
    61                                                         '<div class="dashicons dashicons-no remove"></div>' +
    62                                                 '</div>' +
    63                                                 '<div class="wpview-content wpview-type-' + this.type + '">' +
    64                                                         ( this.getHtml() || this.loadingPlaceholder() ) +
    65                                                 '</div>' +
    66                                                 ( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) +
    67                                         '</div>' +
    68                                         '<p class="wpview-selection-after">\u00a0</p>',
    69                                         'wrap'
    70                                 );
    71 
    72                                 $( this ).trigger( 'ready' );
    73 
    74                                 this.rendered( true );
    75                         }
    76                 },
    77                 unbind: function() {},
    78                 getEditors: function( callback ) {
    79                         var editors = [];
    80 
    81                         _.each( tinymce.editors, function( editor ) {
    82                                 if ( editor.plugins.wpview ) {
    83                                         if ( callback ) {
    84                                                 callback( editor );
    85                                         }
    86 
    87                                         editors.push( editor );
    88                                 }
    89                         }, this );
    90 
    91                         return editors;
    92                 },
    93                 getNodes: function( callback ) {
    94                         var nodes = [],
    95                                 self = this;
    96 
    97                         this.getEditors( function( editor ) {
    98                                 $( editor.getBody() )
    99                                 .find( '[data-wpview-text="' + self.encodedText + '"]' )
    100                                 .each( function ( i, node ) {
    101                                         if ( callback ) {
    102                                                 callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
    103                                         }
    104 
    105                                         nodes.push( node );
    106                                 } );
    107                         } );
    108 
    109                         return nodes;
    110                 },
    111                 setContent: function( html, option ) {
    112                         this.getNodes( function ( editor, node, content ) {
    113                                 var el = ( option === 'wrap' || option === 'replace' ) ? node : content,
    114                                         insert = html;
    115 
    116                                 if ( _.isString( insert ) ) {
    117                                         insert = editor.dom.createFragment( insert );
    118                                 }
    119 
    120                                 if ( option === 'replace' ) {
    121                                         editor.dom.replace( insert, el );
    122                                 } else {
    123                                         el.innerHTML = '';
    124                                         el.appendChild( insert );
    125                                 }
    126                         } );
    127                 },
    128                 /* jshint scripturl: true */
    129                 setIframes: function ( head, body ) {
    130                         var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
    131                                 importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';
    132 
    133                         if ( head || body.indexOf( '<script' ) !== -1 ) {
    134                                 this.getNodes( function ( editor, node, content ) {
    135                                         var dom = editor.dom,
    136                                                 styles = '',
    137                                                 bodyClasses = editor.getBody().className || '',
    138                                                 iframe, iframeDoc, i, resize;
    139 
    140                                         content.innerHTML = '';
    141                                         head = head || '';
    142 
    143                                         if ( importStyles ) {
    144                                                 if ( ! wp.mce.views.sandboxStyles ) {
    145                                                         tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {
    146                                                                 if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
    147                                                                         link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
    148 
    149                                                                         styles += dom.getOuterHTML( link ) + '\n';
    150                                                                 }
    151                                                         });
    152 
    153                                                         wp.mce.views.sandboxStyles = styles;
    154                                                 } else {
    155                                                         styles = wp.mce.views.sandboxStyles;
    156                                                 }
    157                                         }
    158 
    159                                         // Seems Firefox needs a bit of time to insert/set the view nodes, or the iframe will fail
    160                                         // especially when switching Text => Visual.
    161                                         setTimeout( function() {
    162                                                 iframe = dom.add( content, 'iframe', {
    163                                                         src: tinymce.Env.ie ? 'javascript:""' : '',
    164                                                         frameBorder: '0',
    165                                                         allowTransparency: 'true',
    166                                                         scrolling: 'no',
    167                                                         'class': 'wpview-sandbox',
    168                                                         style: {
    169                                                                 width: '100%',
    170                                                                 display: 'block'
    171                                                         }
    172                                                 } );
    173 
    174                                                 iframeDoc = iframe.contentWindow.document;
    175 
    176                                                 iframeDoc.open();
    177                                                 iframeDoc.write(
    178                                                         '<!DOCTYPE html>' +
    179                                                         '<html>' +
    180                                                                 '<head>' +
    181                                                                         '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
    182                                                                         head +
    183                                                                         styles +
    184                                                                         '<style>' +
    185                                                                                 'html {' +
    186                                                                                         'background: transparent;' +
    187                                                                                         'padding: 0;' +
    188                                                                                         'margin: 0;' +
    189                                                                                 '}' +
    190                                                                                 'body#wpview-iframe-sandbox {' +
    191                                                                                         'background: transparent;' +
    192                                                                                         'padding: 1px 0 !important;' +
    193                                                                                         'margin: -1px 0 0 !important;' +
    194                                                                                 '}' +
    195                                                                                 'body#wpview-iframe-sandbox:before,' +
    196                                                                                 'body#wpview-iframe-sandbox:after {' +
    197                                                                                         'display: none;' +
    198                                                                                         'content: "";' +
    199                                                                                 '}' +
    200                                                                         '</style>' +
    201                                                                 '</head>' +
    202                                                                 '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
    203                                                                         body +
    204                                                                 '</body>' +
    205                                                         '</html>'
    206                                                 );
    207                                                 iframeDoc.close();
    208 
    209                                                 resize = function() {
    210                                                         var $iframe, iframeDocHeight;
    211 
    212                                                         // Make sure the iframe still exists.
    213                                                         if ( iframe.contentWindow ) {
    214                                                                 $iframe = $( iframe );
    215                                                                 iframeDocHeight = $( iframeDoc.body ).height();
    216 
    217                                                                 if ( $iframe.height() !== iframeDocHeight ) {
    218                                                                         $iframe.height( iframeDocHeight );
    219                                                                         editor.nodeChanged();
    220                                                                 }
    221                                                         }
    222                                                 };
    223 
    224                                                 if ( MutationObserver ) {
    225                                                         new MutationObserver( _.debounce( function() {
    226                                                                 resize();
    227                                                         }, 100 ) )
    228                                                         .observe( iframeDoc.body, {
    229                                                                 attributes: true,
    230                                                                 childList: true,
    231                                                                 subtree: true
    232                                                         } );
    233                                                 } else {
    234                                                         for ( i = 1; i < 6; i++ ) {
    235                                                                 setTimeout( resize, i * 700 );
    236                                                         }
    237                                                 }
    238 
    239                                                 if ( importStyles ) {
    240                                                         editor.on( 'wp-body-class-change', function() {
    241                                                                 iframeDoc.body.className = editor.getBody().className;
    242                                                         });
    243                                                 }
    244                                         }, waitInterval );
    245                                 });
    246                         } else {
    247                                 this.setContent( body );
    248                         }
    249                 },
    250                 setError: function( message, dashicon ) {
    251                         this.setContent(
    252                                 '<div class="wpview-error">' +
    253                                         '<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +
    254                                         '<p>' + message + '</p>' +
    255                                 '</div>'
    256                         );
    257                 },
    258                 rendered: function( value ) {
    259                         var notRendered;
    260 
    261                         this.getNodes( function( editor, node ) {
    262                                 if ( value != null ) {
    263                                         $( node ).data( 'rendered', value === true );
    264                                 } else {
    265                                         notRendered = notRendered || ! $( node ).data( 'rendered' );
    266                                 }
    267                         } );
    268 
    269                         return ! notRendered;
    270                 }
    271         } );
    272 
    273         // take advantage of the Backbone extend method
    274         wp.mce.View.extend = Backbone.View.extend;
    275 
    276         /**
    27736         * wp.mce.views
    27837         *
    27938         * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
     
    28342        wp.mce.views = {
    28443
    28544                /**
    286                  * wp.mce.views.register( type, view )
    287                  *
    288                  * Registers a new TinyMCE view.
    289                  *
    290                  * @param type
    291                  * @param constructor
     45                 * Registers a new view type.
    29246                 *
     47                 * @param {String} type   The view type.
     48                 * @param {Object} extend An object to extend wp.mce.View.prototype with.
    29349                 */
    294                 register: function( type, constructor ) {
    295                         var defaultConstructor = {
    296                                         type: type,
    297                                         View: {},
    298                                         toView: function( content ) {
    299                                                 var match = wp.shortcode.next( this.type, content );
    300 
    301                                                 if ( ! match ) {
    302                                                         return;
    303                                                 }
    304 
    305                                                 return {
    306                                                         index: match.index,
    307                                                         content: match.content,
    308                                                         options: {
    309                                                                 shortcode: match.shortcode
    310                                                         }
    311                                                 };
    312                                         }
    313                                 };
    314 
    315                         constructor = _.defaults( constructor, defaultConstructor );
    316                         constructor.View = wp.mce.View.extend( constructor.View );
    317 
    318                         views[ type ] = constructor;
     50                register: function( type, extend ) {
     51                        views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) );
    31952                },
    32053
    32154                /**
    322                  * wp.mce.views.get( id )
    323                  *
    324                  * Returns a TinyMCE view constructor.
     55                 * Unregisters a view type.
    32556                 *
    326                  * @param type
     57                 * @param {String} type The view type.
    32758                 */
    328                 get: function( type ) {
    329                         return views[ type ];
     59                unregister: function( type ) {
     60                        delete views[ type ];
    33061                },
    33162
    33263                /**
    333                  * wp.mce.views.unregister( type )
    334                  *
    335                  * Unregisters a TinyMCE view.
     64                 * Returns the settings of a view type.
    33665                 *
    337                  * @param type
     66                 * @param {String} type The view type.
    33867                 */
    339                 unregister: function( type ) {
    340                         delete views[ type ];
     68                get: function( type ) {
     69                        return views[ type ];
    34170                },
    34271
    34372                /**
    344                  * wp.mce.views.unbind( editor )
    345                  *
    346                  * The editor DOM is being rebuilt, run cleanup.
     73                 * Unbinds all view nodes.
     74                 * Runs before removing all view nodes from the DOM.
    34775                 */
    34876                unbind: function() {
    34977                        _.each( instances, function( instance ) {
     
    35280                },
    35381
    35482                /**
    355                  * toViews( content )
    356                  * Scans a `content` string for each view's pattern, replacing any
    357                  * matches with wrapper elements, and creates a new instance for
    358                  * every match, which triggers the related data to be fetched.
     83                 * Scans a given string for each view's pattern,
     84                 * replacing any matches with markers,
     85                 * and creates a new instance for every match.
    35986                 *
    360                  * @param content
     87                 * @param {String} content The string to scan.
    36188                 */
    362                 toViews: function( content ) {
     89                setMarkers: function( content ) {
    36390                        var pieces = [ { content: content } ],
     91                                self = this,
    36492                                current;
    36593
    366                         _.each( views, function( view, viewType ) {
     94                        _.each( views, function( view, type ) {
    36795                                current = pieces.slice();
    36896                                pieces  = [];
    36997
     
    379107
    380108                                        // Iterate through the string progressively matching views
    381109                                        // and slicing the string as we go.
    382                                         while ( remaining && (result = view.toView( remaining )) ) {
     110                                        while ( remaining && ( result = view.prototype.match( remaining ) ) ) {
    383111                                                // Any text before the match becomes an unprocessed piece.
    384112                                                if ( result.index ) {
    385                                                         pieces.push({ content: remaining.substring( 0, result.index ) });
     113                                                        pieces.push( { content: remaining.substring( 0, result.index ) } );
    386114                                                }
    387115
     116                                                self.createInstance( type, result.content, result.options );
     117
    388118                                                // Add the processed piece for the match.
    389                                                 pieces.push({
    390                                                         content: wp.mce.views.toView( viewType, result.content, result.options ),
     119                                                pieces.push( {
     120                                                        content: '<p data-wpview-marker="' + encodeURIComponent( result.content ) + '">' + result.content + '</p>',
    391121                                                        processed: true
    392                                                 });
     122                                                } );
    393123
    394124                                                // Update the remaining content.
    395125                                                remaining = remaining.slice( result.index + result.content.length );
    396126                                        }
    397127
    398                                         // There are no additional matches. If any content remains,
    399                                         // add it as an unprocessed piece.
     128                                        // There are no additional matches.
     129                                        // If any content remains, add it as an unprocessed piece.
    400130                                        if ( remaining ) {
    401                                                 pieces.push({ content: remaining });
     131                                                pieces.push( { content: remaining } );
    402132                                        }
    403                                 });
    404                         });
     133                                } );
     134                        } );
    405135
    406                         return _.pluck( pieces, 'content' ).join('');
     136                        return _.pluck( pieces, 'content' ).join( '' );
    407137                },
    408138
    409139                /**
    410                  * Create a placeholder for a particular view type
    411                  *
    412                  * @param viewType
    413                  * @param text
    414                  * @param options
     140                 * Create a view instance.
    415141                 *
     142                 * @param {String} type    The view type.
     143                 * @param {String} text    The textual representation of the view.
     144                 * @param {Object} options Options.
    416145                 */
    417                 toView: function( viewType, text, options ) {
    418                         var view = wp.mce.views.get( viewType ),
    419                                 encodedText = window.encodeURIComponent( text ),
    420                                 instance, viewOptions;
    421 
    422 
    423                         if ( ! view ) {
    424                                 return text;
    425                         }
    426 
    427                         if ( ! wp.mce.views.getInstance( encodedText ) ) {
    428                                 viewOptions = options;
    429                                 viewOptions.type = viewType;
    430                                 viewOptions.encodedText = encodedText;
    431                                 instance = new view.View( viewOptions );
    432                                 instances[ encodedText ] = instance;
     146                createInstance: function( type, text, options ) {
     147                        var View = this.get( type ),
     148                                encodedText = encodeURIComponent( text ),
     149                                instance = this.getInstance( encodedText );
     150
     151                        if ( instance ) {
     152                                return instance;
    433153                        }
    434154
    435                         return wp.html.string({
    436                                 tag: 'div',
    437 
    438                                 attrs: {
    439                                         'class': 'wpview-wrap',
    440                                         'data-wpview-text': encodedText,
    441                                         'data-wpview-type': viewType
    442                                 },
     155                        options = _.extend( options || {}, {
     156                                text: text,
     157                                encodedText: encodedText
     158                        } );
    443159
    444                                 content: '\u00a0'
    445                         });
     160                        return instances[ encodedText ] = new View( options );
    446161                },
    447162
    448163                /**
    449                  * Refresh views after an update is made
     164                 * Get a view instance.
    450165                 *
    451                  * @param view {object} being refreshed
    452                  * @param text {string} textual representation of the view
    453                  * @param force {Boolean} whether to force rendering
     166                 * @param {String} text The textual representation of the view.
    454167                 */
    455                 refreshView: function( view, text, force ) {
    456                         var encodedText = window.encodeURIComponent( text ),
    457                                 viewOptions,
    458                                 result, instance;
    459 
    460                         instance = wp.mce.views.getInstance( encodedText );
    461 
    462                         if ( ! instance ) {
    463                                 result = view.toView( text );
    464                                 viewOptions = result.options;
    465                                 viewOptions.type = view.type;
    466                                 viewOptions.encodedText = encodedText;
    467                                 instance = new view.View( viewOptions );
    468                                 instances[ encodedText ] = instance;
    469                         }
    470 
    471                         instance.render( force );
    472                 },
    473 
    474                 getInstance: function( encodedText ) {
    475                         return instances[ encodedText ];
     168                getInstance: function( text ) {
     169                        return instances[ encodeURIComponent( text ) ];
    476170                },
    477171
    478172                /**
    479                  * render( scope )
    480                  *
    481                  * Renders any view instances inside a DOM node `scope`.
     173                 * Renders all view nodes that are not yet rendered.
    482174                 *
    483                  * View instances are detected by the presence of wrapper elements.
    484                  * To generate wrapper elements, pass your content through
    485                  * `wp.mce.view.toViews( content )`.
     175                 * @param {Boolean} force Rerender all view nodes.
    486176                 */
    487177                render: function( force ) {
    488178                        _.each( instances, function( instance ) {
     
    490180                        } );
    491181                },
    492182
    493                 edit: function( node ) {
    494                         var viewType = $( node ).data('wpview-type'),
    495                                 view = wp.mce.views.get( viewType );
     183                /**
     184                 * Update the text of a given view node.
     185                 *
     186                 * @param {String}         text   The new text.
     187                 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
     188                 * @param {HTMLElement}    node   The view node to update.
     189                 */
     190                update: function( text, editor, node ) {
     191                        var oldText = decodeURIComponent( $( node ).data( 'wpview-text' ) ),
     192                                instance = this.getInstance( oldText );
    496193
    497                         if ( view ) {
    498                                 view.edit( node );
     194                        if ( instance ) {
     195                                instance.update( text, editor, node );
     196                        }
     197                },
     198
     199                /**
     200                 * Renders any editing interface based on the view type.
     201                 *
     202                 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
     203                 * @param {HTMLElement}    node   The view node to edit.
     204                 */
     205                edit: function( editor, node ) {
     206                        var text = decodeURIComponent( $( node ).data( 'wpview-text' ) ),
     207                                instance = this.getInstance( text );
     208
     209                        if ( instance && instance.edit ) {
     210                                instance.edit( text, function( text ) {
     211                                        instance.update( text, editor, node );
     212                                } );
    499213                        }
    500214                }
    501215        };
    502216
    503         wp.mce.views.register( 'gallery', {
    504                 View: {
    505                         template: media.template( 'editor-gallery' ),
    506 
    507                         // The fallback post ID to use as a parent for galleries that don't
    508                         // specify the `ids` or `include` parameters.
    509                         //
    510                         // Uses the hidden input on the edit posts page by default.
    511                         postID: $('#post_ID').val(),
    512 
    513                         initialize: function( options ) {
    514                                 this.shortcode = options.shortcode;
    515                                 this.fetch();
    516                         },
     217        /**
     218         * A Backbone-like View constructor intended for use when rendering a TinyMCE View.
     219         * The main difference is that the TinyMCE View is not tied to a particular DOM node.
     220         *
     221         * @param {Object} Options.
     222         */
     223        wp.mce.View = function( options ) {
     224                _.extend( this, options );
     225                this.initialize();
     226        };
    517227
    518                         fetch: function() {
    519                                 var self = this;
     228        wp.mce.View.extend = Backbone.View.extend;
    520229
    521                                 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
    522                                 this.dfd = this.attachments.more().done( function() {
    523                                         self.render( true );
    524                                 } );
    525                         },
     230        _.extend( wp.mce.View.prototype, {
    526231
    527                         getHtml: function() {
    528                                 var attrs = this.shortcode.attrs.named,
    529                                         attachments = false,
    530                                         options;
    531 
    532                                 // Don't render errors while still fetching attachments
    533                                 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
    534                                         return '';
    535                                 }
     232                /**
     233                 * The content.
     234                 *
     235                 * @type {*}
     236                 */
     237                content: null,
    536238
    537                                 if ( this.attachments.length ) {
    538                                         attachments = this.attachments.toJSON();
     239                /**
     240                 * Whether or not to display a loader.
     241                 *
     242                 * @type {Boolean}
     243                 */
     244                loader: true,
    539245
    540                                         _.each( attachments, function( attachment ) {
    541                                                 if ( attachment.sizes ) {
    542                                                         if ( attrs.size && attachment.sizes[ attrs.size ] ) {
    543                                                                 attachment.thumbnail = attachment.sizes[ attrs.size ];
    544                                                         } else if ( attachment.sizes.thumbnail ) {
    545                                                                 attachment.thumbnail = attachment.sizes.thumbnail;
    546                                                         } else if ( attachment.sizes.full ) {
    547                                                                 attachment.thumbnail = attachment.sizes.full;
    548                                                         }
    549                                                 }
    550                                         } );
    551                                 }
     246                /**
     247                 * Runs after the view instance is created.
     248                 */
     249                initialize: function() {},
    552250
    553                                 options = {
    554                                         attachments: attachments,
    555                                         columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
    556                                 };
     251                /**
     252                 * Retuns the content to render in the view node.
     253                 *
     254                 * @return {*}
     255                 */
     256                getContent: function() {
     257                        return this.content;
     258                },
    557259
    558                                 return this.template( options );
     260                /**
     261                 * Renders all view nodes tied to this view instance that are not yet rendered.
     262                 *
     263                 * @param {Boolean} force Rerender all view nodes tied to this view instance.
     264                 */
     265                render: function( force ) {
     266                        // If there's nothing to render an no loader needs to be shown, stop.
     267                        if ( ! this.loader && ! this.getContent() ) {
     268                                return;
    559269                        }
    560                 },
    561270
    562                 edit: function( node ) {
    563                         var gallery = wp.media.gallery,
    564                                 self = this,
    565                                 frame, data;
     271                        // We're about to rerender all views of this instance, so unbind rendered views.
     272                        force && this.unbind();
    566273
    567                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    568                         frame = gallery.edit( data );
     274                        // Replace any left over markers.
     275                        this.replaceMarkers();
    569276
    570                         frame.state('gallery-edit').on( 'update', function( selection ) {
    571                                 var shortcode = gallery.shortcode( selection ).string();
    572                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
    573                                 wp.mce.views.refreshView( self, shortcode, true );
    574                         });
     277                        if ( this.getContent() ) {
     278                                this.setContent( this.getContent(), function( editor, node ) {
     279                                        $( node ).data( 'rendered', true );
     280                                        this.bindNodes.apply( this, arguments );
     281                                }, force ? null : false );
     282                        } else {
     283                                this.setLoader();
     284                        }
     285                },
    575286
    576                         frame.on( 'close', function() {
    577                                 frame.detach();
    578                         });
    579                 }
    580         } );
     287                /**
     288                 * Binds a given rendered view node.
     289                 * Runs after a view node's content is added to the DOM.
     290                 *
     291                 * @param {tinymce.Editor} editor      The TinyMCE editor instance the view node is in.
     292                 * @param {HTMLElement}    node        The view node.
     293                 * @param {HTMLElement}    contentNode The view's content node.
     294                 */
     295                bindNodes: function( /* editor, node, contentNode */ ) {},
    581296
    582         /**
    583          * These are base methods that are shared by the audio and video shortcode's MCE controller.
    584          *
    585          * @mixin
    586          */
    587         wp.mce.av = {
    588                 View: {
    589                         overlay: true,
     297                /**
     298                 * Unbinds all view nodes tied to this view instance.
     299                 * Runs before their content is removed from the DOM.
     300                 */
     301                unbind: function() {
     302                        this.getNodes( function() {
     303                                this.unbindNodes.apply( this, arguments );
     304                        }, true );
     305                },
     306
     307                /**
     308                 * Unbinds a given view node.
     309                 * Runs before the view node's content is removed from the DOM.
     310                 *
     311                 * @param {tinymce.Editor} editor      The TinyMCE editor instance the view node is in.
     312                 * @param {HTMLElement}    node        The view node.
     313                 * @param {HTMLElement}    contentNode The view's content node.
     314                 */
     315                unbindNodes: function( /* editor, node, contentNode */ ) {},
    590316
    591                         action: 'parse-media-shortcode',
     317                /**
     318                 * Gets all the TinyMCE editor instances that support views.
     319                 *
     320                 * @param {Function} callback A callback.
     321                 */
     322                getEditors: function( callback ) {
     323                        _.each( tinymce.editors, function( editor ) {
     324                                if ( editor.plugins.wpview ) {
     325                                        callback.call( this, editor );
     326                                }
     327                        }, this );
     328                },
    592329
    593                         initialize: function( options ) {
     330                /**
     331                 * Gets all view nodes tied to this view instance.
     332                 *
     333                 * @param {Function} callback A callback.
     334                 * @param {Boolean}  rendered Get (un)rendered view nodes. Optional.
     335                 */
     336                getNodes: function( callback, rendered ) {
     337                        this.getEditors( function( editor ) {
    594338                                var self = this;
    595339
    596                                 this.shortcode = options.shortcode;
     340                                $( editor.getBody() )
     341                                        .find( '[data-wpview-text="' + self.encodedText + '"]' )
     342                                        .filter( function() {
     343                                                var data;
     344
     345                                                if ( rendered == null ) {
     346                                                        return true;
     347                                                }
    597348
    598                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
    599                                 $( this ).on( 'ready', this.setNodes );
     349                                                data = $( this ).data( 'rendered' ) === true;
    600350
    601                                 $( document ).on( 'media:edit', this.stopPlayers );
     351                                                return rendered ? data : ! data;
     352                                        } )
     353                                        .each( function() {
     354                                                callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) );
     355                                        } );
     356                        } );
     357                },
    602358
    603                                 this.fetch();
     359                /**
     360                 * Gets all marker nodes tied to this view instance.
     361                 *
     362                 * @param {Function} callback A callback.
     363                 */
     364                getMarkers: function( callback ) {
     365                        this.getEditors( function( editor ) {
     366                                var self = this;
    604367
    605                                 this.getEditors( function( editor ) {
    606                                         editor.on( 'hide', function () {
    607                                                 mediaWindows = [];
    608                                                 windowIdx = 0;
    609                                                 self.stopPlayers();
     368                                $( editor.getBody() )
     369                                        .find( '[data-wpview-marker="' + this.encodedText + '"]' )
     370                                        .each( function() {
     371                                                callback.call( self, editor, this );
    610372                                        } );
    611                                 });
    612                         },
     373                        } );
     374                },
    613375
    614                         pauseOtherWindows: function ( win ) {
    615                                 _.each( mediaWindows, function ( mediaWindow ) {
    616                                         if ( mediaWindow.sandboxId !== win.sandboxId ) {
    617                                                 _.each( mediaWindow.mejs.players, function ( player ) {
    618                                                         player.pause();
    619                                                 } );
    620                                         }
    621                                 } );
    622                         },
     376                /**
     377                 * Replaces all marker nodes tied to this view instance.
     378                 */
     379                replaceMarkers: function() {
     380                        this.getMarkers( function( editor, node ) {
     381                                if ( $( node ).text() !== this.text ) {
     382                                        editor.dom.setAttrib( node, 'data-wpview-marker', null );
     383                                        return;
     384                                }
    623385
    624                         iframeLoaded: function (win) {
    625                                 return _.bind( function () {
    626                                         var callback;
    627                                         if ( ! win.mejs || _.isEmpty( win.mejs.players ) ) {
    628                                                 return;
    629                                         }
     386                                editor.dom.replace(
     387                                        editor.dom.createFragment(
     388                                                '<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' +
     389                                                        '<p class="wpview-selection-before">\u00a0</p>' +
     390                                                        '<div class="wpview-body" contenteditable="false">' +
     391                                                                '<div class="toolbar mce-arrow-down">' +
     392                                                                        ( this.edit ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +
     393                                                                        '<div class="dashicons dashicons-no remove"></div>' +
     394                                                                '</div>' +
     395                                                                '<div class="wpview-content wpview-type-' + this.type + '"></div>' +
     396                                                        '</div>' +
     397                                                        '<p class="wpview-selection-after">\u00a0</p>' +
     398                                                '</div>'
     399                                        ),
     400                                        node
     401                                );
     402                        } );
     403                },
    630404
    631                                         win.sandboxId = windowIdx;
    632                                         windowIdx++;
    633                                         mediaWindows.push( win );
    634 
    635                                         callback = _.bind( function () {
    636                                                 this.pauseOtherWindows( win );
    637                                         }, this );
    638 
    639                                         if ( ! _.isEmpty( win.mejs.MediaPluginBridge.pluginMediaElements ) ) {
    640                                                 _.each( win.mejs.MediaPluginBridge.pluginMediaElements, function ( mediaElement ) {
    641                                                         mediaElement.addEventListener( 'play', callback );
    642                                                 } );
    643                                         }
     405                /**
     406                 * Removes all marker nodes tied to this view instance.
     407                 */
     408                removeMarkers: function() {
     409                        this.getMarkers( function( editor, node ) {
     410                                editor.dom.setAttrib( node, 'data-wpview-marker', null );
     411                        } );
     412                },
    644413
    645                                         _.each( win.mejs.players, function ( player ) {
    646                                                 $( player.node ).on( 'play', callback );
    647                                         }, this );
    648                                 }, this );
    649                         },
    650 
    651                         listenToSandboxes: function () {
    652                                 _.each( this.getNodes(), function ( node ) {
    653                                         var win, iframe = $( '.wpview-sandbox', node ).get( 0 );
    654                                         if ( iframe && ( win = iframe.contentWindow ) ) {
    655                                                 $( win ).load( _.bind( this.iframeLoaded( win ), this ) );
     414                /**
     415                 * Sets the content for all view nodes tied to this view instance.
     416                 *
     417                 * @param {*}        content  The content to set.
     418                 * @param {Function} callback A callback. Optional.
     419                 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
     420                 */
     421                setContent: function( content, callback, rendered ) {
     422                        if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) {
     423                                this.setIframes( content.head, content.body, callback, rendered );
     424                        } else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
     425                                this.setIframes( null, content, callback, rendered );
     426                        } else {
     427                                this.getNodes( function( editor, node, contentNode ) {
     428                                        content = content.body || content;
     429
     430                                        if ( content.indexOf( '<iframe' ) !== -1 ) {
     431                                                content += '<div class="wpview-overlay"></div>';
    656432                                        }
    657                                 }, this );
    658                         },
    659433
    660                         deferredListen: function () {
    661                                 window.setTimeout( _.bind( this.listenToSandboxes, this ), this.getNodes().length * waitInterval );
    662                         },
    663 
    664                         setNodes: function () {
    665                                 if ( this.parsed ) {
    666                                         this.setIframes( this.parsed.head, this.parsed.body );
    667                                         this.deferredListen();
    668                                 } else {
    669                                         this.fail();
    670                                 }
    671                         },
     434                                        contentNode.innerHTML = '';
     435                                        contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
    672436
    673                         fetch: function () {
    674                                 var self = this;
     437                                        callback && callback.apply( this, arguments );
     438                                }, rendered );
     439                        }
     440                },
    675441
    676                                 wp.ajax.send( this.action, {
    677                                         data: {
    678                                                 post_ID: $( '#post_ID' ).val() || 0,
    679                                                 type: this.shortcode.tag,
    680                                                 shortcode: this.shortcode.string()
    681                                         }
    682                                 } )
    683                                 .done( function( response ) {
    684                                         if ( response ) {
    685                                                 self.parsed = response;
    686                                                 self.setIframes( response.head, response.body );
    687                                                 self.deferredListen();
    688                                         } else {
    689                                                 self.fail( true );
    690                                         }
    691                                 } )
    692                                 .fail( function( response ) {
    693                                         self.fail( response || true );
    694                                 } );
    695                         },
     442                /**
     443                 * Sets the content in an iframe for all view nodes tied to this view instance.
     444                 *
     445                 * @param {String}   head     HTML string to be added to the head of the document.
     446                 * @param {String}   body     HTML string to be added to the body of the document.
     447                 * @param {Function} callback A callback. Optional.
     448                 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
     449                 */
     450                setIframes: function( head, body, callback, rendered ) {
     451                        var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
     452                                importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';
    696453
    697                         fail: function( error ) {
    698                                 if ( ! this.error ) {
    699                                         if ( error ) {
    700                                                 this.error = error;
    701                                         } else {
    702                                                 return;
    703                                         }
    704                                 }
     454                        this.getNodes( function( editor, node, content ) {
     455                                var dom = editor.dom,
     456                                        styles = '',
     457                                        bodyClasses = editor.getBody().className || '',
     458                                        iframe, iframeDoc, i, resize;
     459
     460                                content.innerHTML = '';
     461                                head = head || '';
     462
     463                                if ( importStyles ) {
     464                                        if ( ! wp.mce.views.sandboxStyles ) {
     465                                                tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {
     466                                                        if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
     467                                                                link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
    705468
    706                                 if ( this.error.message ) {
    707                                         if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' ||
    708                                                 this.error.type === 'no-items' ) {
     469                                                                styles += dom.getOuterHTML( link ) + '\n';
     470                                                        }
     471                                                });
    709472
    710                                                 this.setError( this.error.message, 'admin-media' );
     473                                                wp.mce.views.sandboxStyles = styles;
    711474                                        } else {
    712                                                 this.setContent( '<p>' + this.original + '</p>', 'replace' );
     475                                                styles = wp.mce.views.sandboxStyles;
    713476                                        }
    714                                 } else if ( this.error.statusText ) {
    715                                         this.setError( this.error.statusText, 'admin-media' );
    716                                 } else if ( this.original ) {
    717                                         this.setContent( '<p>' + this.original + '</p>', 'replace' );
    718477                                }
    719                         },
    720478
    721                         stopPlayers: function( remove ) {
    722                                 var rem = remove === 'remove';
     479                                // Seems Firefox needs a bit of time to insert/set the view nodes,
     480                                // or the iframe will fail especially when switching Text => Visual.
     481                                setTimeout( function() {
     482                                        iframe = dom.add( content, 'iframe', {
     483                                                /* jshint scripturl: true */
     484                                                src: tinymce.Env.ie ? 'javascript:""' : '',
     485                                                frameBorder: '0',
     486                                                allowTransparency: 'true',
     487                                                scrolling: 'no',
     488                                                'class': 'wpview-sandbox',
     489                                                style: {
     490                                                        width: '100%',
     491                                                        display: 'block'
     492                                                }
     493                                        } );
     494
     495                                        dom.add( content, 'div', { 'class': 'wpview-overlay' } );
    723496
    724                                 this.getNodes( function( editor, node, content ) {
    725                                         var p, win,
    726                                                 iframe = $( 'iframe.wpview-sandbox', content ).get(0);
     497                                        iframeDoc = iframe.contentWindow.document;
    727498
    728                                         if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) {
    729                                                 // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there.
    730                                                 try {
    731                                                         for ( p in win.mejs.players ) {
    732                                                                 win.mejs.players[p].pause();
     499                                        iframeDoc.open();
    733500
    734                                                                 if ( rem ) {
    735                                                                         win.mejs.players[p].remove();
    736                                                                 }
     501                                        iframeDoc.write(
     502                                                '<!DOCTYPE html>' +
     503                                                '<html>' +
     504                                                        '<head>' +
     505                                                                '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
     506                                                                head +
     507                                                                styles +
     508                                                                '<style>' +
     509                                                                        'html {' +
     510                                                                                'background: transparent;' +
     511                                                                                'padding: 0;' +
     512                                                                                'margin: 0;' +
     513                                                                        '}' +
     514                                                                        'body#wpview-iframe-sandbox {' +
     515                                                                                'background: transparent;' +
     516                                                                                'padding: 1px 0 !important;' +
     517                                                                                'margin: -1px 0 0 !important;' +
     518                                                                        '}' +
     519                                                                        'body#wpview-iframe-sandbox:before,' +
     520                                                                        'body#wpview-iframe-sandbox:after {' +
     521                                                                                'display: none;' +
     522                                                                                'content: "";' +
     523                                                                        '}' +
     524                                                                '</style>' +
     525                                                        '</head>' +
     526                                                        '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
     527                                                                body +
     528                                                        '</body>' +
     529                                                '</html>'
     530                                        );
     531
     532                                        iframeDoc.close();
     533
     534                                        resize = function() {
     535                                                var $iframe, iframeDocHeight;
     536
     537                                                // Make sure the iframe still exists.
     538                                                if ( iframe.contentWindow ) {
     539                                                        $iframe = $( iframe );
     540                                                        iframeDocHeight = $( iframeDoc.body ).height();
     541
     542                                                        if ( $iframe.height() !== iframeDocHeight ) {
     543                                                                $iframe.height( iframeDocHeight );
     544                                                                editor.nodeChanged();
    737545                                                        }
    738                                                 } catch( er ) {}
     546                                                }
     547                                        };
     548
     549                                        if ( MutationObserver ) {
     550                                                new MutationObserver( _.debounce( function() {
     551                                                        resize();
     552                                                }, 100 ) )
     553                                                .observe( iframeDoc.body, {
     554                                                        attributes: true,
     555                                                        childList: true,
     556                                                        subtree: true
     557                                                } );
     558                                        } else {
     559                                                for ( i = 1; i < 6; i++ ) {
     560                                                        setTimeout( resize, i * 700 );
     561                                                }
     562                                        }
     563
     564                                        if ( importStyles ) {
     565                                                editor.on( 'wp-body-class-change', function() {
     566                                                        iframeDoc.body.className = editor.getBody().className;
     567                                                } );
    739568                                        }
    740                                 });
    741                         },
     569                                }, 50 );
     570
     571                                callback && callback.apply( this, arguments );
     572                        }, rendered );
     573                },
     574
     575                /**
     576                 * Sets a loader for all view nodes tied to this view instance.
     577                 */
     578                setLoader: function() {
     579                        this.setContent(
     580                                '<div class="loading-placeholder">' +
     581                                        '<div class="dashicons dashicons-admin-media"></div>' +
     582                                        '<div class="wpview-loading"><ins></ins></div>' +
     583                                '</div>'
     584                        );
     585                },
     586
     587                /**
     588                 * Sets an error for all view nodes tied to this view instance.
     589                 *
     590                 * @param {String} message  The error message to set.
     591                 * @param {String} dashicon A dashicon ID (optional). {@link https://developer.wordpress.org/resource/dashicons/}
     592                 */
     593                setError: function( message, dashicon ) {
     594                        this.setContent(
     595                                '<div class="wpview-error">' +
     596                                        '<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
     597                                        '<p>' + message + '</p>' +
     598                                '</div>'
     599                        );
     600                },
     601
     602                /**
     603                 * Tries to find a text match in a given string.
     604                 *
     605                 * @param {String} content The string to scan.
     606                 *
     607                 * @return {Object}
     608                 */
     609                match: function( content ) {
     610                        var match = wp.shortcode.next( this.type, content );
    742611
    743                         unbind: function() {
    744                                 this.stopPlayers( 'remove' );
     612                        if ( match ) {
     613                                return {
     614                                        index: match.index,
     615                                        content: match.content,
     616                                        options: {
     617                                                shortcode: match.shortcode
     618                                        }
     619                                };
    745620                        }
    746621                },
    747622
    748623                /**
    749                  * Called when a TinyMCE view is clicked for editing.
    750                  * - Parses the shortcode out of the element's data attribute
    751                  * - Calls the `edit` method on the shortcode model
    752                  * - Launches the model window
    753                  * - Bind's an `update` callback which updates the element's data attribute
    754                  *   re-renders the view
     624                 * Update the text of a given view node.
    755625                 *
    756                  * @param {HTMLElement} node
     626                 * @param {String}         text   The new text.
     627                 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
     628                 * @param {HTMLElement}    node   The view node to update.
    757629                 */
    758                 edit: function( node ) {
     630                update: function( text, editor, node ) {
     631                        $( node ).data( 'rendered', false );
     632                        editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
     633                        wp.mce.views.createInstance( this.type, text, this.match( text ).options ).render();
     634                }
     635        } );
     636} )( window, window.wp, window.jQuery );
     637
     638/*
     639 * The WordPress core TinyMCE views.
     640 * Views for the gallery, audio, video, playlist and embed shortcodes,
     641 * and a view for embeddable URLs.
     642 */
     643( function( window, views, $ ) {
     644        var postID = $( '#post_ID' ).val() || 0,
     645                media, gallery, av, embed;
     646
     647        media = {
     648                state: [],
     649
     650                edit: function( text, update ) {
    759651                        var media = wp.media[ this.type ],
    760                                 self = this,
    761                                 frame, data, callback;
     652                                frame = media.edit( text );
    762653
    763                         $( document ).trigger( 'media:edit' );
     654                        this.stopPlayers && this.stopPlayers();
     655
     656                        _.each( this.state, function( state ) {
     657                                frame.state( state ).on( 'update', function( selection ) {
     658                                        update( media.shortcode( selection ).string() );
     659                                } );
     660                        } );
    764661
    765                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    766                         frame = media.edit( data );
    767662                        frame.on( 'close', function() {
    768663                                frame.detach();
    769664                        } );
     665                }
     666        };
    770667
    771                         callback = function( selection ) {
    772                                 var shortcode = wp.media[ self.type ].shortcode( selection ).string();
    773                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
    774                                 wp.mce.views.refreshView( self, shortcode );
    775                                 frame.detach();
    776                         };
    777                         if ( _.isArray( self.state ) ) {
    778                                 _.each( self.state, function (state) {
    779                                         frame.state( state ).on( 'update', callback );
     668        gallery = _.extend( {}, media, {
     669                state: [ 'gallery-edit' ],
     670
     671                initialize: function() {
     672                        var self = this;
     673
     674                        this.template = wp.media.template( 'editor-gallery' );
     675                        this.attachments = wp.media.gallery.attachments( this.shortcode, postID );
     676                        this.dfd = this.attachments.more().done( function() {
     677                                self.render();
     678                        } );
     679                },
     680
     681                getContent: function() {
     682                        var attrs = this.shortcode.attrs.named,
     683                                attachments = false;
     684
     685                        // Don't render errors while still fetching attachments
     686                        if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
     687                                return '';
     688                        }
     689
     690                        if ( this.attachments.length ) {
     691                                attachments = this.attachments.toJSON();
     692
     693                                _.each( attachments, function( attachment ) {
     694                                        if ( attachment.sizes ) {
     695                                                if ( attrs.size && attachment.sizes[ attrs.size ] ) {
     696                                                        attachment.thumbnail = attachment.sizes[ attrs.size ];
     697                                                } else if ( attachment.sizes.thumbnail ) {
     698                                                        attachment.thumbnail = attachment.sizes.thumbnail;
     699                                                } else if ( attachment.sizes.full ) {
     700                                                        attachment.thumbnail = attachment.sizes.full;
     701                                                }
     702                                        }
    780703                                } );
    781                         } else {
    782                                 frame.state( self.state ).on( 'update', callback );
    783704                        }
    784                         frame.open();
     705
     706                        return this.template( {
     707                                attachments: attachments,
     708                                columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
     709                        } );
    785710                }
    786         };
     711        } );
    787712
    788         /**
    789          * TinyMCE handler for the video shortcode
    790          *
    791          * @mixes wp.mce.av
    792          */
    793         wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
    794                 state: 'video-details'
    795         } ) );
     713        av = _.extend( {}, media, {
     714                action: 'parse-media-shortcode',
    796715
    797         /**
    798          * TinyMCE handler for the audio shortcode
    799          *
    800          * @mixes wp.mce.av
    801          */
    802         wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
    803                 state: 'audio-details'
    804         } ) );
     716                initialize: function() {
     717                        var self = this;
    805718
    806         /**
    807          * TinyMCE handler for the playlist shortcode
    808          *
    809          * @mixes wp.mce.av
    810          */
    811         wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
    812                 state: [ 'playlist-edit', 'video-playlist-edit' ]
    813         } ) );
     719                        if ( this.url ) {
     720                                this.loader = false;
     721                                this.shortcode = wp.media.embed.shortcode( {
     722                                        url: this.url
     723                                } );
     724                        }
    814725
    815         /**
    816          * TinyMCE handler for the embed shortcode
    817          */
    818         wp.mce.embedMixin = {
    819                 View: _.extend( {}, wp.mce.av.View, {
    820                         overlay: true,
    821                         action: 'parse-embed',
    822                         initialize: function( options ) {
    823                                 this.content = options.content;
    824                                 this.original = options.url || options.shortcode.string();
    825 
    826                                 if ( options.url ) {
    827                                         this.shortcode = media.embed.shortcode( {
    828                                                 url: options.url
    829                                         } );
     726                        wp.ajax.send( this.action, {
     727                                data: {
     728                                        post_ID: postID,
     729                                        type: this.shortcode.tag,
     730                                        shortcode: this.shortcode.string()
     731                                }
     732                        } )
     733                        .done( function( response ) {
     734                                self.content = response;
     735                                self.render();
     736                        } )
     737                        .fail( function( response ) {
     738                                if ( self.type === 'embedURL' ) {
     739                                        self.removeMarkers();
    830740                                } else {
    831                                         this.shortcode = options.shortcode;
     741                                        self.setError( response.message || response.statusText, 'admin-media' );
    832742                                }
     743                        } );
    833744
    834                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch' );
    835                                 $( this ).on( 'ready', this.setNodes );
    836 
    837                                 this.fetch();
    838                         }
    839                 } ),
    840                 edit: function( node ) {
    841                         var embed = media.embed,
    842                                 self = this,
    843                                 frame,
    844                                 data,
    845                                 isURL = 'embedURL' === this.type;
     745                        this.getEditors( function( editor ) {
     746                                editor.on( 'wpview-selected', function() {
     747                                        self.stopPlayers();
     748                                } );
     749                        } );
     750                },
    846751
    847                         $( document ).trigger( 'media:edit' );
     752                stopPlayers: function( remove ) {
     753                        this.getNodes( function( editor, node, content ) {
     754                                var win = $( 'iframe.wpview-sandbox', content ).get( 0 );
    848755
    849                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    850                         frame = embed.edit( data, isURL );
    851                         frame.on( 'close', function() {
    852                                 frame.detach();
     756                                if ( win && ( win = win.contentWindow ) && win.mejs ) {
     757                                        _.each( win.mejs.players, function( player ) {
     758                                                try {
     759                                                        player[ remove ? 'remove' : 'pause' ]();
     760                                                } catch ( e ) {}
     761                                        } );
     762                                }
    853763                        } );
    854                         frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
    855                                 if ( ! url ) {
    856                                         return;
     764                }
     765        } );
     766
     767        embed = _.extend( {}, av, {
     768                action: 'parse-embed',
     769
     770                edit: function( text, update ) {
     771                        var media = wp.media.embed,
     772                                isURL = 'embedURL' === this.type,
     773                                frame = media.edit( text, isURL );
     774
     775                        this.stopPlayers();
     776
     777                        frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
     778                                if ( url ) {
     779                                        frame.state( 'embed' ).metadata = model.toJSON();
    857780                                }
    858                                 frame.state( 'embed' ).metadata = model.toJSON();
    859781                        } );
    860                         frame.state( 'embed' ).on( 'select', function() {
    861                                 var shortcode;
    862782
     783                        frame.state( 'embed' ).on( 'select', function() {
    863784                                if ( isURL ) {
    864                                         shortcode = frame.state( 'embed' ).metadata.url;
     785                                        update( frame.state( 'embed' ).metadata.url );
    865786                                } else {
    866                                         shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
     787                                        update( media.shortcode( frame.state( 'embed' ).metadata ).string() );
    867788                                }
    868                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
    869                                 wp.mce.views.refreshView( self, shortcode );
     789                        } );
     790
     791                        frame.on( 'close', function() {
    870792                                frame.detach();
    871793                        } );
     794
    872795                        frame.open();
    873796                }
    874         };
     797        } );
     798
     799        views.register( 'gallery', _.extend( {}, gallery ) );
     800
     801        views.register( 'audio', _.extend( {}, av, {
     802                state: [ 'audio-details' ]
     803        } ) );
     804
     805        views.register( 'video', _.extend( {}, av, {
     806                state: [ 'video-details' ]
     807        } ) );
     808
     809        views.register( 'playlist', _.extend( {}, av, {
     810                state: [ 'playlist-edit', 'video-playlist-edit' ]
     811        } ) );
    875812
    876         wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
     813        views.register( 'embed', _.extend( {}, embed ) );
    877814
    878         wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
    879                 toView: function( content ) {
     815        views.register( 'embedURL', _.extend( {}, embed, {
     816                match: function( content ) {
    880817                        var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi,
    881818                                match = re.exec( tinymce.trim( content ) );
    882819
    883                         if ( ! match ) {
    884                                 return;
     820                        if ( match ) {
     821                                return {
     822                                        index: match.index + match[1].length,
     823                                        content: match[2],
     824                                        options: {
     825                                                url: match[2]
     826                                        }
     827                                };
    885828                        }
    886 
    887                         return {
    888                                 index: match.index + match[1].length,
    889                                 content: match[2],
    890                                 options: {
    891                                         url: match[2]
    892                                 }
    893                         };
    894829                }
    895830        } ) );
    896 
    897 }(jQuery));
     831} )( window, window.wp.mce.views, window.jQuery );
  • src/wp-includes/js/tinymce/plugins/wpview/plugin.js

     
    237237                        return;
    238238                }
    239239
    240                 event.content = wp.mce.views.toViews( event.content );
     240                event.content = wp.mce.views.setMarkers( event.content );
    241241        });
    242242
    243243        // When the editor's content has been updated and the DOM has been
     
    341341                                                        editor.focus();
    342342                                                }
    343343
    344                                                 wp.mce.views.edit( view );
     344                                                wp.mce.views.edit( editor, view );
    345345                                                return false;
    346346                                        } else if ( editor.dom.hasClass( event.target, 'remove' ) ) {
    347347                                                removeView( view );