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