balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

mce-view.js (26059B)


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