]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/mce-view.js
WordPress 4.0
[autoinstalls/wordpress.git] / wp-includes / js / mce-view.js
1 /* global tinymce */
2 /**
3  * Note: this API is "experimental" meaning that it will probably change
4  * in the next few releases based on feedback from 3.9.0.
5  * If you decide to use it, please follow the development closely.
6  */
7
8 // Ensure the global `wp` object exists.
9 window.wp = window.wp || {};
10
11 ( function( $ ) {
12         'use strict';
13
14         var views = {},
15                 instances = {},
16                 media = wp.media,
17                 viewOptions = ['encodedText'];
18
19         // Create the `wp.mce` object if necessary.
20         wp.mce = wp.mce || {};
21
22         /**
23          * wp.mce.View
24          *
25          * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
26          * that the TinyMCE View is not tied to a particular DOM node.
27          *
28          * @param {Object} [options={}]
29          */
30         wp.mce.View = function( options ) {
31                 options = options || {};
32                 this.type = options.type;
33                 _.extend( this, _.pick( options, viewOptions ) );
34                 this.initialize.apply( this, arguments );
35         };
36
37         _.extend( wp.mce.View.prototype, {
38                 initialize: function() {},
39                 getHtml: function() {
40                         return '';
41                 },
42                 loadingPlaceholder: function() {
43                         return '' +
44                                 '<div class="loading-placeholder">' +
45                                         '<div class="dashicons dashicons-admin-media"></div>' +
46                                         '<div class="wpview-loading"><ins></ins></div>' +
47                                 '</div>';
48                 },
49                 render: function( force ) {
50                         if ( force || ! this.rendered() ) {
51                                 this.unbind();
52
53                                 this.setContent(
54                                         '<p class="wpview-selection-before">\u00a0</p>' +
55                                         '<div class="wpview-body" contenteditable="false">' +
56                                                 '<div class="toolbar">' +
57                                                         ( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +
58                                                         '<div class="dashicons dashicons-no-alt remove"></div>' +
59                                                 '</div>' +
60                                                 '<div class="wpview-content wpview-type-' + this.type + '">' +
61                                                         ( this.getHtml() || this.loadingPlaceholder() ) +
62                                                 '</div>' +
63                                                 ( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) +
64                                         '</div>' +
65                                         '<p class="wpview-selection-after">\u00a0</p>',
66                                         'wrap'
67                                 );
68
69                                 $( this ).trigger( 'ready' );
70
71                                 this.rendered( true );
72                         }
73                 },
74                 unbind: function() {},
75                 getEditors: function( callback ) {
76                         var editors = [];
77
78                         _.each( tinymce.editors, function( editor ) {
79                                 if ( editor.plugins.wpview ) {
80                                         if ( callback ) {
81                                                 callback( editor );
82                                         }
83
84                                         editors.push( editor );
85                                 }
86                         }, this );
87
88                         return editors;
89                 },
90                 getNodes: function( callback ) {
91                         var nodes = [],
92                                 self = this;
93
94                         this.getEditors( function( editor ) {
95                                 $( editor.getBody() )
96                                 .find( '[data-wpview-text="' + self.encodedText + '"]' )
97                                 .each( function ( i, node ) {
98                                         if ( callback ) {
99                                                 callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
100                                         }
101
102                                         nodes.push( node );
103                                 } );
104                         } );
105
106                         return nodes;
107                 },
108                 setContent: function( html, option ) {
109                         this.getNodes( function ( editor, node, content ) {
110                                 var el = ( option === 'wrap' || option === 'replace' ) ? node : content,
111                                         insert = html;
112
113                                 if ( _.isString( insert ) ) {
114                                         insert = editor.dom.createFragment( insert );
115                                 }
116
117                                 if ( option === 'replace' ) {
118                                         editor.dom.replace( insert, el );
119                                 } else {
120                                         el.innerHTML = '';
121                                         el.appendChild( insert );
122                                 }
123                         } );
124                 },
125                 /* jshint scripturl: true */
126                 setIframes: function ( head, body ) {
127                         var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
128                                 importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';
129
130                         if ( head || body.indexOf( '<script' ) !== -1 ) {
131                                 this.getNodes( function ( editor, node, content ) {
132                                         var dom = editor.dom,
133                                                 styles = '',
134                                                 bodyClasses = editor.getBody().className || '',
135                                                 iframe, iframeDoc, i, resize;
136
137                                         content.innerHTML = '';
138                                         head = head || '';
139
140                                         if ( importStyles ) {
141                                                 if ( ! wp.mce.views.sandboxStyles ) {
142                                                         tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {
143                                                                 if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
144                                                                         link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
145
146                                                                         styles += dom.getOuterHTML( link ) + '\n';
147                                                                 }
148                                                         });
149
150                                                         wp.mce.views.sandboxStyles = styles;
151                                                 } else {
152                                                         styles = wp.mce.views.sandboxStyles;
153                                                 }
154                                         }
155
156                                         // Seems Firefox needs a bit of time to insert/set the view nodes, or the iframe will fail
157                                         // especially when switching Text => Visual.
158                                         setTimeout( function() {
159                                                 iframe = dom.add( content, 'iframe', {
160                                                         src: tinymce.Env.ie ? 'javascript:""' : '',
161                                                         frameBorder: '0',
162                                                         allowTransparency: 'true',
163                                                         scrolling: 'no',
164                                                         'class': 'wpview-sandbox',
165                                                         style: {
166                                                                 width: '100%',
167                                                                 display: 'block'
168                                                         }
169                                                 } );
170
171                                                 iframeDoc = iframe.contentWindow.document;
172
173                                                 iframeDoc.open();
174                                                 iframeDoc.write(
175                                                         '<!DOCTYPE html>' +
176                                                         '<html>' +
177                                                                 '<head>' +
178                                                                         '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
179                                                                         head +
180                                                                         styles +
181                                                                         '<style>' +
182                                                                                 'html {' +
183                                                                                         'background: transparent;' +
184                                                                                         'padding: 0;' +
185                                                                                         'margin: 0;' +
186                                                                                 '}' +
187                                                                                 'body#wpview-iframe-sandbox {' +
188                                                                                         'background: transparent;' +
189                                                                                         'padding: 1px 0 !important;' +
190                                                                                         'margin: -1px 0 0 !important;' +
191                                                                                 '}' +
192                                                                                 'body#wpview-iframe-sandbox:before,' +
193                                                                                 'body#wpview-iframe-sandbox:after {' +
194                                                                                         'display: none;' +
195                                                                                         'content: "";' +
196                                                                                 '}' +
197                                                                         '</style>' +
198                                                                 '</head>' +
199                                                                 '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
200                                                                         body +
201                                                                 '</body>' +
202                                                         '</html>'
203                                                 );
204                                                 iframeDoc.close();
205
206                                                 resize = function() {
207                                                         // Make sure the iframe still exists.
208                                                         iframe.contentWindow && $( iframe ).height( $( iframeDoc.body ).height() );
209                                                 };
210
211                                                 if ( MutationObserver ) {
212                                                         new MutationObserver( _.debounce( function() {
213                                                                 resize();
214                                                         }, 100 ) )
215                                                         .observe( iframeDoc.body, {
216                                                                 attributes: true,
217                                                                 childList: true,
218                                                                 subtree: true
219                                                         } );
220                                                 } else {
221                                                         for ( i = 1; i < 6; i++ ) {
222                                                                 setTimeout( resize, i * 700 );
223                                                         }
224                                                 }
225
226                                                 if ( importStyles ) {
227                                                         editor.on( 'wp-body-class-change', function() {
228                                                                 iframeDoc.body.className = editor.getBody().className;
229                                                         });
230                                                 }
231                                         }, 50 );
232                                 });
233                         } else {
234                                 this.setContent( body );
235                         }
236                 },
237                 setError: function( message, dashicon ) {
238                         this.setContent(
239                                 '<div class="wpview-error">' +
240                                         '<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +
241                                         '<p>' + message + '</p>' +
242                                 '</div>'
243                         );
244                 },
245                 rendered: function( value ) {
246                         var notRendered;
247
248                         this.getNodes( function( editor, node ) {
249                                 if ( value != null ) {
250                                         $( node ).data( 'rendered', value === true );
251                                 } else {
252                                         notRendered = notRendered || ! $( node ).data( 'rendered' );
253                                 }
254                         } );
255
256                         return ! notRendered;
257                 }
258         } );
259
260         // take advantage of the Backbone extend method
261         wp.mce.View.extend = Backbone.View.extend;
262
263         /**
264          * wp.mce.views
265          *
266          * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
267          * At its core, it serves as a series of converters, transforming text to a
268          * custom UI, and back again.
269          */
270         wp.mce.views = {
271
272                 /**
273                  * wp.mce.views.register( type, view )
274                  *
275                  * Registers a new TinyMCE view.
276                  *
277                  * @param type
278                  * @param constructor
279                  *
280                  */
281                 register: function( type, constructor ) {
282                         var defaultConstructor = {
283                                         type: type,
284                                         View: {},
285                                         toView: function( content ) {
286                                                 var match = wp.shortcode.next( this.type, content );
287
288                                                 if ( ! match ) {
289                                                         return;
290                                                 }
291
292                                                 return {
293                                                         index: match.index,
294                                                         content: match.content,
295                                                         options: {
296                                                                 shortcode: match.shortcode
297                                                         }
298                                                 };
299                                         }
300                                 };
301
302                         constructor = _.defaults( constructor, defaultConstructor );
303                         constructor.View = wp.mce.View.extend( constructor.View );
304
305                         views[ type ] = constructor;
306                 },
307
308                 /**
309                  * wp.mce.views.get( id )
310                  *
311                  * Returns a TinyMCE view constructor.
312                  *
313                  * @param type
314                  */
315                 get: function( type ) {
316                         return views[ type ];
317                 },
318
319                 /**
320                  * wp.mce.views.unregister( type )
321                  *
322                  * Unregisters a TinyMCE view.
323                  *
324                  * @param type
325                  */
326                 unregister: function( type ) {
327                         delete views[ type ];
328                 },
329
330                 /**
331                  * wp.mce.views.unbind( editor )
332                  *
333                  * The editor DOM is being rebuilt, run cleanup.
334                  */
335                 unbind: function() {
336                         _.each( instances, function( instance ) {
337                                 instance.unbind();
338                         } );
339                 },
340
341                 /**
342                  * toViews( content )
343                  * Scans a `content` string for each view's pattern, replacing any
344                  * matches with wrapper elements, and creates a new instance for
345                  * every match, which triggers the related data to be fetched.
346                  *
347                  * @param content
348                  */
349                 toViews: function( content ) {
350                         var pieces = [ { content: content } ],
351                                 current;
352
353                         _.each( views, function( view, viewType ) {
354                                 current = pieces.slice();
355                                 pieces  = [];
356
357                                 _.each( current, function( piece ) {
358                                         var remaining = piece.content,
359                                                 result;
360
361                                         // Ignore processed pieces, but retain their location.
362                                         if ( piece.processed ) {
363                                                 pieces.push( piece );
364                                                 return;
365                                         }
366
367                                         // Iterate through the string progressively matching views
368                                         // and slicing the string as we go.
369                                         while ( remaining && (result = view.toView( remaining )) ) {
370                                                 // Any text before the match becomes an unprocessed piece.
371                                                 if ( result.index ) {
372                                                         pieces.push({ content: remaining.substring( 0, result.index ) });
373                                                 }
374
375                                                 // Add the processed piece for the match.
376                                                 pieces.push({
377                                                         content: wp.mce.views.toView( viewType, result.content, result.options ),
378                                                         processed: true
379                                                 });
380
381                                                 // Update the remaining content.
382                                                 remaining = remaining.slice( result.index + result.content.length );
383                                         }
384
385                                         // There are no additional matches. If any content remains,
386                                         // add it as an unprocessed piece.
387                                         if ( remaining ) {
388                                                 pieces.push({ content: remaining });
389                                         }
390                                 });
391                         });
392
393                         return _.pluck( pieces, 'content' ).join('');
394                 },
395
396                 /**
397                  * Create a placeholder for a particular view type
398                  *
399                  * @param viewType
400                  * @param text
401                  * @param options
402                  *
403                  */
404                 toView: function( viewType, text, options ) {
405                         var view = wp.mce.views.get( viewType ),
406                                 encodedText = window.encodeURIComponent( text ),
407                                 instance, viewOptions;
408
409
410                         if ( ! view ) {
411                                 return text;
412                         }
413
414                         if ( ! wp.mce.views.getInstance( encodedText ) ) {
415                                 viewOptions = options;
416                                 viewOptions.type = viewType;
417                                 viewOptions.encodedText = encodedText;
418                                 instance = new view.View( viewOptions );
419                                 instances[ encodedText ] = instance;
420                         }
421
422                         return wp.html.string({
423                                 tag: 'div',
424
425                                 attrs: {
426                                         'class': 'wpview-wrap',
427                                         'data-wpview-text': encodedText,
428                                         'data-wpview-type': viewType
429                                 },
430
431                                 content: '\u00a0'
432                         });
433                 },
434
435                 /**
436                  * Refresh views after an update is made
437                  *
438                  * @param view {object} being refreshed
439                  * @param text {string} textual representation of the view
440                  */
441                 refreshView: function( view, text ) {
442                         var encodedText = window.encodeURIComponent( text ),
443                                 viewOptions,
444                                 result, instance;
445
446                         instance = wp.mce.views.getInstance( encodedText );
447
448                         if ( ! instance ) {
449                                 result = view.toView( text );
450                                 viewOptions = result.options;
451                                 viewOptions.type = view.type;
452                                 viewOptions.encodedText = encodedText;
453                                 instance = new view.View( viewOptions );
454                                 instances[ encodedText ] = instance;
455                         }
456
457                         instance.render();
458                 },
459
460                 getInstance: function( encodedText ) {
461                         return instances[ encodedText ];
462                 },
463
464                 /**
465                  * render( scope )
466                  *
467                  * Renders any view instances inside a DOM node `scope`.
468                  *
469                  * View instances are detected by the presence of wrapper elements.
470                  * To generate wrapper elements, pass your content through
471                  * `wp.mce.view.toViews( content )`.
472                  */
473                 render: function( force ) {
474                         _.each( instances, function( instance ) {
475                                 instance.render( force );
476                         } );
477                 },
478
479                 edit: function( node ) {
480                         var viewType = $( node ).data('wpview-type'),
481                                 view = wp.mce.views.get( viewType );
482
483                         if ( view ) {
484                                 view.edit( node );
485                         }
486                 }
487         };
488
489         wp.mce.views.register( 'gallery', {
490                 View: {
491                         template: media.template( 'editor-gallery' ),
492
493                         // The fallback post ID to use as a parent for galleries that don't
494                         // specify the `ids` or `include` parameters.
495                         //
496                         // Uses the hidden input on the edit posts page by default.
497                         postID: $('#post_ID').val(),
498
499                         initialize: function( options ) {
500                                 this.shortcode = options.shortcode;
501                                 this.fetch();
502                         },
503
504                         fetch: function() {
505                                 var self = this;
506
507                                 this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
508                                 this.dfd = this.attachments.more().done( function() {
509                                         self.render( true );
510                                 } );
511                         },
512
513                         getHtml: function() {
514                                 var attrs = this.shortcode.attrs.named,
515                                         attachments = false,
516                                         options;
517
518                                 // Don't render errors while still fetching attachments
519                                 if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
520                                         return '';
521                                 }
522
523                                 if ( this.attachments.length ) {
524                                         attachments = this.attachments.toJSON();
525
526                                         _.each( attachments, function( attachment ) {
527                                                 if ( attachment.sizes ) {
528                                                         if ( attachment.sizes.thumbnail ) {
529                                                                 attachment.thumbnail = attachment.sizes.thumbnail;
530                                                         } else if ( attachment.sizes.full ) {
531                                                                 attachment.thumbnail = attachment.sizes.full;
532                                                         }
533                                                 }
534                                         } );
535                                 }
536
537                                 options = {
538                                         attachments: attachments,
539                                         columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
540                                 };
541
542                                 return this.template( options );
543                         }
544                 },
545
546                 edit: function( node ) {
547                         var gallery = wp.media.gallery,
548                                 self = this,
549                                 frame, data;
550
551                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
552                         frame = gallery.edit( data );
553
554                         frame.state('gallery-edit').on( 'update', function( selection ) {
555                                 var shortcode = gallery.shortcode( selection ).string();
556                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
557                                 wp.mce.views.refreshView( self, shortcode );
558                         });
559
560                         frame.on( 'close', function() {
561                                 frame.detach();
562                         });
563                 }
564         } );
565
566         /**
567          * These are base methods that are shared by the audio and video shortcode's MCE controller.
568          *
569          * @mixin
570          */
571         wp.mce.av = {
572                 View: {
573                         overlay: true,
574
575                         action: 'parse-media-shortcode',
576
577                         initialize: function( options ) {
578                                 var self = this;
579
580                                 this.shortcode = options.shortcode;
581
582                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
583                                 $( this ).on( 'ready', this.setNodes );
584
585                                 $( document ).on( 'media:edit', this.stopPlayers );
586
587                                 this.fetch();
588
589                                 this.getEditors( function( editor ) {
590                                         editor.on( 'hide', self.stopPlayers );
591                                 });
592                         },
593
594                         setNodes: function () {
595                                 if ( this.parsed ) {
596                                         this.setIframes( this.parsed.head, this.parsed.body );
597                                 } else {
598                                         this.fail();
599                                 }
600                         },
601
602                         fetch: function () {
603                                 var self = this;
604
605                                 wp.ajax.send( this.action, {
606                                         data: {
607                                                 post_ID: $( '#post_ID' ).val() || 0,
608                                                 type: this.shortcode.tag,
609                                                 shortcode: this.shortcode.string()
610                                         }
611                                 } )
612                                 .done( function( response ) {
613                                         if ( response ) {
614                                                 self.parsed = response;
615                                                 self.setIframes( response.head, response.body );
616                                         } else {
617                                                 self.fail( true );
618                                         }
619                                 } )
620                                 .fail( function( response ) {
621                                         self.fail( response || true );
622                                 } );
623                         },
624
625                         fail: function( error ) {
626                                 if ( ! this.error ) {
627                                         if ( error ) {
628                                                 this.error = error;
629                                         } else {
630                                                 return;
631                                         }
632                                 }
633
634                                 if ( this.error.message ) {
635                                         if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' ||
636                                                 this.error.type === 'no-items' ) {
637
638                                                 this.setError( this.error.message, 'admin-media' );
639                                         } else {
640                                                 this.setContent( '<p>' + this.original + '</p>', 'replace' );
641                                         }
642                                 } else if ( this.error.statusText ) {
643                                         this.setError( this.error.statusText, 'admin-media' );
644                                 } else if ( this.original ) {
645                                         this.setContent( '<p>' + this.original + '</p>', 'replace' );
646                                 }
647                         },
648
649                         stopPlayers: function( remove ) {
650                                 var rem = remove === 'remove';
651
652                                 this.getNodes( function( editor, node, content ) {
653                                         var p, win,
654                                                 iframe = $( 'iframe.wpview-sandbox', content ).get(0);
655
656                                         if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) {
657                                                 // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there.
658                                                 try {
659                                                         for ( p in win.mejs.players ) {
660                                                                 win.mejs.players[p].pause();
661
662                                                                 if ( rem ) {
663                                                                         win.mejs.players[p].remove();
664                                                                 }
665                                                         }
666                                                 } catch( er ) {}
667                                         }
668                                 });
669                         },
670
671                         unbind: function() {
672                                 this.stopPlayers( 'remove' );
673                         }
674                 },
675
676                 /**
677                  * Called when a TinyMCE view is clicked for editing.
678                  * - Parses the shortcode out of the element's data attribute
679                  * - Calls the `edit` method on the shortcode model
680                  * - Launches the model window
681                  * - Bind's an `update` callback which updates the element's data attribute
682                  *   re-renders the view
683                  *
684                  * @param {HTMLElement} node
685                  */
686                 edit: function( node ) {
687                         var media = wp.media[ this.type ],
688                                 self = this,
689                                 frame, data, callback;
690
691                         $( document ).trigger( 'media:edit' );
692
693                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
694                         frame = media.edit( data );
695                         frame.on( 'close', function() {
696                                 frame.detach();
697                         } );
698
699                         callback = function( selection ) {
700                                 var shortcode = wp.media[ self.type ].shortcode( selection ).string();
701                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
702                                 wp.mce.views.refreshView( self, shortcode );
703                                 frame.detach();
704                         };
705                         if ( _.isArray( self.state ) ) {
706                                 _.each( self.state, function (state) {
707                                         frame.state( state ).on( 'update', callback );
708                                 } );
709                         } else {
710                                 frame.state( self.state ).on( 'update', callback );
711                         }
712                         frame.open();
713                 }
714         };
715
716         /**
717          * TinyMCE handler for the video shortcode
718          *
719          * @mixes wp.mce.av
720          */
721         wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
722                 state: 'video-details'
723         } ) );
724
725         /**
726          * TinyMCE handler for the audio shortcode
727          *
728          * @mixes wp.mce.av
729          */
730         wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
731                 state: 'audio-details'
732         } ) );
733
734         /**
735          * TinyMCE handler for the playlist shortcode
736          *
737          * @mixes wp.mce.av
738          */
739         wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
740                 state: [ 'playlist-edit', 'video-playlist-edit' ]
741         } ) );
742
743         /**
744          * TinyMCE handler for the embed shortcode
745          */
746         wp.mce.embedMixin = {
747                 View: _.extend( {}, wp.mce.av.View, {
748                         overlay: true,
749                         action: 'parse-embed',
750                         initialize: function( options ) {
751                                 this.content = options.content;
752                                 this.original = options.url || options.shortcode.string();
753
754                                 if ( options.url ) {
755                                         this.shortcode = media.embed.shortcode( {
756                                                 url: options.url
757                                         } );
758                                 } else {
759                                         this.shortcode = options.shortcode;
760                                 }
761
762                                 _.bindAll( this, 'setIframes', 'setNodes', 'fetch' );
763                                 $( this ).on( 'ready', this.setNodes );
764
765                                 this.fetch();
766                         }
767                 } ),
768                 edit: function( node ) {
769                         var embed = media.embed,
770                                 self = this,
771                                 frame,
772                                 data,
773                                 isURL = 'embedURL' === this.type;
774
775                         $( document ).trigger( 'media:edit' );
776
777                         data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
778                         frame = embed.edit( data, isURL );
779                         frame.on( 'close', function() {
780                                 frame.detach();
781                         } );
782                         frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
783                                 if ( ! url ) {
784                                         return;
785                                 }
786                                 frame.state( 'embed' ).metadata = model.toJSON();
787                         } );
788                         frame.state( 'embed' ).on( 'select', function() {
789                                 var shortcode;
790
791                                 if ( isURL ) {
792                                         shortcode = frame.state( 'embed' ).metadata.url;
793                                 } else {
794                                         shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
795                                 }
796                                 $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
797                                 wp.mce.views.refreshView( self, shortcode );
798                                 frame.detach();
799                         } );
800                         frame.open();
801                 }
802         };
803
804         wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
805
806         wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
807                 toView: function( content ) {
808                         var re = /(?:^|<p>)(https?:\/\/[^\s"]+?)(?:<\/p>\s*|$)/gi,
809                                 match = re.exec( tinymce.trim( content ) );
810
811                         if ( ! match ) {
812                                 return;
813                         }
814
815                         return {
816                                 index: match.index,
817                                 content: match[0],
818                                 options: {
819                                         url: match[1]
820                                 }
821                         };
822                 }
823         } ) );
824
825 }(jQuery));