Make WordPress Core

Changeset 31546


Ignore:
Timestamp:
02/25/2015 11:12:08 PM (10 years ago)
Author:
azaozz
Message:

TinyMCE: wpView improvements:

  • Better structure, simpler "view" registration, better extensibility.
  • Better inline documentation.
  • Don't show a placeholder for pasted link until we know the link is "embeddable'.

And many more improvements. Props iseulde. See #31412.

Location:
trunk/src/wp-includes
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/js/mce-view.js

    r31485 r31546  
    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'];
    21 
    22     // Create the `wp.mce` object if necessary.
     31        instances = {};
     32
    2333    wp.mce = wp.mce || {};
    24 
    25     /**
    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;
    27534
    27635    /**
     
    28443
    28544        /**
    286          * wp.mce.views.register( type, view )
    287          *
    288          * Registers a new TinyMCE view.
    289          *
    290          * @param type
    291          * @param constructor
    292          *
    293          */
    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;
    319         },
    320 
    321         /**
    322          * wp.mce.views.get( id )
    323          *
    324          * Returns a TinyMCE view constructor.
    325          *
    326          * @param type
     45         * Registers a new view type.
     46         *
     47         * @param {String} type   The view type.
     48         * @param {Object} extend An object to extend wp.mce.View.prototype with.
     49         */
     50        register: function( type, extend ) {
     51            views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) );
     52        },
     53
     54        /**
     55         * Unregisters a view type.
     56         *
     57         * @param {String} type The view type.
     58         */
     59        unregister: function( type ) {
     60            delete views[ type ];
     61        },
     62
     63        /**
     64         * Returns the settings of a view type.
     65         *
     66         * @param {String} type The view type.
    32767         */
    32868        get: function( type ) {
     
    33171
    33272        /**
    333          * wp.mce.views.unregister( type )
    334          *
    335          * Unregisters a TinyMCE view.
    336          *
    337          * @param type
    338          */
    339         unregister: function( type ) {
    340             delete views[ type ];
    341         },
    342 
    343         /**
    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() {
     
    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.
    359          *
    360          * @param content
    361          */
    362         toViews: function( content ) {
     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.
     86         *
     87         * @param {String} content The string to scan.
     88         */
     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  = [];
     
    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.
     
    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 });
    402                     }
    403                 });
    404             });
    405 
    406             return _.pluck( pieces, 'content' ).join('');
    407         },
    408 
    409         /**
    410          * Create a placeholder for a particular view type
    411          *
    412          * @param viewType
    413          * @param text
    414          * @param options
    415          *
    416          */
    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;
     131                        pieces.push( { content: remaining } );
     132                    }
     133                } );
     134            } );
     135
     136            return _.pluck( pieces, 'content' ).join( '' );
     137        },
     138
     139        /**
     140         * Create a view instance.
     141         *
     142         * @param {String} type    The view type.
     143         * @param {String} text    The textual representation of the view.
     144         * @param {Object} options Options.
     145         */
     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;
    425153            }
    426154
    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;
    433             }
    434 
    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                 },
    443 
    444                 content: '\u00a0'
    445             });
    446         },
    447 
    448         /**
    449          * Refresh views after an update is made
    450          *
    451          * @param view {object} being refreshed
    452          * @param text {string} textual representation of the view
    453          * @param force {Boolean} whether to force rendering
    454          */
    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 ];
    476         },
    477 
    478         /**
    479          * render( scope )
    480          *
    481          * Renders any view instances inside a DOM node `scope`.
    482          *
    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 )`.
     155            options = _.extend( options || {}, {
     156                text: text,
     157                encodedText: encodedText
     158            } );
     159
     160            return instances[ encodedText ] = new View( options );
     161        },
     162
     163        /**
     164         * Get a view instance.
     165         *
     166         * @param {String} text The textual representation of the view.
     167         */
     168        getInstance: function( text ) {
     169            return instances[ encodeURIComponent( text ) ];
     170        },
     171
     172        /**
     173         * Renders all view nodes that are not yet rendered.
     174         *
     175         * @param {Boolean} force Rerender all view nodes.
    486176         */
    487177        render: function( force ) {
     
    491181        },
    492182
    493         edit: function( node ) {
    494             var viewType = $( node ).data('wpview-type'),
    495                 view = wp.mce.views.get( viewType );
    496 
    497             if ( view ) {
    498                 view.edit( node );
     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 );
     193
     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             },
    517 
    518             fetch: function() {
     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    };
     227
     228    wp.mce.View.extend = Backbone.View.extend;
     229
     230    _.extend( wp.mce.View.prototype, {
     231
     232        /**
     233         * The content.
     234         *
     235         * @type {*}
     236         */
     237        content: null,
     238
     239        /**
     240         * Whether or not to display a loader.
     241         *
     242         * @type {Boolean}
     243         */
     244        loader: true,
     245
     246        /**
     247         * Runs after the view instance is created.
     248         */
     249        initialize: function() {},
     250
     251        /**
     252         * Retuns the content to render in the view node.
     253         *
     254         * @return {*}
     255         */
     256        getContent: function() {
     257            return this.content;
     258        },
     259
     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;
     269            }
     270
     271            // We're about to rerender all views of this instance, so unbind rendered views.
     272            force && this.unbind();
     273
     274            // Replace any left over markers.
     275            this.replaceMarkers();
     276
     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        },
     286
     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 */ ) {},
     296
     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 */ ) {},
     316
     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        },
     329
     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 ) {
    519338                var self = this;
    520339
    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             },
    526 
    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 '';
     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                        }
     348
     349                        data = $( this ).data( 'rendered' ) === true;
     350
     351                        return rendered ? data : ! data;
     352                    } )
     353                    .each( function() {
     354                        callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) );
     355                    } );
     356            } );
     357        },
     358
     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;
     367
     368                $( editor.getBody() )
     369                    .find( '[data-wpview-marker="' + this.encodedText + '"]' )
     370                    .each( function() {
     371                        callback.call( self, editor, this );
     372                    } );
     373            } );
     374        },
     375
     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;
    535384                }
    536385
    537                 if ( this.attachments.length ) {
    538                     attachments = this.attachments.toJSON();
    539 
    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;
     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        },
     404
     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        },
     413
     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>';
     432                    }
     433
     434                    contentNode.innerHTML = '';
     435                    contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
     436
     437                    callback && callback.apply( this, arguments );
     438                }, rendered );
     439            }
     440        },
     441
     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';
     453
     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 ) {
     468
     469                                styles += dom.getOuterHTML( link ) + '\n';
     470                            }
     471                        });
     472
     473                        wp.mce.views.sandboxStyles = styles;
     474                    } else {
     475                        styles = wp.mce.views.sandboxStyles;
     476                    }
     477                }
     478
     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' } );
     496
     497                    iframeDoc = iframe.contentWindow.document;
     498
     499                    iframeDoc.open();
     500
     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();
    548545                            }
    549546                        }
     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                        } );
     568                    }
     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 );
     611
     612            if ( match ) {
     613                return {
     614                    index: match.index,
     615                    content: match.content,
     616                    options: {
     617                        shortcode: match.shortcode
     618                    }
     619                };
     620            }
     621        },
     622
     623        /**
     624         * Update the text of a given view node.
     625         *
     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.
     629         */
     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 ) {
     651            var media = wp.media[ this.type ],
     652                frame = media.edit( text );
     653
     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            } );
     661
     662            frame.on( 'close', function() {
     663                frame.detach();
     664            } );
     665        }
     666    };
     667
     668    gallery = _.extend( {}, media, {
     669        state: [ 'gallery-edit' ],
     670        template: wp.media.template( 'editor-gallery' ),
     671
     672        initialize: function() {
     673            var attachments = wp.media.gallery.attachments( this.shortcode, postID ),
     674                attrs = this.shortcode.attrs.named,
     675                self = this;
     676
     677            attachments.more()
     678            .done( function() {
     679                attachments = attachments.toJSON();
     680
     681                _.each( attachments, function( attachment ) {
     682                    if ( attachment.sizes ) {
     683                        if ( attrs.size && attachment.sizes[ attrs.size ] ) {
     684                            attachment.thumbnail = attachment.sizes[ attrs.size ];
     685                        } else if ( attachment.sizes.thumbnail ) {
     686                            attachment.thumbnail = attachment.sizes.thumbnail;
     687                        } else if ( attachment.sizes.full ) {
     688                            attachment.thumbnail = attachment.sizes.full;
     689                        }
     690                    }
     691                } );
     692
     693                self.content = self.template( {
     694                    attachments: attachments,
     695                    columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
     696                } );
     697
     698                self.render();
     699            } )
     700            .fail( function( jqXHR, textStatus ) {
     701                self.setError( textStatus );
     702            } );
     703        }
     704    } );
     705
     706    av = _.extend( {}, media, {
     707        action: 'parse-media-shortcode',
     708
     709        initialize: function() {
     710            var self = this;
     711
     712            if ( this.url ) {
     713                this.loader = false;
     714                this.shortcode = wp.media.embed.shortcode( {
     715                    url: this.url
     716                } );
     717            }
     718
     719            wp.ajax.send( this.action, {
     720                data: {
     721                    post_ID: postID,
     722                    type: this.shortcode.tag,
     723                    shortcode: this.shortcode.string()
     724                }
     725            } )
     726            .done( function( response ) {
     727                self.content = response;
     728                self.render();
     729            } )
     730            .fail( function( response ) {
     731                if ( self.type === 'embedURL' ) {
     732                    self.removeMarkers();
     733                } else {
     734                    self.setError( response.message || response.statusText, 'admin-media' );
     735                }
     736            } );
     737
     738            this.getEditors( function( editor ) {
     739                editor.on( 'wpview-selected', function() {
     740                    self.stopPlayers();
     741                } );
     742            } );
     743        },
     744
     745        stopPlayers: function( remove ) {
     746            this.getNodes( function( editor, node, content ) {
     747                var win = $( 'iframe.wpview-sandbox', content ).get( 0 );
     748
     749                if ( win && ( win = win.contentWindow ) && win.mejs ) {
     750                    _.each( win.mejs.players, function( player ) {
     751                        try {
     752                            player[ remove ? 'remove' : 'pause' ]();
     753                        } catch ( e ) {}
    550754                    } );
    551755                }
    552 
    553                 options = {
    554                     attachments: attachments,
    555                     columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
    556                 };
    557 
    558                 return this.template( options );
    559             }
    560         },
    561 
    562         edit: function( node ) {
    563             var gallery = wp.media.gallery,
    564                 self = this,
    565                 frame, data;
    566 
    567             data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    568             frame = gallery.edit( data );
    569 
    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             });
     756            } );
     757        }
     758    } );
     759
     760    embed = _.extend( {}, av, {
     761        action: 'parse-embed',
     762
     763        edit: function( text, update ) {
     764            var media = wp.media.embed,
     765                isURL = 'embedURL' === this.type,
     766                frame = media.edit( text, isURL );
     767
     768            this.stopPlayers();
     769
     770            frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
     771                if ( url ) {
     772                    frame.state( 'embed' ).metadata = model.toJSON();
     773                }
     774            } );
     775
     776            frame.state( 'embed' ).on( 'select', function() {
     777                if ( isURL ) {
     778                    update( frame.state( 'embed' ).metadata.url );
     779                } else {
     780                    update( media.shortcode( frame.state( 'embed' ).metadata ).string() );
     781                }
     782            } );
    575783
    576784            frame.on( 'close', function() {
    577785                frame.detach();
    578             });
     786            } );
     787
     788            frame.open();
    579789        }
    580790    } );
    581791
    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,
    590 
    591             action: 'parse-media-shortcode',
    592 
    593             initialize: function( options ) {
    594                 var self = this;
    595 
    596                 this.shortcode = options.shortcode;
    597 
    598                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
    599                 $( this ).on( 'ready', this.setNodes );
    600 
    601                 $( document ).on( 'media:edit', this.stopPlayers );
    602 
    603                 this.fetch();
    604 
    605                 this.getEditors( function( editor ) {
    606                     editor.on( 'hide', function () {
    607                         mediaWindows = [];
    608                         windowIdx = 0;
    609                         self.stopPlayers();
    610                     } );
    611                 });
    612             },
    613 
    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             },
    623 
    624             iframeLoaded: function (win) {
    625                 return _.bind( function () {
    626                     var callback;
    627                     if ( ! win.mejs || _.isEmpty( win.mejs.players ) ) {
    628                         return;
    629                     }
    630 
    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                     }
    644 
    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 ) );
    656                     }
    657                 }, this );
    658             },
    659 
    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             },
    672 
    673             fetch: function () {
    674                 var self = this;
    675 
    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             },
    696 
    697             fail: function( error ) {
    698                 if ( ! this.error ) {
    699                     if ( error ) {
    700                         this.error = error;
    701                     } else {
    702                         return;
    703                     }
    704                 }
    705 
    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' ) {
    709 
    710                         this.setError( this.error.message, 'admin-media' );
    711                     } else {
    712                         this.setContent( '<p>' + this.original + '</p>', 'replace' );
    713                     }
    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' );
    718                 }
    719             },
    720 
    721             stopPlayers: function( remove ) {
    722                 var rem = remove === 'remove';
    723 
    724                 this.getNodes( function( editor, node, content ) {
    725                     var p, win,
    726                         iframe = $( 'iframe.wpview-sandbox', content ).get(0);
    727 
    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();
    733 
    734                                 if ( rem ) {
    735                                     win.mejs.players[p].remove();
    736                                 }
    737                             }
    738                         } catch( er ) {}
    739                     }
    740                 });
    741             },
    742 
    743             unbind: function() {
    744                 this.stopPlayers( 'remove' );
    745             }
    746         },
    747 
    748         /**
    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
    755          *
    756          * @param {HTMLElement} node
    757          */
    758         edit: function( node ) {
    759             var media = wp.media[ this.type ],
    760                 self = this,
    761                 frame, data, callback;
    762 
    763             $( document ).trigger( 'media:edit' );
    764 
    765             data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    766             frame = media.edit( data );
    767             frame.on( 'close', function() {
    768                 frame.detach();
    769             } );
    770 
    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 );
    780                 } );
    781             } else {
    782                 frame.state( self.state ).on( 'update', callback );
    783             }
    784             frame.open();
    785         }
    786     };
    787 
    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'
     792    views.register( 'gallery', _.extend( {}, gallery ) );
     793
     794    views.register( 'audio', _.extend( {}, av, {
     795        state: [ 'audio-details' ]
    795796    } ) );
    796797
    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'
     798    views.register( 'video', _.extend( {}, av, {
     799        state: [ 'video-details' ]
    804800    } ) );
    805801
    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, {
     802    views.register( 'playlist', _.extend( {}, av, {
    812803        state: [ 'playlist-edit', 'video-playlist-edit' ]
    813804    } ) );
    814805
    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                     } );
    830                 } else {
    831                     this.shortcode = options.shortcode;
    832                 }
    833 
    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;
    846 
    847             $( document ).trigger( 'media:edit' );
    848 
    849             data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
    850             frame = embed.edit( data, isURL );
    851             frame.on( 'close', function() {
    852                 frame.detach();
    853             } );
    854             frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
    855                 if ( ! url ) {
    856                     return;
    857                 }
    858                 frame.state( 'embed' ).metadata = model.toJSON();
    859             } );
    860             frame.state( 'embed' ).on( 'select', function() {
    861                 var shortcode;
    862 
    863                 if ( isURL ) {
    864                     shortcode = frame.state( 'embed' ).metadata.url;
    865                 } else {
    866                     shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
    867                 }
    868                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
    869                 wp.mce.views.refreshView( self, shortcode );
    870                 frame.detach();
    871             } );
    872             frame.open();
    873         }
    874     };
    875 
    876     wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
    877 
    878     wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
    879         toView: function( content ) {
     806    views.register( 'embed', _.extend( {}, embed ) );
     807
     808    views.register( 'embedURL', _.extend( {}, embed, {
     809        match: function( content ) {
    880810            var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi,
    881811                match = re.exec( tinymce.trim( content ) );
    882812
    883             if ( ! match ) {
    884                 return;
     813            if ( match ) {
     814                return {
     815                    index: match.index + match[1].length,
     816                    content: match[2],
     817                    options: {
     818                        url: match[2]
     819                    }
     820                };
    885821            }
    886 
    887             return {
    888                 index: match.index + match[1].length,
    889                 content: match[2],
    890                 options: {
    891                     url: match[2]
    892                 }
    893             };
    894822        }
    895823    } ) );
    896 
    897 }(jQuery));
     824} )( window, window.wp.mce.views, window.jQuery );
  • trunk/src/wp-includes/js/tinymce/plugins/wpview/plugin.js

    r31143 r31546  
    238238        }
    239239
    240         event.content = wp.mce.views.toViews( event.content );
     240        event.content = wp.mce.views.setMarkers( event.content );
    241241    });
    242242
     
    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' ) ) {
  • trunk/src/wp-includes/media-template.php

    r31016 r31546  
    12061206
    12071207    <script type="text/html" id="tmpl-editor-gallery">
    1208         <# if ( data.attachments ) { #>
     1208        <# if ( data.attachments.length ) { #>
    12091209            <div class="gallery gallery-columns-{{ data.columns }}">
    12101210                <# _.each( data.attachments, function( attachment, index ) { #>
  • trunk/src/wp-includes/version.php

    r31533 r31546  
    1919 * @global string $tinymce_version
    2020 */
    21 $tinymce_version = '4107-20141130';
     21$tinymce_version = '4107-20150225';
    2222
    2323/**
Note: See TracChangeset for help on using the changeset viewer.