]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-includes/js/mce-view.js
WordPress 4.1.4-scripts
[autoinstalls/wordpress.git] / wp-includes / js / mce-view.js
index 912c4c7ce4c161d795391a8ae30222abcc068da1..da1ce6aed2c4c99f1bad6c59caca548a8eec9fd3 100644 (file)
+/* global tinymce */
+/**
+ * Note: this API is "experimental" meaning that it will probably change
+ * in the next few releases based on feedback from 3.9.0.
+ * If you decide to use it, please follow the development closely.
+ */
+
 // Ensure the global `wp` object exists.
 window.wp = window.wp || {};
 
-(function($){
+( function( $ ) {
+       'use strict';
+
        var views = {},
-               instances = {};
+               instances = {},
+               media = wp.media,
+               mediaWindows = [],
+               windowIdx = 0,
+               waitInterval = 50,
+               viewOptions = ['encodedText'];
 
        // Create the `wp.mce` object if necessary.
        wp.mce = wp.mce || {};
 
-       // wp.mce.view
-       // -----------
-       // A set of utilities that simplifies adding custom UI within a TinyMCE editor.
-       // At its core, it serves as a series of converters, transforming text to a
-       // custom UI, and back again.
-       wp.mce.view = {
-               // ### defaults
-               defaults: {
-                       // The default properties used for objects with the `pattern` key in
-                       // `wp.mce.view.add()`.
-                       pattern: {
-                               view: Backbone.View,
-                               text: function( instance ) {
-                                       return instance.options.original;
-                               },
-
-                               toView: function( content ) {
-                                       if ( ! this.pattern )
-                                               return;
-
-                                       this.pattern.lastIndex = 0;
-                                       var match = this.pattern.exec( content );
+       /**
+        * wp.mce.View
+        *
+        * A Backbone-like View constructor intended for use when rendering a TinyMCE View. The main difference is
+        * that the TinyMCE View is not tied to a particular DOM node.
+        *
+        * @param {Object} [options={}]
+        */
+       wp.mce.View = function( options ) {
+               options = options || {};
+               this.type = options.type;
+               _.extend( this, _.pick( options, viewOptions ) );
+               this.initialize.apply( this, arguments );
+       };
 
-                                       if ( ! match )
-                                               return;
+       _.extend( wp.mce.View.prototype, {
+               initialize: function() {},
+               getHtml: function() {
+                       return '';
+               },
+               loadingPlaceholder: function() {
+                       return '' +
+                               '<div class="loading-placeholder">' +
+                                       '<div class="dashicons dashicons-admin-media"></div>' +
+                                       '<div class="wpview-loading"><ins></ins></div>' +
+                               '</div>';
+               },
+               render: function( force ) {
+                       if ( force || ! this.rendered() ) {
+                               this.unbind();
+
+                               this.setContent(
+                                       '<p class="wpview-selection-before">\u00a0</p>' +
+                                       '<div class="wpview-body" contenteditable="false">' +
+                                               '<div class="toolbar mce-arrow-down">' +
+                                                       ( _.isFunction( views[ this.type ].edit ) ? '<div class="dashicons dashicons-edit edit"></div>' : '' ) +
+                                                       '<div class="dashicons dashicons-no remove"></div>' +
+                                               '</div>' +
+                                               '<div class="wpview-content wpview-type-' + this.type + '">' +
+                                                       ( this.getHtml() || this.loadingPlaceholder() ) +
+                                               '</div>' +
+                                               ( this.overlay ? '<div class="wpview-overlay"></div>' : '' ) +
+                                       '</div>' +
+                                       '<p class="wpview-selection-after">\u00a0</p>',
+                                       'wrap'
+                               );
+
+                               $( this ).trigger( 'ready' );
+
+                               this.rendered( true );
+                       }
+               },
+               unbind: function() {},
+               getEditors: function( callback ) {
+                       var editors = [];
+
+                       _.each( tinymce.editors, function( editor ) {
+                               if ( editor.plugins.wpview ) {
+                                       if ( callback ) {
+                                               callback( editor );
+                                       }
 
-                                       return {
-                                               index:   match.index,
-                                               content: match[0],
-                                               options: {
-                                                       original: match[0],
-                                                       results:  match
-                                               }
-                                       };
+                                       editors.push( editor );
                                }
-                       },
+                       }, this );
 
-                       // The default properties used for objects with the `shortcode` key in
-                       // `wp.mce.view.add()`.
-                       shortcode: {
-                               view: Backbone.View,
-                               text: function( instance ) {
-                                       return instance.options.shortcode.string();
-                               },
+                       return editors;
+               },
+               getNodes: function( callback ) {
+                       var nodes = [],
+                               self = this;
+
+                       this.getEditors( function( editor ) {
+                               $( editor.getBody() )
+                               .find( '[data-wpview-text="' + self.encodedText + '"]' )
+                               .each( function ( i, node ) {
+                                       if ( callback ) {
+                                               callback( editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
+                                       }
 
-                               toView: function( content ) {
-                                       var match = wp.shortcode.next( this.shortcode, content );
+                                       nodes.push( node );
+                               } );
+                       } );
 
-                                       if ( ! match )
-                                               return;
+                       return nodes;
+               },
+               setContent: function( html, option ) {
+                       this.getNodes( function ( editor, node, content ) {
+                               var el = ( option === 'wrap' || option === 'replace' ) ? node : content,
+                                       insert = html;
 
-                                       return {
-                                               index:   match.index,
-                                               content: match.content,
-                                               options: {
-                                                       shortcode: match.shortcode
-                                               }
-                                       };
+                               if ( _.isString( insert ) ) {
+                                       insert = editor.dom.createFragment( insert );
                                }
-                       }
-               },
 
-               // ### add( id, options )
-               // Registers a new TinyMCE view.
-               //
-               // Accepts a unique `id` and an `options` object.
-               //
-               // `options` accepts the following properties:
-               //
-               // * `pattern` is the regular expression used to scan the content and
-               // detect matching views.
-               //
-               // * `view` is a `Backbone.View` constructor. If a plain object is
-               // provided, it will automatically extend the parent constructor
-               // (usually `Backbone.View`). Views are instantiated when the `pattern`
-               // is successfully matched. The instance's `options` object is provided
-               // with the `original` matched value, the match `results` including
-               // capture groups, and the `viewType`, which is the constructor's `id`.
-               //
-               // * `extend` an existing view by passing in its `id`. The current
-               // view will inherit all properties from the parent view, and if
-               // `view` is set to a plain object, it will extend the parent `view`
-               // constructor.
-               //
-               // * `text` is a method that accepts an instance of the `view`
-               // constructor and transforms it into a text representation.
-               add: function( id, options ) {
-                       var parent, remove, base, properties;
-
-                       // Fetch the parent view or the default options.
-                       if ( options.extend )
-                               parent = wp.mce.view.get( options.extend );
-                       else if ( options.shortcode )
-                               parent = wp.mce.view.defaults.shortcode;
-                       else
-                               parent = wp.mce.view.defaults.pattern;
-
-                       // Extend the `options` object with the parent's properties.
-                       _.defaults( options, parent );
-                       options.id = id;
-
-                       // Create properties used to enhance the view for use in TinyMCE.
-                       properties = {
-                               // Ensure the wrapper element and references to the view are
-                               // removed. Otherwise, removed views could randomly restore.
-                               remove: function() {
-                                       delete instances[ this.el.id ];
-                                       this.$el.parent().remove();
-
-                                       // Trigger the inherited `remove` method.
-                                       if ( remove )
-                                               remove.apply( this, arguments );
-
-                                       return this;
+                               if ( option === 'replace' ) {
+                                       editor.dom.replace( insert, el );
+                               } else {
+                                       el.innerHTML = '';
+                                       el.appendChild( insert );
                                }
-                       };
+                       } );
+               },
+               /* jshint scripturl: true */
+               setIframes: function ( head, body ) {
+                       var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
+                               importStyles = this.type === 'video' || this.type === 'audio' || this.type === 'playlist';
+
+                       if ( head || body.indexOf( '<script' ) !== -1 ) {
+                               this.getNodes( function ( editor, node, content ) {
+                                       var dom = editor.dom,
+                                               styles = '',
+                                               bodyClasses = editor.getBody().className || '',
+                                               iframe, iframeDoc, i, resize;
+
+                                       content.innerHTML = '';
+                                       head = head || '';
+
+                                       if ( importStyles ) {
+                                               if ( ! wp.mce.views.sandboxStyles ) {
+                                                       tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) {
+                                                               if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 &&
+                                                                       link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) {
+
+                                                                       styles += dom.getOuterHTML( link ) + '\n';
+                                                               }
+                                                       });
+
+                                                       wp.mce.views.sandboxStyles = styles;
+                                               } else {
+                                                       styles = wp.mce.views.sandboxStyles;
+                                               }
+                                       }
 
-                       // If the `view` provided was an object, use the parent's
-                       // `view` constructor as a base. If a `view` constructor
-                       // was provided, treat that as the base.
-                       if ( _.isFunction( options.view ) ) {
-                               base = options.view;
+                                       // Seems Firefox needs a bit of time to insert/set the view nodes, or the iframe will fail
+                                       // especially when switching Text => Visual.
+                                       setTimeout( function() {
+                                               iframe = dom.add( content, 'iframe', {
+                                                       src: tinymce.Env.ie ? 'javascript:""' : '',
+                                                       frameBorder: '0',
+                                                       allowTransparency: 'true',
+                                                       scrolling: 'no',
+                                                       'class': 'wpview-sandbox',
+                                                       style: {
+                                                               width: '100%',
+                                                               display: 'block'
+                                                       }
+                                               } );
+
+                                               iframeDoc = iframe.contentWindow.document;
+
+                                               iframeDoc.open();
+                                               iframeDoc.write(
+                                                       '<!DOCTYPE html>' +
+                                                       '<html>' +
+                                                               '<head>' +
+                                                                       '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
+                                                                       head +
+                                                                       styles +
+                                                                       '<style>' +
+                                                                               'html {' +
+                                                                                       'background: transparent;' +
+                                                                                       'padding: 0;' +
+                                                                                       'margin: 0;' +
+                                                                               '}' +
+                                                                               'body#wpview-iframe-sandbox {' +
+                                                                                       'background: transparent;' +
+                                                                                       'padding: 1px 0 !important;' +
+                                                                                       'margin: -1px 0 0 !important;' +
+                                                                               '}' +
+                                                                               'body#wpview-iframe-sandbox:before,' +
+                                                                               'body#wpview-iframe-sandbox:after {' +
+                                                                                       'display: none;' +
+                                                                                       'content: "";' +
+                                                                               '}' +
+                                                                       '</style>' +
+                                                               '</head>' +
+                                                               '<body id="wpview-iframe-sandbox" class="' + bodyClasses + '">' +
+                                                                       body +
+                                                               '</body>' +
+                                                       '</html>'
+                                               );
+                                               iframeDoc.close();
+
+                                               resize = function() {
+                                                       // Make sure the iframe still exists.
+                                                       iframe.contentWindow && $( iframe ).height( $( iframeDoc.body ).height() );
+                                               };
+
+                                               if ( MutationObserver ) {
+                                                       new MutationObserver( _.debounce( function() {
+                                                               resize();
+                                                       }, 100 ) )
+                                                       .observe( iframeDoc.body, {
+                                                               attributes: true,
+                                                               childList: true,
+                                                               subtree: true
+                                                       } );
+                                               } else {
+                                                       for ( i = 1; i < 6; i++ ) {
+                                                               setTimeout( resize, i * 700 );
+                                                       }
+                                               }
+
+                                               if ( importStyles ) {
+                                                       editor.on( 'wp-body-class-change', function() {
+                                                               iframeDoc.body.className = editor.getBody().className;
+                                                       });
+                                               }
+                                       }, waitInterval );
+                               });
                        } else {
-                               base   = parent.view;
-                               remove = options.view.remove;
-                               _.defaults( properties, options.view );
+                               this.setContent( body );
                        }
+               },
+               setError: function( message, dashicon ) {
+                       this.setContent(
+                               '<div class="wpview-error">' +
+                                       '<div class="dashicons dashicons-' + ( dashicon ? dashicon : 'no' ) + '"></div>' +
+                                       '<p>' + message + '</p>' +
+                               '</div>'
+                       );
+               },
+               rendered: function( value ) {
+                       var notRendered;
+
+                       this.getNodes( function( editor, node ) {
+                               if ( value != null ) {
+                                       $( node ).data( 'rendered', value === true );
+                               } else {
+                                       notRendered = notRendered || ! $( node ).data( 'rendered' );
+                               }
+                       } );
 
-                       // If there's a `remove` method on the `base` view that wasn't
-                       // created by this method, inherit it.
-                       if ( ! remove && ! base._mceview )
-                               remove = base.prototype.remove;
+                       return ! notRendered;
+               }
+       } );
+
+       // take advantage of the Backbone extend method
+       wp.mce.View.extend = Backbone.View.extend;
+
+       /**
+        * wp.mce.views
+        *
+        * A set of utilities that simplifies adding custom UI within a TinyMCE editor.
+        * At its core, it serves as a series of converters, transforming text to a
+        * custom UI, and back again.
+        */
+       wp.mce.views = {
+
+               /**
+                * wp.mce.views.register( type, view )
+                *
+                * Registers a new TinyMCE view.
+                *
+                * @param type
+                * @param constructor
+                *
+                */
+               register: function( type, constructor ) {
+                       var defaultConstructor = {
+                                       type: type,
+                                       View: {},
+                                       toView: function( content ) {
+                                               var match = wp.shortcode.next( this.type, content );
+
+                                               if ( ! match ) {
+                                                       return;
+                                               }
 
-                       // Automatically create the new `Backbone.View` constructor.
-                       options.view = base.extend( properties, {
-                               // Flag that the new view has been created by `wp.mce.view`.
-                               _mceview: true
-                       });
+                                               return {
+                                                       index: match.index,
+                                                       content: match.content,
+                                                       options: {
+                                                               shortcode: match.shortcode
+                                                       }
+                                               };
+                                       }
+                               };
+
+                       constructor = _.defaults( constructor, defaultConstructor );
+                       constructor.View = wp.mce.View.extend( constructor.View );
 
-                       views[ id ] = options;
+                       views[ type ] = constructor;
                },
 
-               // ### get( id )
-               // Returns a TinyMCE view options object.
-               get: function( id ) {
-                       return views[ id ];
+               /**
+                * wp.mce.views.get( id )
+                *
+                * Returns a TinyMCE view constructor.
+                *
+                * @param type
+                */
+               get: function( type ) {
+                       return views[ type ];
                },
 
-               // ### remove( id )
-               // Unregisters a TinyMCE view.
-               remove: function( id ) {
-                       delete views[ id ];
+               /**
+                * wp.mce.views.unregister( type )
+                *
+                * Unregisters a TinyMCE view.
+                *
+                * @param type
+                */
+               unregister: function( type ) {
+                       delete views[ type ];
                },
 
-               // ### toViews( content )
-               // Scans a `content` string for each view's pattern, replacing any
-               // matches with wrapper elements, and creates a new view instance for
-               // every match.
-               //
-               // To render the views, call `wp.mce.view.render( scope )`.
+               /**
+                * wp.mce.views.unbind( editor )
+                *
+                * The editor DOM is being rebuilt, run cleanup.
+                */
+               unbind: function() {
+                       _.each( instances, function( instance ) {
+                               instance.unbind();
+                       } );
+               },
+
+               /**
+                * toViews( content )
+                * Scans a `content` string for each view's pattern, replacing any
+                * matches with wrapper elements, and creates a new instance for
+                * every match, which triggers the related data to be fetched.
+                *
+                * @param content
+                */
                toViews: function( content ) {
                        var pieces = [ { content: content } ],
                                current;
@@ -190,12 +371,13 @@ window.wp = window.wp || {};
                                        // and slicing the string as we go.
                                        while ( remaining && (result = view.toView( remaining )) ) {
                                                // Any text before the match becomes an unprocessed piece.
-                                               if ( result.index )
+                                               if ( result.index ) {
                                                        pieces.push({ content: remaining.substring( 0, result.index ) });
+                                               }
 
                                                // Add the processed piece for the match.
                                                pieces.push({
-                                                       content:   wp.mce.view.toView( viewType, result.options ),
+                                                       content: wp.mce.views.toView( viewType, result.content, result.options ),
                                                        processed: true
                                                });
 
@@ -205,145 +387,502 @@ window.wp = window.wp || {};
 
                                        // There are no additional matches. If any content remains,
                                        // add it as an unprocessed piece.
-                                       if ( remaining )
+                                       if ( remaining ) {
                                                pieces.push({ content: remaining });
+                                       }
                                });
                        });
 
                        return _.pluck( pieces, 'content' ).join('');
                },
 
-               toView: function( viewType, options ) {
-                       var view = wp.mce.view.get( viewType ),
-                               instance, id;
-
-                       if ( ! view )
-                               return '';
-
-                       // Create a new view instance.
-                       instance = new view.view( _.extend( options || {}, {
-                               viewType: viewType
-                       }) );
-
-                       // Use the view's `id` if it already exists. Otherwise,
-                       // create a new `id`.
-                       id = instance.el.id = instance.el.id || _.uniqueId('__wpmce-');
-                       instances[ id ] = instance;
+               /**
+                * Create a placeholder for a particular view type
+                *
+                * @param viewType
+                * @param text
+                * @param options
+                *
+                */
+               toView: function( viewType, text, options ) {
+                       var view = wp.mce.views.get( viewType ),
+                               encodedText = window.encodeURIComponent( text ),
+                               instance, viewOptions;
+
+
+                       if ( ! view ) {
+                               return text;
+                       }
 
-                       // Create a dummy `$wrapper` property to allow `$wrapper` to be
-                       // called in the view's `render` method without a conditional.
-                       instance.$wrapper = $();
+                       if ( ! wp.mce.views.getInstance( encodedText ) ) {
+                               viewOptions = options;
+                               viewOptions.type = viewType;
+                               viewOptions.encodedText = encodedText;
+                               instance = new view.View( viewOptions );
+                               instances[ encodedText ] = instance;
+                       }
 
                        return wp.html.string({
-                               // If the view is a span, wrap it in a span.
-                               tag: 'span' === instance.tagName ? 'span' : 'div',
+                               tag: 'div',
 
                                attrs: {
-                                       'class':           'wp-view-wrap wp-view-type-' + viewType,
-                                       'data-wp-view':    id,
-                                       'contenteditable': false
-                               }
+                                       'class': 'wpview-wrap',
+                                       'data-wpview-text': encodedText,
+                                       'data-wpview-type': viewType
+                               },
+
+                               content: '\u00a0'
                        });
                },
 
-               // ### render( scope )
-               // Renders any view instances inside a DOM node `scope`.
-               //
-               // View instances are detected by the presence of wrapper elements.
-               // To generate wrapper elements, pass your content through
-               // `wp.mce.view.toViews( content )`.
-               render: function( scope ) {
-                       $( '.wp-view-wrap', scope ).each( function() {
-                               var wrapper = $(this),
-                                       view = wp.mce.view.instance( this );
-
-                               if ( ! view )
-                                       return;
+               /**
+                * Refresh views after an update is made
+                *
+                * @param view {object} being refreshed
+                * @param text {string} textual representation of the view
+                * @param force {Boolean} whether to force rendering
+                */
+               refreshView: function( view, text, force ) {
+                       var encodedText = window.encodeURIComponent( text ),
+                               viewOptions,
+                               result, instance;
+
+                       instance = wp.mce.views.getInstance( encodedText );
+
+                       if ( ! instance ) {
+                               result = view.toView( text );
+                               viewOptions = result.options;
+                               viewOptions.type = view.type;
+                               viewOptions.encodedText = encodedText;
+                               instance = new view.View( viewOptions );
+                               instances[ encodedText ] = instance;
+                       }
 
-                               // Link the real wrapper to the view.
-                               view.$wrapper = wrapper;
-                               // Render the view.
-                               view.render();
-                               // Detach the view element to ensure events are not unbound.
-                               view.$el.detach();
-
-                               // Empty the wrapper, attach the view element to the wrapper,
-                               // and add an ending marker to the wrapper to help regexes
-                               // scan the HTML string.
-                               wrapper.empty().append( view.el ).append('<span data-wp-view-end class="wp-view-end"></span>');
-                       });
+                       instance.render( force );
                },
 
-               // ### toText( content )
-               // Scans an HTML `content` string and replaces any view instances with
-               // their respective text representations.
-               toText: function( content ) {
-                       return content.replace( /<(?:div|span)[^>]+data-wp-view="([^"]+)"[^>]*>.*?<span[^>]+data-wp-view-end[^>]*><\/span><\/(?:div|span)>/g, function( match, id ) {
-                               var instance = instances[ id ],
-                                       view;
+               getInstance: function( encodedText ) {
+                       return instances[ encodedText ];
+               },
+
+               /**
+                * render( scope )
+                *
+                * Renders any view instances inside a DOM node `scope`.
+                *
+                * View instances are detected by the presence of wrapper elements.
+                * To generate wrapper elements, pass your content through
+                * `wp.mce.view.toViews( content )`.
+                */
+               render: function( force ) {
+                       _.each( instances, function( instance ) {
+                               instance.render( force );
+                       } );
+               },
 
-                               if ( instance )
-                                       view = wp.mce.view.get( instance.options.viewType );
+               edit: function( node ) {
+                       var viewType = $( node ).data('wpview-type'),
+                               view = wp.mce.views.get( viewType );
 
-                               return instance && view ? view.text( instance ) : '';
-                       });
+                       if ( view ) {
+                               view.edit( node );
+                       }
+               }
+       };
+
+       wp.mce.views.register( 'gallery', {
+               View: {
+                       template: media.template( 'editor-gallery' ),
+
+                       // The fallback post ID to use as a parent for galleries that don't
+                       // specify the `ids` or `include` parameters.
+                       //
+                       // Uses the hidden input on the edit posts page by default.
+                       postID: $('#post_ID').val(),
+
+                       initialize: function( options ) {
+                               this.shortcode = options.shortcode;
+                               this.fetch();
+                       },
+
+                       fetch: function() {
+                               var self = this;
+
+                               this.attachments = wp.media.gallery.attachments( this.shortcode, this.postID );
+                               this.dfd = this.attachments.more().done( function() {
+                                       self.render( true );
+                               } );
+                       },
+
+                       getHtml: function() {
+                               var attrs = this.shortcode.attrs.named,
+                                       attachments = false,
+                                       options;
+
+                               // Don't render errors while still fetching attachments
+                               if ( this.dfd && 'pending' === this.dfd.state() && ! this.attachments.length ) {
+                                       return '';
+                               }
+
+                               if ( this.attachments.length ) {
+                                       attachments = this.attachments.toJSON();
+
+                                       _.each( attachments, function( attachment ) {
+                                               if ( attachment.sizes ) {
+                                                       if ( attrs.size && attachment.sizes[ attrs.size ] ) {
+                                                               attachment.thumbnail = attachment.sizes[ attrs.size ];
+                                                       } else if ( attachment.sizes.thumbnail ) {
+                                                               attachment.thumbnail = attachment.sizes.thumbnail;
+                                                       } else if ( attachment.sizes.full ) {
+                                                               attachment.thumbnail = attachment.sizes.full;
+                                                       }
+                                               }
+                                       } );
+                               }
+
+                               options = {
+                                       attachments: attachments,
+                                       columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
+                               };
+
+                               return this.template( options );
+                       }
                },
 
-               // ### Remove internal TinyMCE attributes.
-               removeInternalAttrs: function( attrs ) {
-                       var result = {};
-                       _.each( attrs, function( value, attr ) {
-                               if ( -1 === attr.indexOf('data-mce') )
-                                       result[ attr ] = value;
+               edit: function( node ) {
+                       var gallery = wp.media.gallery,
+                               self = this,
+                               frame, data;
+
+                       data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
+                       frame = gallery.edit( data );
+
+                       frame.state('gallery-edit').on( 'update', function( selection ) {
+                               var shortcode = gallery.shortcode( selection ).string(), force;
+                               $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
+                               force = ( data !== shortcode );
+                               wp.mce.views.refreshView( self, shortcode, force );
                        });
-                       return result;
-               },
 
-               // ### Parse an attribute string and removes internal TinyMCE attributes.
-               attrs: function( content ) {
-                       return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
-               },
+                       frame.on( 'close', function() {
+                               frame.detach();
+                       });
+               }
+       } );
 
-               // ### instance( scope )
-               //
-               // Accepts a MCE view wrapper `node` (i.e. a node with the
-               // `wp-view-wrap` class).
-               instance: function( node ) {
-                       var id = $( node ).data('wp-view');
+       /**
+        * These are base methods that are shared by the audio and video shortcode's MCE controller.
+        *
+        * @mixin
+        */
+       wp.mce.av = {
+               View: {
+                       overlay: true,
 
-                       if ( id )
-                               return instances[ id ];
-               },
+                       action: 'parse-media-shortcode',
 
-               // ### Select a view.
-               //
-               // Accepts a MCE view wrapper `node` (i.e. a node with the
-               // `wp-view-wrap` class).
-               select: function( node ) {
-                       var $node = $(node);
+                       initialize: function( options ) {
+                               var self = this;
 
-                       // Bail if node is already selected.
-                       if ( $node.hasClass('selected') )
-                               return;
+                               this.shortcode = options.shortcode;
+
+                               _.bindAll( this, 'setIframes', 'setNodes', 'fetch', 'stopPlayers' );
+                               $( this ).on( 'ready', this.setNodes );
+
+                               $( document ).on( 'media:edit', this.stopPlayers );
+
+                               this.fetch();
+
+                               this.getEditors( function( editor ) {
+                                       editor.on( 'hide', function () {
+                                               mediaWindows = [];
+                                               windowIdx = 0;
+                                               self.stopPlayers();
+                                       } );
+                               });
+                       },
+
+                       pauseOtherWindows: function ( win ) {
+                               _.each( mediaWindows, function ( mediaWindow ) {
+                                       if ( mediaWindow.sandboxId !== win.sandboxId ) {
+                                               _.each( mediaWindow.mejs.players, function ( player ) {
+                                                       player.pause();
+                                               } );
+                                       }
+                               } );
+                       },
+
+                       iframeLoaded: function (win) {
+                               return _.bind( function () {
+                                       var callback;
+                                       if ( ! win.mejs || _.isEmpty( win.mejs.players ) ) {
+                                               return;
+                                       }
+
+                                       win.sandboxId = windowIdx;
+                                       windowIdx++;
+                                       mediaWindows.push( win );
+
+                                       callback = _.bind( function () {
+                                               this.pauseOtherWindows( win );
+                                       }, this );
+
+                                       if ( ! _.isEmpty( win.mejs.MediaPluginBridge.pluginMediaElements ) ) {
+                                               _.each( win.mejs.MediaPluginBridge.pluginMediaElements, function ( mediaElement ) {
+                                                       mediaElement.addEventListener( 'play', callback );
+                                               } );
+                                       }
+
+                                       _.each( win.mejs.players, function ( player ) {
+                                               $( player.node ).on( 'play', callback );
+                                       }, this );
+                               }, this );
+                       },
+
+                       listenToSandboxes: function () {
+                               _.each( this.getNodes(), function ( node ) {
+                                       var win, iframe = $( '.wpview-sandbox', node ).get( 0 );
+                                       if ( iframe && ( win = iframe.contentWindow ) ) {
+                                               $( win ).load( _.bind( this.iframeLoaded( win ), this ) );
+                                       }
+                               }, this );
+                       },
+
+                       deferredListen: function () {
+                               window.setTimeout( _.bind( this.listenToSandboxes, this ), this.getNodes().length * waitInterval );
+                       },
+
+                       setNodes: function () {
+                               if ( this.parsed ) {
+                                       this.setIframes( this.parsed.head, this.parsed.body );
+                                       this.deferredListen();
+                               } else {
+                                       this.fail();
+                               }
+                       },
+
+                       fetch: function () {
+                               var self = this;
+
+                               wp.ajax.send( this.action, {
+                                       data: {
+                                               post_ID: $( '#post_ID' ).val() || 0,
+                                               type: this.shortcode.tag,
+                                               shortcode: this.shortcode.string()
+                                       }
+                               } )
+                               .done( function( response ) {
+                                       if ( response ) {
+                                               self.parsed = response;
+                                               self.setIframes( response.head, response.body );
+                                               self.deferredListen();
+                                       } else {
+                                               self.fail( true );
+                                       }
+                               } )
+                               .fail( function( response ) {
+                                       self.fail( response || true );
+                               } );
+                       },
 
-                       $node.addClass('selected');
-                       $( node.firstChild ).trigger('select');
+                       fail: function( error ) {
+                               if ( ! this.error ) {
+                                       if ( error ) {
+                                               this.error = error;
+                                       } else {
+                                               return;
+                                       }
+                               }
+
+                               if ( this.error.message ) {
+                                       if ( ( this.error.type === 'not-embeddable' && this.type === 'embed' ) || this.error.type === 'not-ssl' ||
+                                               this.error.type === 'no-items' ) {
+
+                                               this.setError( this.error.message, 'admin-media' );
+                                       } else {
+                                               this.setContent( '<p>' + this.original + '</p>', 'replace' );
+                                       }
+                               } else if ( this.error.statusText ) {
+                                       this.setError( this.error.statusText, 'admin-media' );
+                               } else if ( this.original ) {
+                                       this.setContent( '<p>' + this.original + '</p>', 'replace' );
+                               }
+                       },
+
+                       stopPlayers: function( remove ) {
+                               var rem = remove === 'remove';
+
+                               this.getNodes( function( editor, node, content ) {
+                                       var p, win,
+                                               iframe = $( 'iframe.wpview-sandbox', content ).get(0);
+
+                                       if ( iframe && ( win = iframe.contentWindow ) && win.mejs ) {
+                                               // Sometimes ME.js may show a "Download File" placeholder and player.remove() doesn't exist there.
+                                               try {
+                                                       for ( p in win.mejs.players ) {
+                                                               win.mejs.players[p].pause();
+
+                                                               if ( rem ) {
+                                                                       win.mejs.players[p].remove();
+                                                               }
+                                                       }
+                                               } catch( er ) {}
+                                       }
+                               });
+                       },
+
+                       unbind: function() {
+                               this.stopPlayers( 'remove' );
+                       }
                },
 
-               // ### Deselect a view.
-               //
-               // Accepts a MCE view wrapper `node` (i.e. a node with the
-               // `wp-view-wrap` class).
-               deselect: function( node ) {
-                       var $node = $(node);
+               /**
+                * Called when a TinyMCE view is clicked for editing.
+                * - Parses the shortcode out of the element's data attribute
+                * - Calls the `edit` method on the shortcode model
+                * - Launches the model window
+                * - Bind's an `update` callback which updates the element's data attribute
+                *   re-renders the view
+                *
+                * @param {HTMLElement} node
+                */
+               edit: function( node ) {
+                       var media = wp.media[ this.type ],
+                               self = this,
+                               frame, data, callback;
+
+                       $( document ).trigger( 'media:edit' );
+
+                       data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
+                       frame = media.edit( data );
+                       frame.on( 'close', function() {
+                               frame.detach();
+                       } );
+
+                       callback = function( selection ) {
+                               var shortcode = wp.media[ self.type ].shortcode( selection ).string();
+                               $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
+                               wp.mce.views.refreshView( self, shortcode );
+                               frame.detach();
+                       };
+                       if ( _.isArray( self.state ) ) {
+                               _.each( self.state, function (state) {
+                                       frame.state( state ).on( 'update', callback );
+                               } );
+                       } else {
+                               frame.state( self.state ).on( 'update', callback );
+                       }
+                       frame.open();
+               }
+       };
+
+       /**
+        * TinyMCE handler for the video shortcode
+        *
+        * @mixes wp.mce.av
+        */
+       wp.mce.views.register( 'video', _.extend( {}, wp.mce.av, {
+               state: 'video-details'
+       } ) );
+
+       /**
+        * TinyMCE handler for the audio shortcode
+        *
+        * @mixes wp.mce.av
+        */
+       wp.mce.views.register( 'audio', _.extend( {}, wp.mce.av, {
+               state: 'audio-details'
+       } ) );
+
+       /**
+        * TinyMCE handler for the playlist shortcode
+        *
+        * @mixes wp.mce.av
+        */
+       wp.mce.views.register( 'playlist', _.extend( {}, wp.mce.av, {
+               state: [ 'playlist-edit', 'video-playlist-edit' ]
+       } ) );
+
+       /**
+        * TinyMCE handler for the embed shortcode
+        */
+       wp.mce.embedMixin = {
+               View: _.extend( {}, wp.mce.av.View, {
+                       overlay: true,
+                       action: 'parse-embed',
+                       initialize: function( options ) {
+                               this.content = options.content;
+                               this.original = options.url || options.shortcode.string();
+
+                               if ( options.url ) {
+                                       this.shortcode = media.embed.shortcode( {
+                                               url: options.url
+                                       } );
+                               } else {
+                                       this.shortcode = options.shortcode;
+                               }
 
-                       // Bail if node is already selected.
-                       if ( ! $node.hasClass('selected') )
-                               return;
+                               _.bindAll( this, 'setIframes', 'setNodes', 'fetch' );
+                               $( this ).on( 'ready', this.setNodes );
 
-                       $node.removeClass('selected');
-                       $( node.firstChild ).trigger('deselect');
+                               this.fetch();
+                       }
+               } ),
+               edit: function( node ) {
+                       var embed = media.embed,
+                               self = this,
+                               frame,
+                               data,
+                               isURL = 'embedURL' === this.type;
+
+                       $( document ).trigger( 'media:edit' );
+
+                       data = window.decodeURIComponent( $( node ).attr('data-wpview-text') );
+                       frame = embed.edit( data, isURL );
+                       frame.on( 'close', function() {
+                               frame.detach();
+                       } );
+                       frame.state( 'embed' ).props.on( 'change:url', function (model, url) {
+                               if ( ! url ) {
+                                       return;
+                               }
+                               frame.state( 'embed' ).metadata = model.toJSON();
+                       } );
+                       frame.state( 'embed' ).on( 'select', function() {
+                               var shortcode;
+
+                               if ( isURL ) {
+                                       shortcode = frame.state( 'embed' ).metadata.url;
+                               } else {
+                                       shortcode = embed.shortcode( frame.state( 'embed' ).metadata ).string();
+                               }
+                               $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) );
+                               wp.mce.views.refreshView( self, shortcode );
+                               frame.detach();
+                       } );
+                       frame.open();
                }
        };
 
-}(jQuery));
\ No newline at end of file
+       wp.mce.views.register( 'embed', _.extend( {}, wp.mce.embedMixin ) );
+
+       wp.mce.views.register( 'embedURL', _.extend( {}, wp.mce.embedMixin, {
+               toView: function( content ) {
+                       var re = /(?:^|<p>)(https?:\/\/[^\s"]+?)(?:<\/p>\s*|$)/gi,
+                               match = re.exec( tinymce.trim( content ) );
+
+                       if ( ! match ) {
+                               return;
+                       }
+
+                       return {
+                               index: match.index,
+                               content: match[0],
+                               options: {
+                                       url: match[1]
+                               }
+                       };
+               }
+       } ) );
+
+}(jQuery));