PageRenderTime 56ms CodeModel.GetById 17ms app.highlight 31ms RepoModel.GetById 1ms app.codeStats 1ms

/wp-includes/js/mce-view.js

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