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