wp/wp-includes/js/mce-view.js
changeset 5 5e2f62d02dcd
parent 0 d970ebf37754
child 7 cf61fcea0001
--- a/wp/wp-includes/js/mce-view.js	Mon Jun 08 16:11:51 2015 +0000
+++ b/wp/wp-includes/js/mce-view.js	Tue Jun 09 03:35:32 2015 +0200
@@ -1,178 +1,102 @@
-// Ensure the global `wp` object exists.
+/* global tinymce */
+
 window.wp = window.wp || {};
 
-(function($){
+/*
+ * The TinyMCE view API.
+ *
+ * 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.
+ *
+ * Diagram
+ *
+ * |- registered view constructor (type)
+ * |  |- view instance (unique text)
+ * |  |  |- editor 1
+ * |  |  |  |- view node
+ * |  |  |  |- view node
+ * |  |  |  |- ...
+ * |  |  |- editor 2
+ * |  |  |  |- ...
+ * |  |- view instance
+ * |  |  |- ...
+ * |- registered view
+ * |  |- ...
+ */
+( function( window, wp, $ ) {
+	'use strict';
+
 	var views = {},
 		instances = {};
 
-	// 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 );
-
-					if ( ! match )
-						return;
+	/**
+	 * 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 = {
 
-					return {
-						index:   match.index,
-						content: match[0],
-						options: {
-							original: match[0],
-							results:  match
-						}
-					};
-				}
-			},
+		/**
+		 * Registers a new view type.
+		 *
+		 * @param {String} type   The view type.
+		 * @param {Object} extend An object to extend wp.mce.View.prototype with.
+		 */
+		register: function( type, extend ) {
+			views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) );
+		},
 
-			// 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();
-				},
-
-				toView: function( content ) {
-					var match = wp.shortcode.next( this.shortcode, content );
-
-					if ( ! match )
-						return;
-
-					return {
-						index:   match.index,
-						content: match.content,
-						options: {
-							shortcode: match.shortcode
-						}
-					};
-				}
-			}
+		/**
+		 * Unregisters a view type.
+		 *
+		 * @param {String} type The view type.
+		 */
+		unregister: function( type ) {
+			delete views[ type ];
 		},
 
-		// ### 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 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;
-			} else {
-				base   = parent.view;
-				remove = options.view.remove;
-				_.defaults( properties, options.view );
-			}
-
-			// 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;
-
-			// 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
-			});
-
-			views[ id ] = options;
+		/**
+		 * Returns the settings of a view type.
+		 *
+		 * @param {String} type The view type.
+		 *
+		 * @return {Function} The view constructor.
+		 */
+		get: function( type ) {
+			return views[ type ];
 		},
 
-		// ### get( id )
-		// Returns a TinyMCE view options object.
-		get: function( id ) {
-			return views[ id ];
-		},
-
-		// ### remove( id )
-		// Unregisters a TinyMCE view.
-		remove: function( id ) {
-			delete views[ id ];
+		/**
+		 * Unbinds all view nodes.
+		 * Runs before removing all view nodes from the DOM.
+		 */
+		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 view instance for
-		// every match.
-		//
-		// To render the views, call `wp.mce.view.render( scope )`.
-		toViews: function( content ) {
+		/**
+		 * Scans a given string for each view's pattern,
+		 * replacing any matches with markers,
+		 * and creates a new instance for every match.
+		 *
+		 * @param {String} content The string to scan.
+		 *
+		 * @return {String} The string with markers.
+		 */
+		setMarkers: function( content ) {
 			var pieces = [ { content: content } ],
+				self = this,
+				instance,
 				current;
 
-			_.each( views, function( view, viewType ) {
+			_.each( views, function( view, type ) {
 				current = pieces.slice();
 				pieces  = [];
 
@@ -188,162 +112,770 @@
 
 					// Iterate through the string progressively matching views
 					// and slicing the string as we go.
-					while ( remaining && (result = view.toView( remaining )) ) {
+					while ( remaining && ( result = view.prototype.match( remaining ) ) ) {
 						// Any text before the match becomes an unprocessed piece.
-						if ( result.index )
-							pieces.push({ content: remaining.substring( 0, result.index ) });
+						if ( result.index ) {
+							pieces.push( { content: remaining.substring( 0, result.index ) } );
+						}
+
+						instance = self.createInstance( type, result.content, result.options );
 
 						// Add the processed piece for the match.
-						pieces.push({
-							content:   wp.mce.view.toView( viewType, result.options ),
+						pieces.push( {
+							content: '<p data-wpview-marker="' + instance.encodedText + '">' + instance.text + '</p>',
 							processed: true
-						});
+						} );
 
 						// Update the remaining content.
 						remaining = remaining.slice( result.index + result.content.length );
 					}
 
-					// There are no additional matches. If any content remains,
-					// add it as an unprocessed piece.
-					if ( remaining )
-						pieces.push({ content: remaining });
-				});
-			});
+					// There are no additional matches.
+					// If any content remains, add it as an unprocessed piece.
+					if ( remaining ) {
+						pieces.push( { content: remaining } );
+					}
+				} );
+			} );
 
-			return _.pluck( pieces, 'content' ).join('');
+			return _.pluck( pieces, 'content' ).join( '' );
 		},
 
-		toView: function( viewType, options ) {
-			var view = wp.mce.view.get( viewType ),
-				instance, id;
+		/**
+		 * Create a view instance.
+		 *
+		 * @param {String} type    The view type.
+		 * @param {String} text    The textual representation of the view.
+		 * @param {Object} options Options.
+		 *
+		 * @return {wp.mce.View} The view instance.
+		 */
+		createInstance: function( type, text, options ) {
+			var View = this.get( type ),
+				encodedText,
+				instance;
 
-			if ( ! view )
-				return '';
+			text = tinymce.DOM.decode( text ),
+			encodedText = encodeURIComponent( text ),
+			instance = this.getInstance( encodedText );
 
-			// Create a new view instance.
-			instance = new view.view( _.extend( options || {}, {
-				viewType: viewType
-			}) );
+			if ( instance ) {
+				return instance;
+			}
 
-			// 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;
+			options = _.extend( options || {}, {
+				text: text,
+				encodedText: encodedText
+			} );
 
-			// Create a dummy `$wrapper` property to allow `$wrapper` to be
-			// called in the view's `render` method without a conditional.
-			instance.$wrapper = $();
+			return instances[ encodedText ] = new View( options );
+		},
 
-			return wp.html.string({
-				// If the view is a span, wrap it in a span.
-				tag: 'span' === instance.tagName ? 'span' : 'div',
+		/**
+		 * Get a view instance.
+		 *
+		 * @param {(String|HTMLElement)} object The textual representation of the view or the view node.
+		 *
+		 * @return {wp.mce.View} The view instance or undefined.
+		 */
+		getInstance: function( object ) {
+			if ( typeof object === 'string' ) {
+				return instances[ encodeURIComponent( object ) ];
+			}
 
-				attrs: {
-					'class':           'wp-view-wrap wp-view-type-' + viewType,
-					'data-wp-view':    id,
-					'contenteditable': false
-				}
-			});
+			return instances[ $( object ).attr( 'data-wpview-text' ) ];
 		},
 
-		// ### 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;
-
-				// 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>');
-			});
+		/**
+		 * Given a view node, get the view's text.
+		 *
+		 * @param {HTMLElement} node The view node.
+		 *
+		 * @return {String} The textual representation of the view.
+		 */
+		getText: function( node ) {
+			return decodeURIComponent( $( node ).attr( 'data-wpview-text' ) || '' );
 		},
 
-		// ### 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;
-
-				if ( instance )
-					view = wp.mce.view.get( instance.options.viewType );
-
-				return instance && view ? view.text( instance ) : '';
-			});
+		/**
+		 * Renders all view nodes that are not yet rendered.
+		 *
+		 * @param {Boolean} force Rerender all view nodes.
+		 */
+		render: function( force ) {
+			_.each( instances, function( instance ) {
+				instance.render( force );
+			} );
 		},
 
-		// ### Remove internal TinyMCE attributes.
-		removeInternalAttrs: function( attrs ) {
-			var result = {};
-			_.each( attrs, function( value, attr ) {
-				if ( -1 === attr.indexOf('data-mce') )
-					result[ attr ] = value;
-			});
-			return result;
-		},
+		/**
+		 * Update the text of a given view node.
+		 *
+		 * @param {String}         text   The new text.
+		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
+		 * @param {HTMLElement}    node   The view node to update.
+		 */
+		update: function( text, editor, node ) {
+			var instance = this.getInstance( node );
 
-		// ### Parse an attribute string and removes internal TinyMCE attributes.
-		attrs: function( content ) {
-			return wp.mce.view.removeInternalAttrs( wp.html.attrs( content ) );
-		},
-
-		// ### 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');
-
-			if ( id )
-				return instances[ id ];
+			if ( instance ) {
+				instance.update( text, editor, node );
+			}
 		},
 
-		// ### 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);
+		/**
+		 * Renders any editing interface based on the view type.
+		 *
+		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
+		 * @param {HTMLElement}    node   The view node to edit.
+		 */
+		edit: function( editor, node ) {
+			var instance = this.getInstance( node );
 
-			// Bail if node is already selected.
-			if ( $node.hasClass('selected') )
-				return;
-
-			$node.addClass('selected');
-			$( node.firstChild ).trigger('select');
+			if ( instance && instance.edit ) {
+				instance.edit( instance.text, function( text ) {
+					instance.update( text, editor, node );
+				} );
+			}
 		},
 
-		// ### 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);
+		/**
+		 * Remove a given view node from the DOM.
+		 *
+		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
+		 * @param {HTMLElement}    node   The view node to remove.
+		 */
+		remove: function( editor, node ) {
+			var instance = this.getInstance( node );
 
-			// Bail if node is already selected.
-			if ( ! $node.hasClass('selected') )
-				return;
-
-			$node.removeClass('selected');
-			$( node.firstChild ).trigger('deselect');
+			if ( instance ) {
+				instance.remove( editor, node );
+			}
 		}
 	};
 
-}(jQuery));
\ No newline at end of file
+	/**
+	 * 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 Options.
+	 */
+	wp.mce.View = function( options ) {
+		_.extend( this, options );
+		this.initialize();
+	};
+
+	wp.mce.View.extend = Backbone.View.extend;
+
+	_.extend( wp.mce.View.prototype, {
+
+		/**
+		 * The content.
+		 *
+		 * @type {*}
+		 */
+		content: null,
+
+		/**
+		 * Whether or not to display a loader.
+		 *
+		 * @type {Boolean}
+		 */
+		loader: true,
+
+		/**
+		 * Runs after the view instance is created.
+		 */
+		initialize: function() {},
+
+		/**
+		 * Retuns the content to render in the view node.
+		 *
+		 * @return {*}
+		 */
+		getContent: function() {
+			return this.content;
+		},
+
+		/**
+		 * Renders all view nodes tied to this view instance that are not yet rendered.
+		 *
+		 * @param {String} content The content to render. Optional.
+		 * @param {Boolean} force Rerender all view nodes tied to this view instance.
+		 */
+		render: function( content, force ) {
+			if ( content != null ) {
+				this.content = content;
+			}
+
+			content = this.getContent();
+
+			// If there's nothing to render an no loader needs to be shown, stop.
+			if ( ! this.loader && ! content ) {
+				return;
+			}
+
+			// We're about to rerender all views of this instance, so unbind rendered views.
+			force && this.unbind();
+
+			// Replace any left over markers.
+			this.replaceMarkers();
+
+			if ( content ) {
+				this.setContent( content, function( editor, node, contentNode ) {
+					$( node ).data( 'rendered', true );
+					this.bindNode.call( this, editor, node, contentNode );
+				}, force ? null : false );
+			} else {
+				this.setLoader();
+			}
+		},
+
+		/**
+		 * Binds a given node after its content is added to the DOM.
+		 */
+		bindNode: function() {},
+
+		/**
+		 * Unbinds a given node before its content is removed from the DOM.
+		 */
+		unbindNode: function() {},
+
+		/**
+		 * Unbinds all view nodes tied to this view instance.
+		 * Runs before their content is removed from the DOM.
+		 */
+		unbind: function() {
+			this.getNodes( function( editor, node, contentNode ) {
+				this.unbindNode.call( this, editor, node, contentNode );
+				$( node ).trigger( 'wp-mce-view-unbind' );
+			}, true );
+		},
+
+		/**
+		 * Gets all the TinyMCE editor instances that support views.
+		 *
+		 * @param {Function} callback A callback.
+		 */
+		getEditors: function( callback ) {
+			_.each( tinymce.editors, function( editor ) {
+				if ( editor.plugins.wpview ) {
+					callback.call( this, editor );
+				}
+			}, this );
+		},
+
+		/**
+		 * Gets all view nodes tied to this view instance.
+		 *
+		 * @param {Function} callback A callback.
+		 * @param {Boolean}  rendered Get (un)rendered view nodes. Optional.
+		 */
+		getNodes: function( callback, rendered ) {
+			this.getEditors( function( editor ) {
+				var self = this;
+
+				$( editor.getBody() )
+					.find( '[data-wpview-text="' + self.encodedText + '"]' )
+					.filter( function() {
+						var data;
+
+						if ( rendered == null ) {
+							return true;
+						}
+
+						data = $( this ).data( 'rendered' ) === true;
+
+						return rendered ? data : ! data;
+					} )
+					.each( function() {
+						callback.call( self, editor, this, $( this ).find( '.wpview-content' ).get( 0 ) );
+					} );
+			} );
+		},
+
+		/**
+		 * Gets all marker nodes tied to this view instance.
+		 *
+		 * @param {Function} callback A callback.
+		 */
+		getMarkers: function( callback ) {
+			this.getEditors( function( editor ) {
+				var self = this;
+
+				$( editor.getBody() )
+					.find( '[data-wpview-marker="' + this.encodedText + '"]' )
+					.each( function() {
+						callback.call( self, editor, this );
+					} );
+			} );
+		},
+
+		/**
+		 * Replaces all marker nodes tied to this view instance.
+		 */
+		replaceMarkers: function() {
+			this.getMarkers( function( editor, node ) {
+				if ( $( node ).text() !== this.text ) {
+					editor.dom.setAttrib( node, 'data-wpview-marker', null );
+					return;
+				}
+
+				editor.dom.replace(
+					editor.dom.createFragment(
+						'<div class="wpview-wrap" data-wpview-text="' + this.encodedText + '" data-wpview-type="' + this.type + '">' +
+							'<p class="wpview-selection-before">\u00a0</p>' +
+							'<div class="wpview-body" contenteditable="false">' +
+								'<div class="wpview-content wpview-type-' + this.type + '"></div>' +
+							'</div>' +
+							'<p class="wpview-selection-after">\u00a0</p>' +
+						'</div>'
+					),
+					node
+				);
+			} );
+		},
+
+		/**
+		 * Removes all marker nodes tied to this view instance.
+		 */
+		removeMarkers: function() {
+			this.getMarkers( function( editor, node ) {
+				editor.dom.setAttrib( node, 'data-wpview-marker', null );
+			} );
+		},
+
+		/**
+		 * Sets the content for all view nodes tied to this view instance.
+		 *
+		 * @param {*}        content  The content to set.
+		 * @param {Function} callback A callback. Optional.
+		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
+		 */
+		setContent: function( content, callback, rendered ) {
+			if ( _.isObject( content ) && content.body.indexOf( '<script' ) !== -1 ) {
+				this.setIframes( content.head || '', content.body, callback, rendered );
+			} else if ( _.isString( content ) && content.indexOf( '<script' ) !== -1 ) {
+				this.setIframes( '', content, callback, rendered );
+			} else {
+				this.getNodes( function( editor, node, contentNode ) {
+					content = content.body || content;
+
+					if ( content.indexOf( '<iframe' ) !== -1 ) {
+						content += '<div class="wpview-overlay"></div>';
+					}
+
+					contentNode.innerHTML = '';
+					contentNode.appendChild( _.isString( content ) ? editor.dom.createFragment( content ) : content );
+
+					callback && callback.call( this, editor, node, contentNode );
+				}, rendered );
+			}
+		},
+
+		/**
+		 * Sets the content in an iframe for all view nodes tied to this view instance.
+		 *
+		 * @param {String}   head     HTML string to be added to the head of the document.
+		 * @param {String}   body     HTML string to be added to the body of the document.
+		 * @param {Function} callback A callback. Optional.
+		 * @param {Boolean}  rendered Only set for (un)rendered nodes. Optional.
+		 */
+		setIframes: function( head, body, callback, rendered ) {
+			var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
+				self = this;
+
+			this.getNodes( function( editor, node, contentNode ) {
+				var dom = editor.dom,
+					styles = '',
+					bodyClasses = editor.getBody().className || '',
+					editorHead = editor.getDoc().getElementsByTagName( 'head' )[0];
+
+				tinymce.each( dom.$( 'link[rel="stylesheet"]', editorHead ), 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 );
+					}
+				} );
+
+				// Seems the browsers need a bit of time to insert/set the view nodes,
+				// or the iframe will fail especially when switching Text => Visual.
+				setTimeout( function() {
+					var iframe, iframeDoc, observer, i;
+
+					contentNode.innerHTML = '';
+
+					iframe = dom.add( contentNode, 'iframe', {
+						/* jshint scripturl: true */
+						src: tinymce.Env.ie ? 'javascript:""' : '',
+						frameBorder: '0',
+						allowTransparency: 'true',
+						scrolling: 'no',
+						'class': 'wpview-sandbox',
+						style: {
+							width: '100%',
+							display: 'block'
+						}
+					} );
+
+					dom.add( contentNode, 'div', { 'class': 'wpview-overlay' } );
+
+					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();
+
+					function resize() {
+						var $iframe, iframeDocHeight;
+
+						// Make sure the iframe still exists.
+						if ( iframe.contentWindow ) {
+							$iframe = $( iframe );
+							iframeDocHeight = $( iframeDoc.body ).height();
+
+							if ( $iframe.height() !== iframeDocHeight ) {
+								$iframe.height( iframeDocHeight );
+								editor.nodeChanged();
+							}
+						}
+					}
+
+					$( iframe.contentWindow ).on( 'load', resize );
+
+					if ( MutationObserver ) {
+						observer = new MutationObserver( _.debounce( resize, 100 ) );
+
+						observer.observe( iframeDoc.body, {
+							attributes: true,
+							childList: true,
+							subtree: true
+						} );
+
+						$( node ).one( 'wp-mce-view-unbind', function() {
+							observer.disconnect();
+						} );
+					} else {
+						for ( i = 1; i < 6; i++ ) {
+							setTimeout( resize, i * 700 );
+						}
+					}
+
+					function classChange() {
+						iframeDoc.body.className = editor.getBody().className;
+					}
+
+					editor.on( 'wp-body-class-change', classChange );
+
+					$( node ).one( 'wp-mce-view-unbind', function() {
+						editor.off( 'wp-body-class-change', classChange );
+					} );
+
+					callback && callback.call( self, editor, node, contentNode );
+				}, 50 );
+			}, rendered );
+		},
+
+		/**
+		 * Sets a loader for all view nodes tied to this view instance.
+		 */
+		setLoader: function() {
+			this.setContent(
+				'<div class="loading-placeholder">' +
+					'<div class="dashicons dashicons-admin-media"></div>' +
+					'<div class="wpview-loading"><ins></ins></div>' +
+				'</div>'
+			);
+		},
+
+		/**
+		 * Sets an error for all view nodes tied to this view instance.
+		 *
+		 * @param {String} message  The error message to set.
+		 * @param {String} dashicon A dashicon ID (optional). {@link https://developer.wordpress.org/resource/dashicons/}
+		 */
+		setError: function( message, dashicon ) {
+			this.setContent(
+				'<div class="wpview-error">' +
+					'<div class="dashicons dashicons-' + ( dashicon || 'no' ) + '"></div>' +
+					'<p>' + message + '</p>' +
+				'</div>'
+			);
+		},
+
+		/**
+		 * Tries to find a text match in a given string.
+		 *
+		 * @param {String} content The string to scan.
+		 *
+		 * @return {Object}
+		 */
+		match: function( content ) {
+			var match = wp.shortcode.next( this.type, content );
+
+			if ( match ) {
+				return {
+					index: match.index,
+					content: match.content,
+					options: {
+						shortcode: match.shortcode
+					}
+				};
+			}
+		},
+
+		/**
+		 * Update the text of a given view node.
+		 *
+		 * @param {String}         text   The new text.
+		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
+		 * @param {HTMLElement}    node   The view node to update.
+		 */
+		update: function( text, editor, node ) {
+			_.find( views, function( view, type ) {
+				var match = view.prototype.match( text );
+
+				if ( match ) {
+					$( node ).data( 'rendered', false );
+					editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) );
+					wp.mce.views.createInstance( type, text, match.options ).render();
+					editor.focus();
+
+					return true;
+				}
+			} );
+		},
+
+		/**
+		 * Remove a given view node from the DOM.
+		 *
+		 * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in.
+		 * @param {HTMLElement}    node   The view node to remove.
+		 */
+		remove: function( editor, node ) {
+			this.unbindNode.call( this, editor, node, $( node ).find( '.wpview-content' ).get( 0 ) );
+			$( node ).trigger( 'wp-mce-view-unbind' );
+			editor.dom.remove( node );
+			editor.focus();
+		}
+	} );
+} )( window, window.wp, window.jQuery );
+
+/*
+ * The WordPress core TinyMCE views.
+ * Views for the gallery, audio, video, playlist and embed shortcodes,
+ * and a view for embeddable URLs.
+ */
+( function( window, views, $ ) {
+	var postID = $( '#post_ID' ).val() || 0,
+		media, gallery, av, embed;
+
+	media = {
+		state: [],
+
+		edit: function( text, update ) {
+			var media = wp.media[ this.type ],
+				frame = media.edit( text );
+
+			this.pausePlayers && this.pausePlayers();
+
+			_.each( this.state, function( state ) {
+				frame.state( state ).on( 'update', function( selection ) {
+					update( media.shortcode( selection ).string() );
+				} );
+			} );
+
+			frame.on( 'close', function() {
+				frame.detach();
+			} );
+
+			frame.open();
+		}
+	};
+
+	gallery = _.extend( {}, media, {
+		state: [ 'gallery-edit' ],
+		template: wp.media.template( 'editor-gallery' ),
+
+		initialize: function() {
+			var attachments = wp.media.gallery.attachments( this.shortcode, postID ),
+				attrs = this.shortcode.attrs.named,
+				self = this;
+
+			attachments.more()
+			.done( function() {
+				attachments = 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;
+						}
+					}
+				} );
+
+				self.render( self.template( {
+					attachments: attachments,
+					columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
+				} ) );
+			} )
+			.fail( function( jqXHR, textStatus ) {
+				self.setError( textStatus );
+			} );
+		}
+	} );
+
+	av = _.extend( {}, media, {
+		action: 'parse-media-shortcode',
+
+		initialize: function() {
+			var self = this;
+
+			if ( this.url ) {
+				this.loader = false;
+				this.shortcode = wp.media.embed.shortcode( {
+					url: this.text
+				} );
+			}
+
+			wp.ajax.post( this.action, {
+				post_ID: postID,
+				type: this.shortcode.tag,
+				shortcode: this.shortcode.string()
+			} )
+			.done( function( response ) {
+				self.render( response );
+			} )
+			.fail( function( response ) {
+				if ( self.url ) {
+					self.removeMarkers();
+				} else {
+					self.setError( response.message || response.statusText, 'admin-media' );
+				}
+			} );
+
+			this.getEditors( function( editor ) {
+				editor.on( 'wpview-selected', function() {
+					self.pausePlayers();
+				} );
+			} );
+		},
+
+		pausePlayers: function() {
+			this.getNodes( function( editor, node, content ) {
+				var win = $( 'iframe.wpview-sandbox', content ).get( 0 );
+
+				if ( win && ( win = win.contentWindow ) && win.mejs ) {
+					_.each( win.mejs.players, function( player ) {
+						try {
+							player.pause();
+						} catch ( e ) {}
+					} );
+				}
+			} );
+		}
+	} );
+
+	embed = _.extend( {}, av, {
+		action: 'parse-embed',
+
+		edit: function( text, update ) {
+			var media = wp.media.embed,
+				frame = media.edit( text, this.url ),
+				self = this;
+
+			this.pausePlayers();
+
+			frame.state( 'embed' ).props.on( 'change:url', function( model, url ) {
+				if ( url && model.get( 'url' ) ) {
+					frame.state( 'embed' ).metadata = model.toJSON();
+				}
+			} );
+
+			frame.state( 'embed' ).on( 'select', function() {
+				var data = frame.state( 'embed' ).metadata;
+
+				if ( self.url ) {
+					update( data.url );
+				} else {
+					update( media.shortcode( data ).string() );
+				}
+			} );
+
+			frame.on( 'close', function() {
+				frame.detach();
+			} );
+
+			frame.open();
+		}
+	} );
+
+	views.register( 'gallery', _.extend( {}, gallery ) );
+
+	views.register( 'audio', _.extend( {}, av, {
+		state: [ 'audio-details' ]
+	} ) );
+
+	views.register( 'video', _.extend( {}, av, {
+		state: [ 'video-details' ]
+	} ) );
+
+	views.register( 'playlist', _.extend( {}, av, {
+		state: [ 'playlist-edit', 'video-playlist-edit' ]
+	} ) );
+
+	views.register( 'embed', _.extend( {}, embed ) );
+
+	views.register( 'embedURL', _.extend( {}, embed, {
+		match: function( content ) {
+			var re = /(^|<p>)(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi,
+				match = re.exec( content );
+
+			if ( match ) {
+				return {
+					index: match.index + match[1].length,
+					content: match[2],
+					options: {
+						url: true
+					}
+				};
+			}
+		}
+	} ) );
+} )( window, window.wp.mce.views, window.jQuery );