]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/mce-view.js
WordPress 4.1.1-scripts
[autoinstalls/wordpress.git] / wp-includes / js / mce-view.js
1 /* global tinymce */
2 /**
3  * Note: this API is "experimental" meaning that it will probably change
4  * in the next few releases based on feedback from 3.9.0.
5  * If you decide to use it, please follow the development closely.
6  */
7
8 // Ensure the global `wp` object exists.
9 window.wp = window.wp || {};
10
11 ( function( $ ) {
12         'use strict';
13
14         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.
23         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                                                         // Make sure the iframe still exists.
211                                                         iframe.contentWindow && $( iframe ).height( $( iframeDoc.body ).height() );
212                                                 };
213
214                                                 if ( MutationObserver ) {
215                                                         new MutationObserver( _.debounce( function() {
216                                                                 resize();
217                                                         }, 100 ) )
218                                                         .observe( iframeDoc.body, {
219                                                                 attributes: true,
220                                                                 childList: true,
221                                                                 subtree: true
222                                                         } );
223                                                 } else {
224                                                         for ( i = 1; i < 6; i++ ) {
225                                                                 setTimeout( resize, i * 700 );
226                                                         }
227                                                 }
228
229                                                 if ( importStyles ) {
230                                                         editor.on( 'wp-body-class-change', function() {
231                                                                 iframeDoc.body.className = editor.getBody().className;
232                                                         });
233                                                 }
234                                         }, waitInterval );
235                                 });
236                         } else {
237                                 this.setContent( body );
238                         }
239                 },
240                 setError: function( message, dashicon ) {
241                         this.setContent(
242                                 '<div class="wpview-error">' +
243                                         '<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +
244                                         '<p>' + message + '</p>' +
245                                 '</div>'
246                         );
247                 },
248                 rendered: function( value ) {
249                         var notRendered;
250
251                         this.getNodes( function( editor, node ) {
252                                 if ( value != null ) {
253                                         $( node ).data( 'rendered', value === true );
254                                 } else {
255                                         notRendered = notRendered || ! $( node ).data( 'rendered' );
256                                 }
257                         } );
258
259                         return ! notRendered;
260                 }
261         } );
262
263         // take advantage of the Backbone extend method
264         wp.mce.View.extend = Backbone.View.extend;
265
266         /**
267          * wp.mce.views
268          *
269          * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
270          * At its core, it serves as a series of converters, transforming text to a
271          * custom UI, and back again.
272          */
273         wp.mce.views = {
274
275                 /**
276                  * wp.mce.views.register( type, view )
277                  *
278                  * Registers a new TinyMCE view.
279                  *
280                  * @param type
281                  * @param constructor
282                  *
283                  */
284                 register: function( type, constructor ) {
285                         var defaultConstructor = {
286                                         type: type,
287                                         View: {},
288                                         toView: function( content ) {
289                                                 var match = wp.shortcode.next( this.type, content );
290
291                                                 if ( ! match ) {
292                                                         return;
293                                                 }
294
295                                                 return {
296                                                         index: match.index,
297                                                         content: match.content,
298                                                         options: {
299                                                                 shortcode: match.shortcode
300                                                         }
301                                                 };
302                                         }
303                                 };
304
305                         constructor = _.defaults( constructor, defaultConstructor );
306                         constructor.View = wp.mce.View.extend( constructor.View );
307
308                         views[ type ] = constructor;
309                 },
310
311                 /**
312                  * wp.mce.views.get( id )
313                  *
314                  * Returns a TinyMCE view constructor.
315                  *
316                  * @param type
317                  */
318                 get: function( type ) {
319                         return views[ type ];
320                 },
321
322                 /**
323                  * wp.mce.views.unregister( type )
324                  *
325                  * Unregisters a TinyMCE view.
326                  *
327                  * @param type
328                  */
329                 unregister: function( type ) {
330                         delete views[ type ];
331                 },
332
333                 /**
334                  * wp.mce.views.unbind( editor )
335                  *
336                  * The editor DOM is being rebuilt, run cleanup.
337                  */
338                 unbind: function() {
339                         _.each( instances, function( instance ) {
340                                 instance.unbind();
341                         } );
342                 },
343
344                 /**
345                  * toViews( content )
346                  * Scans a `content` string for each view's pattern, replacing any
347                  * matches with wrapper elements, and creates a new instance for
348                  * every match, which triggers the related data to be fetched.
349                  *
350                  * @param content
351                  */
352                 toViews: function( content ) {
353                         var pieces = [ { content: content } ],
354                                 current;
355
356                         _.each( views, function( view, viewType ) {
357                                 current = pieces.slice();
358                                 pieces  = [];
359
360                                 _.each( current, function( piece ) {
361                                         var remaining = piece.content,
362                                                 result;
363
364                                         // Ignore processed pieces, but retain their location.
365                                         if ( piece.processed ) {
366                                                 pieces.push( piece );
367                                                 return;
368                                         }
369
370                                         // Iterate through the string progressively matching views
371                                         // and slicing the string as we go.
372                                         while ( remaining && (result = view.toView( remaining )) ) {
373                                                 // Any text before the match becomes an unprocessed piece.
374                                                 if ( result.index ) {
375                                                         pieces.push({ content: remaining.substring( 0, result.index ) });
376                                                 }
377
378                                                 // Add the processed piece for the match.
379                                                 pieces.push({
380                                                         content: wp.mce.views.toView( viewType, result.content, result.options ),
381                                                         processed: true
382                                                 });
383
384                                                 // Update the remaining content.
385                                                 remaining = remaining.slice( result.index + result.content.length );
386                                         }
387
388                                         // There are no additional matches. If any content remains,
389                                         // add it as an unprocessed piece.
390                                         if ( remaining ) {
391                                                 pieces.push({ content: remaining });
392                                         }
393                                 });
394                         });
395
396                         return _.pluck( pieces, 'content' ).join('');
397                 },
398
399                 /**
400                  * Create a placeholder for a particular view type
401                  *
402                  * @param viewType
403                  * @param text
404                  * @param options
405                  *
406                  */
407                 toView: function( viewType, text, options ) {
408                         var view = wp.mce.views.get( viewType ),
409                                 encodedText = window.encodeURIComponent( text ),
410                                 instance, viewOptions;
411
412
413                         if ( ! view ) {
414                                 return text;
415                         }
416
417                         if ( ! wp.mce.views.getInstance( encodedText ) ) {
418                                 viewOptions = options;
419                                 viewOptions.type = viewType;
420                                 viewOptions.encodedText = encodedText;
421                                 instance = new view.View( viewOptions );
422                                 instances[ encodedText ] = instance;
423                         }
424
425                         return wp.html.string({
426                                 tag: 'div',
427
428                                 attrs: {
429                                         'class': 'wpview-wrap',
430                                         'data-wpview-text': encodedText,
431                                         'data-wpview-type': viewType
432                                 },
433
434                                 content: '\u00a0'
435                         });
436                 },
437
438                 /**
439                  * Refresh views after an update is made
440                  *
441                  * @param view {object} being refreshed
442                  * @param text {string} textual representation of the view
443                  * @param force {Boolean} whether to force rendering
444                  */
445                 refreshView: function( view, text, force ) {
446                         var encodedText = window.encodeURIComponent( text ),
447                                 viewOptions,
448                                 result, instance;
449
450                         instance = wp.mce.views.getInstance( encodedText );
451
452                         if ( ! instance ) {
453                                 result = view.toView( text );
454                                 viewOptions = result.options;
455                                 viewOptions.type = view.type;
456                                 viewOptions.encodedText = encodedText;
457                                 instance = new view.View( viewOptions );
458                                 instances[ encodedText ] = instance;
459                         }
460
461                         instance.render( force );
462                 },
463
464                 getInstance: function( encodedText ) {
465                         return instances[ encodedText ];
466                 },
467
468                 /**
469                  * render( scope )
470                  *
471                  * Renders any view instances inside a DOM node `scope`.
472                  *
473                  * View instances are detected by the presence of wrapper elements.
474                  * To generate wrapper elements, pass your content through
475                  * `wp.mce.view.toViews( content )`.
476                  */
477                 render: function( force ) {
478                         _.each( instances, function( instance ) {
479                                 instance.render( force );
480                         } );
481                 },
482
483                 edit: function( node ) {
484                         var viewType = $( node ).data('wpview-type'),
485                                 view = wp.mce.views.get( viewType );
486
487                         if ( view ) {
488                                 view.edit( node );
489                         }
490                 }
491         };
492
493         wp.mce.views.register( 'gallery', {
494                 View: {
495                         template: media.template( 'editor-gallery' ),
496
497                         // The fallback post ID to use as a parent for galleries that don't
498                         // specify the `ids` or `include` parameters.
499                         //
500                         // Uses the hidden input on the edit posts page by default.
501                         postID: $('#post_ID').val(),
502
503                         initialize: function( options ) {
504                                 this.shortcode = options.shortcode;
505                                 this.fetch();
506                         },
507
508                         fetch: function() {
509                                 var self = this;
510
511                                 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
512                                 this.dfd = this.attachments.more().done( function() {
513                                         self.render( true );
514                                 } );
515                         },
516
517                         getHtml: function() {
518                                 var attrs = this.shortcode.attrs.named,
519                                         attachments = false,
520                                         options;
521
522                                 // Don't render errors while still fetching attachments
523                                 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
524                                         return '';
525                                 }
526
527                                 if ( this.attachments.length ) {
528                                         attachments = this.attachments.toJSON();
529
530                                         _.each( attachments, function( attachment ) {
531                                                 if ( attachment.sizes ) {
532                                                         if ( attrs.size && attachment.sizes[ attrs.size ] ) {
533                                                                 attachment.thumbnail = attachment.sizes[ attrs.size ];
534                                                         } else if ( attachment.sizes.thumbnail ) {
535                                                                 attachment.thumbnail = attachment.sizes.thumbnail;
536                                                         } else if ( attachment.sizes.full ) {
537                                                                 attachment.thumbnail = attachment.sizes.full;
538                                                         }
539                                                 }
540                                         } );
541                                 }
542
543                                 options = {
544                                         attachments: attachments,
545                                         columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
546                                 };
547
548                                 return this.template( options );
549                         }
550                 },
551
552                 edit: function( node ) {
553                         var gallery = wp.media.gallery,
554                                 self = this,
555                                 frame, data;
556
557                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
558                         frame = gallery.edit( data );
559
560                         frame.state('gallery-edit').on( 'update', function( selection ) {
561                                 var shortcode = gallery.shortcode( selection ).string(), force;
562                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
563                                 force = ( data !== shortcode );
564                                 wp.mce.views.refreshView( self, shortcode, force );
565                         });
566
567                         frame.on( 'close', function() {
568                                 frame.detach();
569                         });
570                 }
571         } );
572
573         /**
574          * These are base methods that are shared by the audio and video shortcode's MCE controller.
575          *
576          * @mixin
577          */
578         wp.mce.av = {
579                 View: {
580                         overlay: true,
581
582                         action: 'parse-media-shortcode',
583
584                         initialize: function( options ) {
585                                 var self = this;
586
587                                 this.shortcode = options.shortcode;
588
589                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
590                                 $( this ).on( 'ready', this.setNodes );
591
592                                 $( document ).on( 'media:edit', this.stopPlayers );
593
594                                 this.fetch();
595
596                                 this.getEditors( function( editor ) {
597                                         editor.on( 'hide', function () {
598                                                 mediaWindows = [];
599                                                 windowIdx = 0;
600                                                 self.stopPlayers();
601                                         } );
602                                 });
603                         },
604
605                         pauseOtherWindows: function ( win ) {
606                                 _.each( mediaWindows, function ( mediaWindow ) {
607                                         if ( mediaWindow.sandboxId !== win.sandboxId ) {
608                                                 _.each( mediaWindow.mejs.players, function ( player ) {
609                                                         player.pause();
610                                                 } );
611                                         }
612                                 } );
613                         },
614
615                         iframeLoaded: function (win) {
616                                 return _.bind( function () {
617                                         var callback;
618                                         if ( ! win.mejs || _.isEmpty( win.mejs.players ) ) {
619                                                 return;
620                                         }
621
622                                         win.sandboxId = windowIdx;
623                                         windowIdx++;
624                                         mediaWindows.push( win );
625
626                                         callback = _.bind( function () {
627                                                 this.pauseOtherWindows( win );
628                                         }, this );
629
630                                         if ( ! _.isEmpty( win.mejs.MediaPluginBridge.pluginMediaElements ) ) {
631                                                 _.each( win.mejs.MediaPluginBridge.pluginMediaElements, function ( mediaElement ) {
632                                                         mediaElement.addEventListener( 'play', callback );
633                                                 } );
634                                         }
635
636                                         _.each( win.mejs.players, function ( player ) {
637                                                 $( player.node ).on( 'play', callback );
638                                         }, this );
639                                 }, this );
640                         },
641
642                         listenToSandboxes: function () {
643                                 _.each( this.getNodes(), function ( node ) {
644                                         var win, iframe = $( '.wpview-sandbox', node ).get( 0 );
645                                         if ( iframe && ( win = iframe.contentWindow ) ) {
646                                                 $( win ).load( _.bind( this.iframeLoaded( win ), this ) );
647                                         }
648                                 }, this );
649                         },
650
651                         deferredListen: function () {
652                                 window.setTimeout( _.bind( this.listenToSandboxes, this ), this.getNodes().length * waitInterval );
653                         },
654
655                         setNodes: function () {
656                                 if ( this.parsed ) {
657                                         this.setIframes( this.parsed.head, this.parsed.body );
658                                         this.deferredListen();
659                                 } else {
660                                         this.fail();
661                                 }
662                         },
663
664                         fetch: function () {
665                                 var self = this;
666
667                                 wp.ajax.send( this.action, {
668                                         data: {
669                                                 post_ID: $( '#post_ID' ).val() || 0,
670                                                 type: this.shortcode.tag,
671                                                 shortcode: this.shortcode.string()
672                                         }
673                                 } )
674                                 .done( function( response ) {
675                                         if ( response ) {
676                                                 self.parsed = response;
677                                                 self.setIframes( response.head, response.body );
678                                                 self.deferredListen();
679                                         } else {
680                                                 self.fail( true );
681                                         }
682                                 } )
683                                 .fail( function( response ) {
684                                         self.fail( response || true );
685                                 } );
686                         },
687
688                         fail: function( error ) {
689                                 if ( ! this.error ) {
690                                         if ( error ) {
691                                                 this.error = error;
692                                         } else {
693                                                 return;
694                                         }
695                                 }
696
697                                 if ( this.error.message ) {
698                                         if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' ||
699                                                 this.error.type === 'no-items' ) {
700
701                                                 this.setError( this.error.message, 'admin-media' );
702                                         } else {
703                                                 this.setContent( '<p>' + this.original + '</p>', 'replace' );
704                                         }
705                                 } else if ( this.error.statusText ) {
706                                         this.setError( this.error.statusText, 'admin-media' );
707                                 } else if ( this.original ) {
708                                         this.setContent( '<p>' + this.original + '</p>', 'replace' );
709                                 }
710                         },
711
712                         stopPlayers: function( remove ) {
713                                 var rem = remove === 'remove';
714
715                                 this.getNodes( function( editor, node, content ) {
716                                         var p, win,
717                                                 iframe = $( 'iframe.wpview-sandbox', content ).get(0);
718
719                                         if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) {
720                                                 // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there.
721                                                 try {
722                                                         for ( p in win.mejs.players ) {
723                                                                 win.mejs.players[p].pause();
724
725                                                                 if ( rem ) {
726                                                                         win.mejs.players[p].remove();
727                                                                 }
728                                                         }
729                                                 } catch( er ) {}
730                                         }
731                                 });
732                         },
733
734                         unbind: function() {
735                                 this.stopPlayers( 'remove' );
736                         }
737                 },
738
739                 /**
740                  * Called when a TinyMCE view is clicked for editing.
741                  * - Parses the shortcode out of the element's data attribute
742                  * - Calls the `edit` method on the shortcode model
743                  * - Launches the model window
744                  * - Bind's an `update` callback which updates the element's data attribute
745                  *   re-renders the view
746                  *
747                  * @param {HTMLElement} node
748                  */
749                 edit: function( node ) {
750                         var media = wp.media[ this.type ],
751                                 self = this,
752                                 frame, data, callback;
753
754                         $( document ).trigger( 'media:edit' );
755
756                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
757                         frame = media.edit( data );
758                         frame.on( 'close', function() {
759                                 frame.detach();
760                         } );
761
762                         callback = function( selection ) {
763                                 var shortcode = wp.media[ self.type ].shortcode( selection ).string();
764                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
765                                 wp.mce.views.refreshView( self, shortcode );
766                                 frame.detach();
767                         };
768                         if ( _.isArray( self.state ) ) {
769                                 _.each( self.state, function (state) {
770                                         frame.state( state ).on( 'update', callback );
771                                 } );
772                         } else {
773                                 frame.state( self.state ).on( 'update', callback );
774                         }
775                         frame.open();
776                 }
777         };
778
779         /**
780          * TinyMCE handler for the video shortcode
781          *
782          * @mixes wp.mce.av
783          */
784         wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
785                 state: 'video-details'
786         } ) );
787
788         /**
789          * TinyMCE handler for the audio shortcode
790          *
791          * @mixes wp.mce.av
792          */
793         wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
794                 state: 'audio-details'
795         } ) );
796
797         /**
798          * TinyMCE handler for the playlist shortcode
799          *
800          * @mixes wp.mce.av
801          */
802         wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
803                 state: [ 'playlist-edit', 'video-playlist-edit' ]
804         } ) );
805
806         /**
807          * TinyMCE handler for the embed shortcode
808          */
809         wp.mce.embedMixin = {
810                 View: _.extend( {}, wp.mce.av.View, {
811                         overlay: true,
812                         action: 'parse-embed',
813                         initialize: function( options ) {
814                                 this.content = options.content;
815                                 this.original = options.url || options.shortcode.string();
816
817                                 if ( options.url ) {
818                                         this.shortcode = media.embed.shortcode( {
819                                                 url: options.url
820                                         } );
821                                 } else {
822                                         this.shortcode = options.shortcode;
823                                 }
824
825                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch' );
826                                 $( this ).on( 'ready', this.setNodes );
827
828                                 this.fetch();
829                         }
830                 } ),
831                 edit: function( node ) {
832                         var embed = media.embed,
833                                 self = this,
834                                 frame,
835                                 data,
836                                 isURL = 'embedURL' === this.type;
837
838                         $( document ).trigger( 'media:edit' );
839
840                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
841                         frame = embed.edit( data, isURL );
842                         frame.on( 'close', function() {
843                                 frame.detach();
844                         } );
845                         frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
846                                 if ( ! url ) {
847                                         return;
848                                 }
849                                 frame.state( 'embed' ).metadata = model.toJSON();
850                         } );
851                         frame.state( 'embed' ).on( 'select', function() {
852                                 var shortcode;
853
854                                 if ( isURL ) {
855                                         shortcode = frame.state( 'embed' ).metadata.url;
856                                 } else {
857                                         shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
858                                 }
859                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
860                                 wp.mce.views.refreshView( self, shortcode );
861                                 frame.detach();
862                         } );
863                         frame.open();
864                 }
865         };
866
867         wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
868
869         wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
870                 toView: function( content ) {
871                         var re = /(?:^|<p>)(https?:\/\/[^\s"]+?)(?:<\/p>\s*|$)/gi,
872                                 match = re.exec( tinymce.trim( content ) );
873
874                         if ( ! match ) {
875                                 return;
876                         }
877
878                         return {
879                                 index: match.index,
880                                 content: match[0],
881                                 options: {
882                                         url: match[1]
883                                 }
884                         };
885                 }
886         } ) );
887
888 }(jQuery));