]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/mce-view.js
Wordpress 4.5.3
[autoinstalls/wordpress.git] / wp-includes / js / mce-view.js
1 /* global tinymce */
2
3 /*
4  * The TinyMCE view API.
5  *
6  * Note: this API is "experimental" meaning that it will probably change
7  * in the next few releases based on feedback from 3.9.0.
8  * If you decide to use it, please follow the development closely.
9  *
10  * Diagram
11  *
12  * |- registered view constructor (type)
13  * |  |- view instance (unique text)
14  * |  |  |- editor 1
15  * |  |  |  |- view node
16  * |  |  |  |- view node
17  * |  |  |  |- ...
18  * |  |  |- editor 2
19  * |  |  |  |- ...
20  * |  |- view instance
21  * |  |  |- ...
22  * |- registered view
23  * |  |- ...
24  */
25 ( function( window, wp, shortcode, $ ) {
26         'use strict';
27
28         var views = {},
29                 instances = {};
30
31         wp.mce = wp.mce || {};
32
33         /**
34          * wp.mce.views
35          *
36          * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
37          * At its core, it serves as a series of converters, transforming text to a
38          * custom UI, and back again.
39          */
40         wp.mce.views = {
41
42                 /**
43                  * Registers a new view type.
44                  *
45                  * @param {String} type   The view type.
46                  * @param {Object} extend An object to extend wp.mce.View.prototype with.
47                  */
48                 register: function( type, extend ) {
49                         views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) );
50                 },
51
52                 /**
53                  * Unregisters a view type.
54                  *
55                  * @param {String} type The view type.
56                  */
57                 unregister: function( type ) {
58                         delete views[ type ];
59                 },
60
61                 /**
62                  * Returns the settings of a view type.
63                  *
64                  * @param {String} type The view type.
65                  *
66                  * @return {Function} The view constructor.
67                  */
68                 get: function( type ) {
69                         return views[ type ];
70                 },
71
72                 /**
73                  * Unbinds all view nodes.
74                  * Runs before removing all view nodes from the DOM.
75                  */
76                 unbind: function() {
77                         _.each( instances, function( instance ) {
78                                 instance.unbind();
79                         } );
80                 },
81
82                 /**
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                  * @return {String} The string with markers.
90                  */
91                 setMarkers: function( content ) {
92                         var pieces = [ { content: content } ],
93                                 self = this,
94                                 instance, current;
95
96                         _.each( views, function( view, type ) {
97                                 current = pieces.slice();
98                                 pieces  = [];
99
100                                 _.each( current, function( piece ) {
101                                         var remaining = piece.content,
102                                                 result, text;
103
104                                         // Ignore processed pieces, but retain their location.
105                                         if ( piece.processed ) {
106                                                 pieces.push( piece );
107                                                 return;
108                                         }
109
110                                         // Iterate through the string progressively matching views
111                                         // and slicing the string as we go.
112                                         while ( remaining && ( result = view.prototype.match( remaining ) ) ) {
113                                                 // Any text before the match becomes an unprocessed piece.
114                                                 if ( result.index ) {
115                                                         pieces.push( { content: remaining.substring( 0, result.index ) } );
116                                                 }
117
118                                                 instance = self.createInstance( type, result.content, result.options );
119                                                 text = instance.loader ? '.' : instance.text;
120
121                                                 // Add the processed piece for the match.
122                                                 pieces.push( {
123                                                         content: instance.ignore ? text : '<p data-wpview-marker="' + instance.encodedText + '">' + text + '</p>',
124                                                         processed: true
125                                                 } );
126
127                                                 // Update the remaining content.
128                                                 remaining = remaining.slice( result.index + result.content.length );
129                                         }
130
131                                         // There are no additional matches.
132                                         // If any content remains, add it as an unprocessed piece.
133                                         if ( remaining ) {
134                                                 pieces.push( { content: remaining } );
135                                         }
136                                 } );
137                         } );
138
139                         content = _.pluck( pieces, 'content' ).join( '' );
140                         return content.replace( /<p>\s*<p data-wpview-marker=/g, '<p data-wpview-marker=' ).replace( /<\/p>\s*<\/p>/g, '</p>' );
141                 },
142
143                 /**
144                  * Create a view instance.
145                  *
146                  * @param {String}  type    The view type.
147                  * @param {String}  text    The textual representation of the view.
148                  * @param {Object}  options Options.
149                  * @param {Boolean} force   Recreate the instance. Optional.
150                  *
151                  * @return {wp.mce.View} The view instance.
152                  */
153                 createInstance: function( type, text, options, force ) {
154                         var View = this.get( type ),
155                                 encodedText,
156                                 instance;
157
158                         text = tinymce.DOM.decode( text );
159
160                         if ( ! force ) {
161                                 instance = this.getInstance( text );
162
163                                 if ( instance ) {
164                                         return instance;
165                                 }
166                         }
167
168                         encodedText = encodeURIComponent( text );
169
170                         options = _.extend( options || {}, {
171                                 text: text,
172                                 encodedText: encodedText
173                         } );
174
175                         return instances[ encodedText ] = new View( options );
176                 },
177
178                 /**
179                  * Get a view instance.
180                  *
181                  * @param {(String|HTMLElement)} object The textual representation of the view or the view node.
182                  *
183                  * @return {wp.mce.View} The view instance or undefined.
184                  */
185                 getInstance: function( object ) {
186                         if ( typeof object === 'string' ) {
187                                 return instances[ encodeURIComponent( object ) ];
188                         }
189
190                         return instances[ $( object ).attr( 'data-wpview-text' ) ];
191                 },
192
193                 /**
194                  * Given a view node, get the view's text.
195                  *
196                  * @param {HTMLElement} node The view node.
197                  *
198                  * @return {String} The textual representation of the view.
199                  */
200                 getText: function( node ) {
201                         return decodeURIComponent( $( node ).attr( 'data-wpview-text' ) || '' );
202                 },
203
204                 /**
205                  * Renders all view nodes that are not yet rendered.
206                  *
207                  * @param {Boolean} force Rerender all view nodes.
208                  */
209                 render: function( force ) {
210                         _.each( instances, function( instance ) {
211                                 instance.render( force );
212                         } );
213                 },
214
215                 /**
216                  * Update the text of a given view node.
217                  *
218                  * @param {String}         text   The new text.
219                  * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
220                  * @param {HTMLElement}    node   The view node to update.
221                  * @param {Boolean}        force  Recreate the instance. Optional.
222                  */
223                 update: function( text, editor, node, force ) {
224                         var instance = this.getInstance( node );
225
226                         if ( instance ) {
227                                 instance.update( text, editor, node, force );
228                         }
229                 },
230
231                 /**
232                  * Renders any editing interface based on the view type.
233                  *
234                  * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
235                  * @param {HTMLElement}    node   The view node to edit.
236                  */
237                 edit: function( editor, node ) {
238                         var instance = this.getInstance( node );
239
240                         if ( instance && instance.edit ) {
241                                 instance.edit( instance.text, function( text, force ) {
242                                         instance.update( text, editor, node, force );
243                                 } );
244                         }
245                 },
246
247                 /**
248                  * Remove a given view node from the DOM.
249                  *
250                  * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
251                  * @param {HTMLElement}    node   The view node to remove.
252                  */
253                 remove: function( editor, node ) {
254                         var instance = this.getInstance( node );
255
256                         if ( instance ) {
257                                 instance.remove( editor, node );
258                         }
259                 }
260         };
261
262         /**
263          * A Backbone-like View constructor intended for use when rendering a TinyMCE View.
264          * The main difference is that the TinyMCE View is not tied to a particular DOM node.
265          *
266          * @param {Object} options Options.
267          */
268         wp.mce.View = function( options ) {
269                 _.extend( this, options );
270                 this.initialize();
271         };
272
273         wp.mce.View.extend = Backbone.View.extend;
274
275         _.extend( wp.mce.View.prototype, {
276
277                 /**
278                  * The content.
279                  *
280                  * @type {*}
281                  */
282                 content: null,
283
284                 /**
285                  * Whether or not to display a loader.
286                  *
287                  * @type {Boolean}
288                  */
289                 loader: true,
290
291                 /**
292                  * Runs after the view instance is created.
293                  */
294                 initialize: function() {},
295
296                 /**
297                  * Retuns the content to render in the view node.
298                  *
299                  * @return {*}
300                  */
301                 getContent: function() {
302                         return this.content;
303                 },
304
305                 /**
306                  * Renders all view nodes tied to this view instance that are not yet rendered.
307                  *
308                  * @param {String}  content The content to render. Optional.
309                  * @param {Boolean} force   Rerender all view nodes tied to this view instance. Optional.
310                  */
311                 render: function( content, force ) {
312                         if ( content != null ) {
313                                 this.content = content;
314                         }
315
316                         content = this.getContent();
317
318                         // If there's nothing to render an no loader needs to be shown, stop.
319                         if ( ! this.loader && ! content ) {
320                                 return;
321                         }
322
323                         // We're about to rerender all views of this instance, so unbind rendered views.
324                         force && this.unbind();
325
326                         // Replace any left over markers.
327                         this.replaceMarkers();
328
329                         if ( content ) {
330                                 this.setContent( content, function( editor, node, contentNode ) {
331                                         $( node ).data( 'rendered', true );
332                                         this.bindNode.call( this, editor, node, contentNode );
333                                 }, force ? null : false );
334                         } else {
335                                 this.setLoader();
336                         }
337                 },
338
339                 /**
340                  * Binds a given node after its content is added to the DOM.
341                  */
342                 bindNode: function() {},
343
344                 /**
345                  * Unbinds a given node before its content is removed from the DOM.
346                  */
347                 unbindNode: function() {},
348
349                 /**
350                  * Unbinds all view nodes tied to this view instance.
351                  * Runs before their content is removed from the DOM.
352                  */
353                 unbind: function() {
354                         this.getNodes( function( editor, node, contentNode ) {
355                                 this.unbindNode.call( this, editor, node, contentNode );
356                                 $( node ).trigger( 'wp-mce-view-unbind' );
357                         }, true );
358                 },
359
360                 /**
361                  * Gets all the TinyMCE editor instances that support views.
362                  *
363                  * @param {Function} callback A callback.
364                  */
365                 getEditors: function( callback ) {
366                         _.each( tinymce.editors, function( editor ) {
367                                 if ( editor.plugins.wpview ) {
368                                         callback.call( this, editor );
369                                 }
370                         }, this );
371                 },
372
373                 /**
374                  * Gets all view nodes tied to this view instance.
375                  *
376                  * @param {Function} callback A callback.
377                  * @param {Boolean}  rendered Get (un)rendered view nodes. Optional.
378                  */
379                 getNodes: function( callback, rendered ) {
380                         this.getEditors( function( editor ) {
381                                 var self = this;
382
383                                 $( editor.getBody() )
384                                         .find( '[data-wpview-text="' + self.encodedText + '"]' )
385                                         .filter( function() {
386                                                 var data;
387
388                                                 if ( rendered == null ) {
389                                                         return true;
390                                                 }
391
392                                                 data = $( this ).data( 'rendered' ) === true;
393
394                                                 return rendered ? data : ! data;
395                                         } )
396                                         .each( function() {
397                                                 callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) );
398                                         } );
399                         } );
400                 },
401
402                 /**
403                  * Gets all marker nodes tied to this view instance.
404                  *
405                  * @param {Function} callback A callback.
406                  */
407                 getMarkers: function( callback ) {
408                         this.getEditors( function( editor ) {
409                                 var self = this;
410
411                                 $( editor.getBody() )
412                                         .find( '[data-wpview-marker="' + this.encodedText + '"]' )
413                                         .each( function() {
414                                                 callback.call( self, editor, this );
415                                         } );
416                         } );
417                 },
418
419                 /**
420                  * Replaces all marker nodes tied to this view instance.
421                  */
422                 replaceMarkers: function() {
423                         this.getMarkers( function( editor, node ) {
424                                 var selected = node === editor.selection.getNode(),
425                                         $viewNode;
426
427                                 if ( ! this.loader && $( node ).text() !== this.text ) {
428                                         editor.dom.setAttrib( node, 'data-wpview-marker', null );
429                                         return;
430                                 }
431
432                                 $viewNode = editor.$(
433                                         '<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' +
434                                                 '<p class="wpview-selection-before">\u00a0</p>' +
435                                                 '<div class="wpview-body" contenteditable="false">' +
436                                                         '<div class="wpview-content wpview-type-' + this.type + '"></div>' +
437                                                 '</div>' +
438                                                 '<p class="wpview-selection-after">\u00a0</p>' +
439                                         '</div>'
440                                 );
441
442                                 editor.$( node ).replaceWith( $viewNode );
443
444                                 if ( selected ) {
445                                         editor.wp.setViewCursor( false, $viewNode[0] );
446                                 }
447                         } );
448                 },
449
450                 /**
451                  * Removes all marker nodes tied to this view instance.
452                  */
453                 removeMarkers: function() {
454                         this.getMarkers( function( editor, node ) {
455                                 editor.dom.setAttrib( node, 'data-wpview-marker', null );
456                         } );
457                 },
458
459                 /**
460                  * Sets the content for all view nodes tied to this view instance.
461                  *
462                  * @param {*}        content  The content to set.
463                  * @param {Function} callback A callback. Optional.
464                  * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
465                  */
466                 setContent: function( content, callback, rendered ) {
467                         if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) {
468                                 this.setIframes( content.head || '', content.body, callback, rendered );
469                         } else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
470                                 this.setIframes( '', content, callback, rendered );
471                         } else {
472                                 this.getNodes( function( editor, node, contentNode ) {
473                                         content = content.body || content;
474
475                                         if ( content.indexOf( '<iframe' ) !== -1 ) {
476                                                 content += '<div class="wpview-overlay"></div>';
477                                         }
478
479                                         contentNode.innerHTML = '';
480                                         contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
481
482                                         callback && callback.call( this, editor, node, contentNode );
483                                 }, rendered );
484                         }
485                 },
486
487                 /**
488                  * Sets the content in an iframe for all view nodes tied to this view instance.
489                  *
490                  * @param {String}   head     HTML string to be added to the head of the document.
491                  * @param {String}   body     HTML string to be added to the body of the document.
492                  * @param {Function} callback A callback. Optional.
493                  * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
494                  */
495                 setIframes: function( head, body, callback, rendered ) {
496                         var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
497                                 self = this;
498
499                         this.getNodes( function( editor, node, contentNode ) {
500                                 var dom = editor.dom,
501                                         styles = '',
502                                         bodyClasses = editor.getBody().className || '',
503                                         editorHead = editor.getDoc().getElementsByTagName( 'head' )[0];
504
505                                 tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), function( link ) {
506                                         if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
507                                                 link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
508
509                                                 styles += dom.getOuterHTML( link );
510                                         }
511                                 } );
512
513                                 if ( self.iframeHeight ) {
514                                         dom.add( contentNode, 'div', { style: {
515                                                 width: '100%',
516                                                 height: self.iframeHeight
517                                         } } );
518                                 }
519
520                                 // Seems the browsers need a bit of time to insert/set the view nodes,
521                                 // or the iframe will fail especially when switching Text => Visual.
522                                 setTimeout( function() {
523                                         var iframe, iframeDoc, observer, i, block;
524
525                                         contentNode.innerHTML = '';
526
527                                         iframe = dom.add( contentNode, 'iframe', {
528                                                 /* jshint scripturl: true */
529                                                 src: tinymce.Env.ie ? 'javascript:""' : '',
530                                                 frameBorder: '0',
531                                                 allowTransparency: 'true',
532                                                 scrolling: 'no',
533                                                 'class': 'wpview-sandbox',
534                                                 style: {
535                                                         width: '100%',
536                                                         display: 'block'
537                                                 },
538                                                 height: self.iframeHeight
539                                         } );
540
541                                         dom.add( contentNode, 'div', { 'class': 'wpview-overlay' } );
542
543                                         iframeDoc = iframe.contentWindow.document;
544
545                                         iframeDoc.open();
546
547                                         iframeDoc.write(
548                                                 '<!DOCTYPE html>' +
549                                                 '<html>' +
550                                                         '<head>' +
551                                                                 '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
552                                                                 head +
553                                                                 styles +
554                                                                 '<style>' +
555                                                                         'html {' +
556                                                                                 'background: transparent;' +
557                                                                                 'padding: 0;' +
558                                                                                 'margin: 0;' +
559                                                                         '}' +
560                                                                         'body#wpview-iframe-sandbox {' +
561                                                                                 'background: transparent;' +
562                                                                                 'padding: 1px 0 !important;' +
563                                                                                 'margin: -1px 0 0 !important;' +
564                                                                         '}' +
565                                                                         'body#wpview-iframe-sandbox:before,' +
566                                                                         'body#wpview-iframe-sandbox:after {' +
567                                                                                 'display: none;' +
568                                                                                 'content: "";' +
569                                                                         '}' +
570                                                                 '</style>' +
571                                                         '</head>' +
572                                                         '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
573                                                                 body +
574                                                         '</body>' +
575                                                 '</html>'
576                                         );
577
578                                         iframeDoc.close();
579
580                                         function resize() {
581                                                 var $iframe;
582
583                                                 if ( block ) {
584                                                         return;
585                                                 }
586
587                                                 // Make sure the iframe still exists.
588                                                 if ( iframe.contentWindow ) {
589                                                         $iframe = $( iframe );
590                                                         self.iframeHeight = $( iframeDoc.body ).height();
591
592                                                         if ( $iframe.height() !== self.iframeHeight ) {
593                                                                 $iframe.height( self.iframeHeight );
594                                                                 editor.nodeChanged();
595                                                         }
596                                                 }
597                                         }
598
599                                         if ( self.iframeHeight ) {
600                                                 block = true;
601
602                                                 setTimeout( function() {
603                                                         block = false;
604                                                         resize();
605                                                 }, 3000 );
606                                         }
607
608                                         $( iframe.contentWindow ).on( 'load', resize );
609
610                                         if ( MutationObserver ) {
611                                                 observer = new MutationObserver( _.debounce( resize, 100 ) );
612
613                                                 observer.observe( iframeDoc.body, {
614                                                         attributes: true,
615                                                         childList: true,
616                                                         subtree: true
617                                                 } );
618
619                                                 $( node ).one( 'wp-mce-view-unbind', function() {
620                                                         observer.disconnect();
621                                                 } );
622                                         } else {
623                                                 for ( i = 1; i < 6; i++ ) {
624                                                         setTimeout( resize, i * 700 );
625                                                 }
626                                         }
627
628                                         function classChange() {
629                                                 iframeDoc.body.className = editor.getBody().className;
630                                         }
631
632                                         editor.on( 'wp-body-class-change', classChange );
633
634                                         $( node ).one( 'wp-mce-view-unbind', function() {
635                                                 editor.off( 'wp-body-class-change', classChange );
636                                         } );
637
638                                         callback && callback.call( self, editor, node, contentNode );
639                                 }, 50 );
640                         }, rendered );
641                 },
642
643                 /**
644                  * Sets a loader for all view nodes tied to this view instance.
645                  */
646                 setLoader: function() {
647                         this.setContent(
648                                 '<div class="loading-placeholder">' +
649                                         '<div class="dashicons dashicons-admin-media"></div>' +
650                                         '<div class="wpview-loading"><ins></ins></div>' +
651                                 '</div>'
652                         );
653                 },
654
655                 /**
656                  * Sets an error for all view nodes tied to this view instance.
657                  *
658                  * @param {String} message  The error message to set.
659                  * @param {String} dashicon A dashicon ID. Optional. {@link https://developer.wordpress.org/resource/dashicons/}
660                  */
661                 setError: function( message, dashicon ) {
662                         this.setContent(
663                                 '<div class="wpview-error">' +
664                                         '<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
665                                         '<p>' + message + '</p>' +
666                                 '</div>'
667                         );
668                 },
669
670                 /**
671                  * Tries to find a text match in a given string.
672                  *
673                  * @param {String} content The string to scan.
674                  *
675                  * @return {Object}
676                  */
677                 match: function( content ) {
678                         var match = shortcode.next( this.type, content );
679
680                         if ( match ) {
681                                 return {
682                                         index: match.index,
683                                         content: match.content,
684                                         options: {
685                                                 shortcode: match.shortcode
686                                         }
687                                 };
688                         }
689                 },
690
691                 /**
692                  * Update the text of a given view node.
693                  *
694                  * @param {String}         text   The new text.
695                  * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
696                  * @param {HTMLElement}    node   The view node to update.
697                  * @param {Boolean}        force  Recreate the instance. Optional.
698                  */
699                 update: function( text, editor, node, force ) {
700                         _.find( views, function( view, type ) {
701                                 var match = view.prototype.match( text );
702
703                                 if ( match ) {
704                                         $( node ).data( 'rendered', false );
705                                         editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
706                                         wp.mce.views.createInstance( type, text, match.options, force ).render();
707                                         editor.focus();
708
709                                         return true;
710                                 }
711                         } );
712                 },
713
714                 /**
715                  * Remove a given view node from the DOM.
716                  *
717                  * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
718                  * @param {HTMLElement}    node   The view node to remove.
719                  */
720                 remove: function( editor, node ) {
721                         this.unbindNode.call( this, editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
722                         $( node ).trigger( 'wp-mce-view-unbind' );
723                         editor.dom.remove( node );
724                         editor.focus();
725                 }
726         } );
727 } )( window, window.wp, window.wp.shortcode, window.jQuery );
728
729 /*
730  * The WordPress core TinyMCE views.
731  * Views for the gallery, audio, video, playlist and embed shortcodes,
732  * and a view for embeddable URLs.
733  */
734 ( function( window, views, media, $ ) {
735         var base, gallery, av, embed,
736                 schema, parser, serializer;
737
738         function verifyHTML( string ) {
739                 var settings = {};
740
741                 if ( ! window.tinymce ) {
742                         return string.replace( /<[^>]+>/g, '' );
743                 }
744
745                 if ( ! string || ( string.indexOf( '<' ) === -1 && string.indexOf( '>' ) === -1 ) ) {
746                         return string;
747                 }
748
749                 schema = schema || new window.tinymce.html.Schema( settings );
750                 parser = parser || new window.tinymce.html.DomParser( settings, schema );
751                 serializer = serializer || new window.tinymce.html.Serializer( settings, schema );
752
753                 return serializer.serialize( parser.parse( string, { forced_root_block: false } ) );
754         }
755
756         base = {
757                 state: [],
758
759                 edit: function( text, update ) {
760                         var type = this.type,
761                                 frame = media[ type ].edit( text );
762
763                         this.pausePlayers && this.pausePlayers();
764
765                         _.each( this.state, function( state ) {
766                                 frame.state( state ).on( 'update', function( selection ) {
767                                         update( media[ type ].shortcode( selection ).string(), type === 'gallery' );
768                                 } );
769                         } );
770
771                         frame.on( 'close', function() {
772                                 frame.detach();
773                         } );
774
775                         frame.open();
776                 }
777         };
778
779         gallery = _.extend( {}, base, {
780                 state: [ 'gallery-edit' ],
781                 template: media.template( 'editor-gallery' ),
782
783                 initialize: function() {
784                         var attachments = media.gallery.attachments( this.shortcode, media.view.settings.post.id ),
785                                 attrs = this.shortcode.attrs.named,
786                                 self = this;
787
788                         attachments.more()
789                         .done( function() {
790                                 attachments = attachments.toJSON();
791
792                                 _.each( attachments, function( attachment ) {
793                                         if ( attachment.sizes ) {
794                                                 if ( attrs.size && attachment.sizes[ attrs.size ] ) {
795                                                         attachment.thumbnail = attachment.sizes[ attrs.size ];
796                                                 } else if ( attachment.sizes.thumbnail ) {
797                                                         attachment.thumbnail = attachment.sizes.thumbnail;
798                                                 } else if ( attachment.sizes.full ) {
799                                                         attachment.thumbnail = attachment.sizes.full;
800                                                 }
801                                         }
802                                 } );
803
804                                 self.render( self.template( {
805                                         verifyHTML: verifyHTML,
806                                         attachments: attachments,
807                                         columns: attrs.columns ? parseInt( attrs.columns, 10 ) : media.galleryDefaults.columns
808                                 } ) );
809                         } )
810                         .fail( function( jqXHR, textStatus ) {
811                                 self.setError( textStatus );
812                         } );
813                 }
814         } );
815
816         av = _.extend( {}, base, {
817                 action: 'parse-media-shortcode',
818
819                 initialize: function() {
820                         var self = this;
821
822                         if ( this.url ) {
823                                 this.loader = false;
824                                 this.shortcode = media.embed.shortcode( {
825                                         url: this.text
826                                 } );
827                         }
828
829                         wp.ajax.post( this.action, {
830                                 post_ID: media.view.settings.post.id,
831                                 type: this.shortcode.tag,
832                                 shortcode: this.shortcode.string()
833                         } )
834                         .done( function( response ) {
835                                 self.render( response );
836                         } )
837                         .fail( function( response ) {
838                                 if ( self.url ) {
839                                         self.ignore = true;
840                                         self.removeMarkers();
841                                 } else {
842                                         self.setError( response.message || response.statusText, 'admin-media' );
843                                 }
844                         } );
845
846                         this.getEditors( function( editor ) {
847                                 editor.on( 'wpview-selected', function() {
848                                         self.pausePlayers();
849                                 } );
850                         } );
851                 },
852
853                 pausePlayers: function() {
854                         this.getNodes( function( editor, node, content ) {
855                                 var win = $( 'iframe.wpview-sandbox', content ).get( 0 );
856
857                                 if ( win && ( win = win.contentWindow ) && win.mejs ) {
858                                         _.each( win.mejs.players, function( player ) {
859                                                 try {
860                                                         player.pause();
861                                                 } catch ( e ) {}
862                                         } );
863                                 }
864                         } );
865                 }
866         } );
867
868         embed = _.extend( {}, av, {
869                 action: 'parse-embed',
870
871                 edit: function( text, update ) {
872                         var frame = media.embed.edit( text, this.url ),
873                                 self = this;
874
875                         this.pausePlayers();
876
877                         frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
878                                 if ( url && model.get( 'url' ) ) {
879                                         frame.state( 'embed' ).metadata = model.toJSON();
880                                 }
881                         } );
882
883                         frame.state( 'embed' ).on( 'select', function() {
884                                 var data = frame.state( 'embed' ).metadata;
885
886                                 if ( self.url ) {
887                                         update( data.url );
888                                 } else {
889                                         update( media.embed.shortcode( data ).string() );
890                                 }
891                         } );
892
893                         frame.on( 'close', function() {
894                                 frame.detach();
895                         } );
896
897                         frame.open();
898                 }
899         } );
900
901         views.register( 'gallery', _.extend( {}, gallery ) );
902
903         views.register( 'audio', _.extend( {}, av, {
904                 state: [ 'audio-details' ]
905         } ) );
906
907         views.register( 'video', _.extend( {}, av, {
908                 state: [ 'video-details' ]
909         } ) );
910
911         views.register( 'playlist', _.extend( {}, av, {
912                 state: [ 'playlist-edit', 'video-playlist-edit' ]
913         } ) );
914
915         views.register( 'embed', _.extend( {}, embed ) );
916
917         views.register( 'embedURL', _.extend( {}, embed, {
918                 match: function( content ) {
919                         var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi,
920                                 match = re.exec( content );
921
922                         if ( match ) {
923                                 return {
924                                         index: match.index + match[1].length,
925                                         content: match[2],
926                                         options: {
927                                                 url: true
928                                         }
929                                 };
930                         }
931                 }
932         } ) );
933 } )( window, window.wp.mce.views, window.wp.media, window.jQuery );