ru-se.com

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

media-editor.js (29119B)


      1 /**
      2  * @output wp-includes/js/media-editor.js
      3  */
      4 
      5 /* global getUserSetting, tinymce, QTags */
      6 
      7 // WordPress, TinyMCE, and Media
      8 // -----------------------------
      9 (function($, _){
     10 	/**
     11 	 * Stores the editors' `wp.media.controller.Frame` instances.
     12 	 *
     13 	 * @static
     14 	 */
     15 	var workflows = {};
     16 
     17 	/**
     18 	 * A helper mixin function to avoid truthy and falsey values being
     19 	 *   passed as an input that expects booleans. If key is undefined in the map,
     20 	 *   but has a default value, set it.
     21 	 *
     22 	 * @param {Object} attrs Map of props from a shortcode or settings.
     23 	 * @param {string} key The key within the passed map to check for a value.
     24 	 * @return {mixed|undefined} The original or coerced value of key within attrs.
     25 	 */
     26 	wp.media.coerce = function ( attrs, key ) {
     27 		if ( _.isUndefined( attrs[ key ] ) && ! _.isUndefined( this.defaults[ key ] ) ) {
     28 			attrs[ key ] = this.defaults[ key ];
     29 		} else if ( 'true' === attrs[ key ] ) {
     30 			attrs[ key ] = true;
     31 		} else if ( 'false' === attrs[ key ] ) {
     32 			attrs[ key ] = false;
     33 		}
     34 		return attrs[ key ];
     35 	};
     36 
     37 	/** @namespace wp.media.string */
     38 	wp.media.string = {
     39 		/**
     40 		 * Joins the `props` and `attachment` objects,
     41 		 * outputting the proper object format based on the
     42 		 * attachment's type.
     43 		 *
     44 		 * @param {Object} [props={}] Attachment details (align, link, size, etc).
     45 		 * @param {Object} attachment The attachment object, media version of Post.
     46 		 * @return {Object} Joined props
     47 		 */
     48 		props: function( props, attachment ) {
     49 			var link, linkUrl, size, sizes,
     50 				defaultProps = wp.media.view.settings.defaultProps;
     51 
     52 			props = props ? _.clone( props ) : {};
     53 
     54 			if ( attachment && attachment.type ) {
     55 				props.type = attachment.type;
     56 			}
     57 
     58 			if ( 'image' === props.type ) {
     59 				props = _.defaults( props || {}, {
     60 					align:   defaultProps.align || getUserSetting( 'align', 'none' ),
     61 					size:    defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
     62 					url:     '',
     63 					classes: []
     64 				});
     65 			}
     66 
     67 			// All attachment-specific settings follow.
     68 			if ( ! attachment ) {
     69 				return props;
     70 			}
     71 
     72 			props.title = props.title || attachment.title;
     73 
     74 			link = props.link || defaultProps.link || getUserSetting( 'urlbutton', 'file' );
     75 			if ( 'file' === link || 'embed' === link ) {
     76 				linkUrl = attachment.url;
     77 			} else if ( 'post' === link ) {
     78 				linkUrl = attachment.link;
     79 			} else if ( 'custom' === link ) {
     80 				linkUrl = props.linkUrl;
     81 			}
     82 			props.linkUrl = linkUrl || '';
     83 
     84 			// Format properties for images.
     85 			if ( 'image' === attachment.type ) {
     86 				props.classes.push( 'wp-image-' + attachment.id );
     87 
     88 				sizes = attachment.sizes;
     89 				size = sizes && sizes[ props.size ] ? sizes[ props.size ] : attachment;
     90 
     91 				_.extend( props, _.pick( attachment, 'align', 'caption', 'alt' ), {
     92 					width:     size.width,
     93 					height:    size.height,
     94 					src:       size.url,
     95 					captionId: 'attachment_' + attachment.id
     96 				});
     97 			} else if ( 'video' === attachment.type || 'audio' === attachment.type ) {
     98 				_.extend( props, _.pick( attachment, 'title', 'type', 'icon', 'mime' ) );
     99 			// Format properties for non-images.
    100 			} else {
    101 				props.title = props.title || attachment.filename;
    102 				props.rel = props.rel || 'attachment wp-att-' + attachment.id;
    103 			}
    104 
    105 			return props;
    106 		},
    107 		/**
    108 		 * Create link markup that is suitable for passing to the editor
    109 		 *
    110 		 * @param {Object} props Attachment details (align, link, size, etc).
    111 		 * @param {Object} attachment The attachment object, media version of Post.
    112 		 * @return {string} The link markup
    113 		 */
    114 		link: function( props, attachment ) {
    115 			var options;
    116 
    117 			props = wp.media.string.props( props, attachment );
    118 
    119 			options = {
    120 				tag:     'a',
    121 				content: props.title,
    122 				attrs:   {
    123 					href: props.linkUrl
    124 				}
    125 			};
    126 
    127 			if ( props.rel ) {
    128 				options.attrs.rel = props.rel;
    129 			}
    130 
    131 			return wp.html.string( options );
    132 		},
    133 		/**
    134 		 * Create an Audio shortcode string that is suitable for passing to the editor
    135 		 *
    136 		 * @param {Object} props Attachment details (align, link, size, etc).
    137 		 * @param {Object} attachment The attachment object, media version of Post.
    138 		 * @return {string} The audio shortcode
    139 		 */
    140 		audio: function( props, attachment ) {
    141 			return wp.media.string._audioVideo( 'audio', props, attachment );
    142 		},
    143 		/**
    144 		 * Create a Video shortcode string that is suitable for passing to the editor
    145 		 *
    146 		 * @param {Object} props Attachment details (align, link, size, etc).
    147 		 * @param {Object} attachment The attachment object, media version of Post.
    148 		 * @return {string} The video shortcode
    149 		 */
    150 		video: function( props, attachment ) {
    151 			return wp.media.string._audioVideo( 'video', props, attachment );
    152 		},
    153 		/**
    154 		 * Helper function to create a media shortcode string
    155 		 *
    156 		 * @access private
    157 		 *
    158 		 * @param {string} type The shortcode tag name: 'audio' or 'video'.
    159 		 * @param {Object} props Attachment details (align, link, size, etc).
    160 		 * @param {Object} attachment The attachment object, media version of Post.
    161 		 * @return {string} The media shortcode
    162 		 */
    163 		_audioVideo: function( type, props, attachment ) {
    164 			var shortcode, html, extension;
    165 
    166 			props = wp.media.string.props( props, attachment );
    167 			if ( props.link !== 'embed' ) {
    168 				return wp.media.string.link( props );
    169 			}
    170 
    171 			shortcode = {};
    172 
    173 			if ( 'video' === type ) {
    174 				if ( attachment.image && -1 === attachment.image.src.indexOf( attachment.icon ) ) {
    175 					shortcode.poster = attachment.image.src;
    176 				}
    177 
    178 				if ( attachment.width ) {
    179 					shortcode.width = attachment.width;
    180 				}
    181 
    182 				if ( attachment.height ) {
    183 					shortcode.height = attachment.height;
    184 				}
    185 			}
    186 
    187 			extension = attachment.filename.split('.').pop();
    188 
    189 			if ( _.contains( wp.media.view.settings.embedExts, extension ) ) {
    190 				shortcode[extension] = attachment.url;
    191 			} else {
    192 				// Render unsupported audio and video files as links.
    193 				return wp.media.string.link( props );
    194 			}
    195 
    196 			html = wp.shortcode.string({
    197 				tag:     type,
    198 				attrs:   shortcode
    199 			});
    200 
    201 			return html;
    202 		},
    203 		/**
    204 		 * Create image markup, optionally with a link and/or wrapped in a caption shortcode,
    205 		 *  that is suitable for passing to the editor
    206 		 *
    207 		 * @param {Object} props Attachment details (align, link, size, etc).
    208 		 * @param {Object} attachment The attachment object, media version of Post.
    209 		 * @return {string}
    210 		 */
    211 		image: function( props, attachment ) {
    212 			var img = {},
    213 				options, classes, shortcode, html;
    214 
    215 			props.type = 'image';
    216 			props = wp.media.string.props( props, attachment );
    217 			classes = props.classes || [];
    218 
    219 			img.src = ! _.isUndefined( attachment ) ? attachment.url : props.url;
    220 			_.extend( img, _.pick( props, 'width', 'height', 'alt' ) );
    221 
    222 			// Only assign the align class to the image if we're not printing
    223 			// a caption, since the alignment is sent to the shortcode.
    224 			if ( props.align && ! props.caption ) {
    225 				classes.push( 'align' + props.align );
    226 			}
    227 
    228 			if ( props.size ) {
    229 				classes.push( 'size-' + props.size );
    230 			}
    231 
    232 			img['class'] = _.compact( classes ).join(' ');
    233 
    234 			// Generate `img` tag options.
    235 			options = {
    236 				tag:    'img',
    237 				attrs:  img,
    238 				single: true
    239 			};
    240 
    241 			// Generate the `a` element options, if they exist.
    242 			if ( props.linkUrl ) {
    243 				options = {
    244 					tag:   'a',
    245 					attrs: {
    246 						href: props.linkUrl
    247 					},
    248 					content: options
    249 				};
    250 			}
    251 
    252 			html = wp.html.string( options );
    253 
    254 			// Generate the caption shortcode.
    255 			if ( props.caption ) {
    256 				shortcode = {};
    257 
    258 				if ( img.width ) {
    259 					shortcode.width = img.width;
    260 				}
    261 
    262 				if ( props.captionId ) {
    263 					shortcode.id = props.captionId;
    264 				}
    265 
    266 				if ( props.align ) {
    267 					shortcode.align = 'align' + props.align;
    268 				}
    269 
    270 				html = wp.shortcode.string({
    271 					tag:     'caption',
    272 					attrs:   shortcode,
    273 					content: html + ' ' + props.caption
    274 				});
    275 			}
    276 
    277 			return html;
    278 		}
    279 	};
    280 
    281 	wp.media.embed = {
    282 		coerce : wp.media.coerce,
    283 
    284 		defaults : {
    285 			url : '',
    286 			width: '',
    287 			height: ''
    288 		},
    289 
    290 		edit : function( data, isURL ) {
    291 			var frame, props = {}, shortcode;
    292 
    293 			if ( isURL ) {
    294 				props.url = data.replace(/<[^>]+>/g, '');
    295 			} else {
    296 				shortcode = wp.shortcode.next( 'embed', data ).shortcode;
    297 
    298 				props = _.defaults( shortcode.attrs.named, this.defaults );
    299 				if ( shortcode.content ) {
    300 					props.url = shortcode.content;
    301 				}
    302 			}
    303 
    304 			frame = wp.media({
    305 				frame: 'post',
    306 				state: 'embed',
    307 				metadata: props
    308 			});
    309 
    310 			return frame;
    311 		},
    312 
    313 		shortcode : function( model ) {
    314 			var self = this, content;
    315 
    316 			_.each( this.defaults, function( value, key ) {
    317 				model[ key ] = self.coerce( model, key );
    318 
    319 				if ( value === model[ key ] ) {
    320 					delete model[ key ];
    321 				}
    322 			});
    323 
    324 			content = model.url;
    325 			delete model.url;
    326 
    327 			return new wp.shortcode({
    328 				tag: 'embed',
    329 				attrs: model,
    330 				content: content
    331 			});
    332 		}
    333 	};
    334 
    335 	/**
    336 	 * @class wp.media.collection
    337 	 *
    338 	 * @param {Object} attributes
    339 	 */
    340 	wp.media.collection = function(attributes) {
    341 		var collections = {};
    342 
    343 		return _.extend(/** @lends wp.media.collection.prototype */{
    344 			coerce : wp.media.coerce,
    345 			/**
    346 			 * Retrieve attachments based on the properties of the passed shortcode
    347 			 *
    348 			 * @param {wp.shortcode} shortcode An instance of wp.shortcode().
    349 			 * @return {wp.media.model.Attachments} A Backbone.Collection containing
    350 			 *                                      the media items belonging to a collection.
    351 			 *                                      The query[ this.tag ] property is a Backbone.Model
    352 			 *                                      containing the 'props' for the collection.
    353 			 */
    354 			attachments: function( shortcode ) {
    355 				var shortcodeString = shortcode.string(),
    356 					result = collections[ shortcodeString ],
    357 					attrs, args, query, others, self = this;
    358 
    359 				delete collections[ shortcodeString ];
    360 				if ( result ) {
    361 					return result;
    362 				}
    363 				// Fill the default shortcode attributes.
    364 				attrs = _.defaults( shortcode.attrs.named, this.defaults );
    365 				args  = _.pick( attrs, 'orderby', 'order' );
    366 
    367 				args.type    = this.type;
    368 				args.perPage = -1;
    369 
    370 				// Mark the `orderby` override attribute.
    371 				if ( undefined !== attrs.orderby ) {
    372 					attrs._orderByField = attrs.orderby;
    373 				}
    374 
    375 				if ( 'rand' === attrs.orderby ) {
    376 					attrs._orderbyRandom = true;
    377 				}
    378 
    379 				// Map the `orderby` attribute to the corresponding model property.
    380 				if ( ! attrs.orderby || /^menu_order(?: ID)?$/i.test( attrs.orderby ) ) {
    381 					args.orderby = 'menuOrder';
    382 				}
    383 
    384 				// Map the `ids` param to the correct query args.
    385 				if ( attrs.ids ) {
    386 					args.post__in = attrs.ids.split(',');
    387 					args.orderby  = 'post__in';
    388 				} else if ( attrs.include ) {
    389 					args.post__in = attrs.include.split(',');
    390 				}
    391 
    392 				if ( attrs.exclude ) {
    393 					args.post__not_in = attrs.exclude.split(',');
    394 				}
    395 
    396 				if ( ! args.post__in ) {
    397 					args.uploadedTo = attrs.id;
    398 				}
    399 
    400 				// Collect the attributes that were not included in `args`.
    401 				others = _.omit( attrs, 'id', 'ids', 'include', 'exclude', 'orderby', 'order' );
    402 
    403 				_.each( this.defaults, function( value, key ) {
    404 					others[ key ] = self.coerce( others, key );
    405 				});
    406 
    407 				query = wp.media.query( args );
    408 				query[ this.tag ] = new Backbone.Model( others );
    409 				return query;
    410 			},
    411 			/**
    412 			 * Triggered when clicking 'Insert {label}' or 'Update {label}'
    413 			 *
    414 			 * @param {wp.media.model.Attachments} attachments A Backbone.Collection containing
    415 			 *      the media items belonging to a collection.
    416 			 *      The query[ this.tag ] property is a Backbone.Model
    417 			 *          containing the 'props' for the collection.
    418 			 * @return {wp.shortcode}
    419 			 */
    420 			shortcode: function( attachments ) {
    421 				var props = attachments.props.toJSON(),
    422 					attrs = _.pick( props, 'orderby', 'order' ),
    423 					shortcode, clone;
    424 
    425 				if ( attachments.type ) {
    426 					attrs.type = attachments.type;
    427 					delete attachments.type;
    428 				}
    429 
    430 				if ( attachments[this.tag] ) {
    431 					_.extend( attrs, attachments[this.tag].toJSON() );
    432 				}
    433 
    434 				/*
    435 				 * Convert all gallery shortcodes to use the `ids` property.
    436 				 * Ignore `post__in` and `post__not_in`; the attachments in
    437 				 * the collection will already reflect those properties.
    438 				 */
    439 				attrs.ids = attachments.pluck('id');
    440 
    441 				// Copy the `uploadedTo` post ID.
    442 				if ( props.uploadedTo ) {
    443 					attrs.id = props.uploadedTo;
    444 				}
    445 				// Check if the gallery is randomly ordered.
    446 				delete attrs.orderby;
    447 
    448 				if ( attrs._orderbyRandom ) {
    449 					attrs.orderby = 'rand';
    450 				} else if ( attrs._orderByField && 'rand' !== attrs._orderByField ) {
    451 					attrs.orderby = attrs._orderByField;
    452 				}
    453 
    454 				delete attrs._orderbyRandom;
    455 				delete attrs._orderByField;
    456 
    457 				// If the `ids` attribute is set and `orderby` attribute
    458 				// is the default value, clear it for cleaner output.
    459 				if ( attrs.ids && 'post__in' === attrs.orderby ) {
    460 					delete attrs.orderby;
    461 				}
    462 
    463 				attrs = this.setDefaults( attrs );
    464 
    465 				shortcode = new wp.shortcode({
    466 					tag:    this.tag,
    467 					attrs:  attrs,
    468 					type:   'single'
    469 				});
    470 
    471 				// Use a cloned version of the gallery.
    472 				clone = new wp.media.model.Attachments( attachments.models, {
    473 					props: props
    474 				});
    475 				clone[ this.tag ] = attachments[ this.tag ];
    476 				collections[ shortcode.string() ] = clone;
    477 
    478 				return shortcode;
    479 			},
    480 			/**
    481 			 * Triggered when double-clicking a collection shortcode placeholder
    482 			 *   in the editor
    483 			 *
    484 			 * @param {string} content Content that is searched for possible
    485 			 *    shortcode markup matching the passed tag name,
    486 			 *
    487 			 * @this wp.media.{prop}
    488 			 *
    489 			 * @return {wp.media.view.MediaFrame.Select} A media workflow.
    490 			 */
    491 			edit: function( content ) {
    492 				var shortcode = wp.shortcode.next( this.tag, content ),
    493 					defaultPostId = this.defaults.id,
    494 					attachments, selection, state;
    495 
    496 				// Bail if we didn't match the shortcode or all of the content.
    497 				if ( ! shortcode || shortcode.content !== content ) {
    498 					return;
    499 				}
    500 
    501 				// Ignore the rest of the match object.
    502 				shortcode = shortcode.shortcode;
    503 
    504 				if ( _.isUndefined( shortcode.get('id') ) && ! _.isUndefined( defaultPostId ) ) {
    505 					shortcode.set( 'id', defaultPostId );
    506 				}
    507 
    508 				attachments = this.attachments( shortcode );
    509 
    510 				selection = new wp.media.model.Selection( attachments.models, {
    511 					props:    attachments.props.toJSON(),
    512 					multiple: true
    513 				});
    514 
    515 				selection[ this.tag ] = attachments[ this.tag ];
    516 
    517 				// Fetch the query's attachments, and then break ties from the
    518 				// query to allow for sorting.
    519 				selection.more().done( function() {
    520 					// Break ties with the query.
    521 					selection.props.set({ query: false });
    522 					selection.unmirror();
    523 					selection.props.unset('orderby');
    524 				});
    525 
    526 				// Destroy the previous gallery frame.
    527 				if ( this.frame ) {
    528 					this.frame.dispose();
    529 				}
    530 
    531 				if ( shortcode.attrs.named.type && 'video' === shortcode.attrs.named.type ) {
    532 					state = 'video-' + this.tag + '-edit';
    533 				} else {
    534 					state = this.tag + '-edit';
    535 				}
    536 
    537 				// Store the current frame.
    538 				this.frame = wp.media({
    539 					frame:     'post',
    540 					state:     state,
    541 					title:     this.editTitle,
    542 					editing:   true,
    543 					multiple:  true,
    544 					selection: selection
    545 				}).open();
    546 
    547 				return this.frame;
    548 			},
    549 
    550 			setDefaults: function( attrs ) {
    551 				var self = this;
    552 				// Remove default attributes from the shortcode.
    553 				_.each( this.defaults, function( value, key ) {
    554 					attrs[ key ] = self.coerce( attrs, key );
    555 					if ( value === attrs[ key ] ) {
    556 						delete attrs[ key ];
    557 					}
    558 				});
    559 
    560 				return attrs;
    561 			}
    562 		}, attributes );
    563 	};
    564 
    565 	wp.media._galleryDefaults = {
    566 		itemtag: 'dl',
    567 		icontag: 'dt',
    568 		captiontag: 'dd',
    569 		columns: '3',
    570 		link: 'post',
    571 		size: 'thumbnail',
    572 		order: 'ASC',
    573 		id: wp.media.view.settings.post && wp.media.view.settings.post.id,
    574 		orderby : 'menu_order ID'
    575 	};
    576 
    577 	if ( wp.media.view.settings.galleryDefaults ) {
    578 		wp.media.galleryDefaults = _.extend( {}, wp.media._galleryDefaults, wp.media.view.settings.galleryDefaults );
    579 	} else {
    580 		wp.media.galleryDefaults = wp.media._galleryDefaults;
    581 	}
    582 
    583 	wp.media.gallery = new wp.media.collection({
    584 		tag: 'gallery',
    585 		type : 'image',
    586 		editTitle : wp.media.view.l10n.editGalleryTitle,
    587 		defaults : wp.media.galleryDefaults,
    588 
    589 		setDefaults: function( attrs ) {
    590 			var self = this, changed = ! _.isEqual( wp.media.galleryDefaults, wp.media._galleryDefaults );
    591 			_.each( this.defaults, function( value, key ) {
    592 				attrs[ key ] = self.coerce( attrs, key );
    593 				if ( value === attrs[ key ] && ( ! changed || value === wp.media._galleryDefaults[ key ] ) ) {
    594 					delete attrs[ key ];
    595 				}
    596 			} );
    597 			return attrs;
    598 		}
    599 	});
    600 
    601 	/**
    602 	 * @namespace wp.media.featuredImage
    603 	 * @memberOf wp.media
    604 	 */
    605 	wp.media.featuredImage = {
    606 		/**
    607 		 * Get the featured image post ID
    608 		 *
    609 		 * @return {wp.media.view.settings.post.featuredImageId|number}
    610 		 */
    611 		get: function() {
    612 			return wp.media.view.settings.post.featuredImageId;
    613 		},
    614 		/**
    615 		 * Sets the featured image ID property and sets the HTML in the post meta box to the new featured image.
    616 		 *
    617 		 * @param {number} id The post ID of the featured image, or -1 to unset it.
    618 		 */
    619 		set: function( id ) {
    620 			var settings = wp.media.view.settings;
    621 
    622 			settings.post.featuredImageId = id;
    623 
    624 			wp.media.post( 'get-post-thumbnail-html', {
    625 				post_id:      settings.post.id,
    626 				thumbnail_id: settings.post.featuredImageId,
    627 				_wpnonce:     settings.post.nonce
    628 			}).done( function( html ) {
    629 				if ( '0' === html ) {
    630 					window.alert( wp.i18n.__( 'Could not set that as the thumbnail image. Try a different attachment.' ) );
    631 					return;
    632 				}
    633 				$( '.inside', '#postimagediv' ).html( html );
    634 			});
    635 		},
    636 		/**
    637 		 * Remove the featured image id, save the post thumbnail data and
    638 		 * set the HTML in the post meta box to no featured image.
    639 		 */
    640 		remove: function() {
    641 			wp.media.featuredImage.set( -1 );
    642 		},
    643 		/**
    644 		 * The Featured Image workflow
    645 		 *
    646 		 * @this wp.media.featuredImage
    647 		 *
    648 		 * @return {wp.media.view.MediaFrame.Select} A media workflow.
    649 		 */
    650 		frame: function() {
    651 			if ( this._frame ) {
    652 				wp.media.frame = this._frame;
    653 				return this._frame;
    654 			}
    655 
    656 			this._frame = wp.media({
    657 				state: 'featured-image',
    658 				states: [ new wp.media.controller.FeaturedImage() , new wp.media.controller.EditImage() ]
    659 			});
    660 
    661 			this._frame.on( 'toolbar:create:featured-image', function( toolbar ) {
    662 				/**
    663 				 * @this wp.media.view.MediaFrame.Select
    664 				 */
    665 				this.createSelectToolbar( toolbar, {
    666 					text: wp.media.view.l10n.setFeaturedImage
    667 				});
    668 			}, this._frame );
    669 
    670 			this._frame.on( 'content:render:edit-image', function() {
    671 				var selection = this.state('featured-image').get('selection'),
    672 					view = new wp.media.view.EditImage( { model: selection.single(), controller: this } ).render();
    673 
    674 				this.content.set( view );
    675 
    676 				// After bringing in the frame, load the actual editor via an Ajax call.
    677 				view.loadEditor();
    678 
    679 			}, this._frame );
    680 
    681 			this._frame.state('featured-image').on( 'select', this.select );
    682 			return this._frame;
    683 		},
    684 		/**
    685 		 * 'select' callback for Featured Image workflow, triggered when
    686 		 *  the 'Set Featured Image' button is clicked in the media modal.
    687 		 *
    688 		 * @this wp.media.controller.FeaturedImage
    689 		 */
    690 		select: function() {
    691 			var selection = this.get('selection').single();
    692 
    693 			if ( ! wp.media.view.settings.post.featuredImageId ) {
    694 				return;
    695 			}
    696 
    697 			wp.media.featuredImage.set( selection ? selection.id : -1 );
    698 		},
    699 		/**
    700 		 * Open the content media manager to the 'featured image' tab when
    701 		 * the post thumbnail is clicked.
    702 		 *
    703 		 * Update the featured image id when the 'remove' link is clicked.
    704 		 */
    705 		init: function() {
    706 			$('#postimagediv').on( 'click', '#set-post-thumbnail', function( event ) {
    707 				event.preventDefault();
    708 				// Stop propagation to prevent thickbox from activating.
    709 				event.stopPropagation();
    710 
    711 				wp.media.featuredImage.frame().open();
    712 			}).on( 'click', '#remove-post-thumbnail', function() {
    713 				wp.media.featuredImage.remove();
    714 				return false;
    715 			});
    716 		}
    717 	};
    718 
    719 	$( wp.media.featuredImage.init );
    720 
    721 	/** @namespace wp.media.editor */
    722 	wp.media.editor = {
    723 		/**
    724 		 * Send content to the editor
    725 		 *
    726 		 * @param {string} html Content to send to the editor
    727 		 */
    728 		insert: function( html ) {
    729 			var editor, wpActiveEditor,
    730 				hasTinymce = ! _.isUndefined( window.tinymce ),
    731 				hasQuicktags = ! _.isUndefined( window.QTags );
    732 
    733 			if ( this.activeEditor ) {
    734 				wpActiveEditor = window.wpActiveEditor = this.activeEditor;
    735 			} else {
    736 				wpActiveEditor = window.wpActiveEditor;
    737 			}
    738 
    739 			/*
    740 			 * Delegate to the global `send_to_editor` if it exists.
    741 			 * This attempts to play nice with any themes/plugins
    742 			 * that have overridden the insert functionality.
    743 			 */
    744 			if ( window.send_to_editor ) {
    745 				return window.send_to_editor.apply( this, arguments );
    746 			}
    747 
    748 			if ( ! wpActiveEditor ) {
    749 				if ( hasTinymce && tinymce.activeEditor ) {
    750 					editor = tinymce.activeEditor;
    751 					wpActiveEditor = window.wpActiveEditor = editor.id;
    752 				} else if ( ! hasQuicktags ) {
    753 					return false;
    754 				}
    755 			} else if ( hasTinymce ) {
    756 				editor = tinymce.get( wpActiveEditor );
    757 			}
    758 
    759 			if ( editor && ! editor.isHidden() ) {
    760 				editor.execCommand( 'mceInsertContent', false, html );
    761 			} else if ( hasQuicktags ) {
    762 				QTags.insertContent( html );
    763 			} else {
    764 				document.getElementById( wpActiveEditor ).value += html;
    765 			}
    766 
    767 			// If the old thickbox remove function exists, call it in case
    768 			// a theme/plugin overloaded it.
    769 			if ( window.tb_remove ) {
    770 				try { window.tb_remove(); } catch( e ) {}
    771 			}
    772 		},
    773 
    774 		/**
    775 		 * Setup 'workflow' and add to the 'workflows' cache. 'open' can
    776 		 *  subsequently be called upon it.
    777 		 *
    778 		 * @param {string} id A slug used to identify the workflow.
    779 		 * @param {Object} [options={}]
    780 		 *
    781 		 * @this wp.media.editor
    782 		 *
    783 		 * @return {wp.media.view.MediaFrame.Select} A media workflow.
    784 		 */
    785 		add: function( id, options ) {
    786 			var workflow = this.get( id );
    787 
    788 			// Only add once: if exists return existing.
    789 			if ( workflow ) {
    790 				return workflow;
    791 			}
    792 
    793 			workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
    794 				frame:    'post',
    795 				state:    'insert',
    796 				title:    wp.media.view.l10n.addMedia,
    797 				multiple: true
    798 			} ) );
    799 
    800 			workflow.on( 'insert', function( selection ) {
    801 				var state = workflow.state();
    802 
    803 				selection = selection || state.get('selection');
    804 
    805 				if ( ! selection ) {
    806 					return;
    807 				}
    808 
    809 				$.when.apply( $, selection.map( function( attachment ) {
    810 					var display = state.display( attachment ).toJSON();
    811 					/**
    812 					 * @this wp.media.editor
    813 					 */
    814 					return this.send.attachment( display, attachment.toJSON() );
    815 				}, this ) ).done( function() {
    816 					wp.media.editor.insert( _.toArray( arguments ).join('\n\n') );
    817 				});
    818 			}, this );
    819 
    820 			workflow.state('gallery-edit').on( 'update', function( selection ) {
    821 				/**
    822 				 * @this wp.media.editor
    823 				 */
    824 				this.insert( wp.media.gallery.shortcode( selection ).string() );
    825 			}, this );
    826 
    827 			workflow.state('playlist-edit').on( 'update', function( selection ) {
    828 				/**
    829 				 * @this wp.media.editor
    830 				 */
    831 				this.insert( wp.media.playlist.shortcode( selection ).string() );
    832 			}, this );
    833 
    834 			workflow.state('video-playlist-edit').on( 'update', function( selection ) {
    835 				/**
    836 				 * @this wp.media.editor
    837 				 */
    838 				this.insert( wp.media.playlist.shortcode( selection ).string() );
    839 			}, this );
    840 
    841 			workflow.state('embed').on( 'select', function() {
    842 				/**
    843 				 * @this wp.media.editor
    844 				 */
    845 				var state = workflow.state(),
    846 					type = state.get('type'),
    847 					embed = state.props.toJSON();
    848 
    849 				embed.url = embed.url || '';
    850 
    851 				if ( 'link' === type ) {
    852 					_.defaults( embed, {
    853 						linkText: embed.url,
    854 						linkUrl: embed.url
    855 					});
    856 
    857 					this.send.link( embed ).done( function( resp ) {
    858 						wp.media.editor.insert( resp );
    859 					});
    860 
    861 				} else if ( 'image' === type ) {
    862 					_.defaults( embed, {
    863 						title:   embed.url,
    864 						linkUrl: '',
    865 						align:   'none',
    866 						link:    'none'
    867 					});
    868 
    869 					if ( 'none' === embed.link ) {
    870 						embed.linkUrl = '';
    871 					} else if ( 'file' === embed.link ) {
    872 						embed.linkUrl = embed.url;
    873 					}
    874 
    875 					this.insert( wp.media.string.image( embed ) );
    876 				}
    877 			}, this );
    878 
    879 			workflow.state('featured-image').on( 'select', wp.media.featuredImage.select );
    880 			workflow.setState( workflow.options.state );
    881 			return workflow;
    882 		},
    883 		/**
    884 		 * Determines the proper current workflow id
    885 		 *
    886 		 * @param {string} [id=''] A slug used to identify the workflow.
    887 		 *
    888 		 * @return {wpActiveEditor|string|tinymce.activeEditor.id}
    889 		 */
    890 		id: function( id ) {
    891 			if ( id ) {
    892 				return id;
    893 			}
    894 
    895 			// If an empty `id` is provided, default to `wpActiveEditor`.
    896 			id = window.wpActiveEditor;
    897 
    898 			// If that doesn't work, fall back to `tinymce.activeEditor.id`.
    899 			if ( ! id && ! _.isUndefined( window.tinymce ) && tinymce.activeEditor ) {
    900 				id = tinymce.activeEditor.id;
    901 			}
    902 
    903 			// Last but not least, fall back to the empty string.
    904 			id = id || '';
    905 			return id;
    906 		},
    907 		/**
    908 		 * Return the workflow specified by id
    909 		 *
    910 		 * @param {string} id A slug used to identify the workflow.
    911 		 *
    912 		 * @this wp.media.editor
    913 		 *
    914 		 * @return {wp.media.view.MediaFrame} A media workflow.
    915 		 */
    916 		get: function( id ) {
    917 			id = this.id( id );
    918 			return workflows[ id ];
    919 		},
    920 		/**
    921 		 * Remove the workflow represented by id from the workflow cache
    922 		 *
    923 		 * @param {string} id A slug used to identify the workflow.
    924 		 *
    925 		 * @this wp.media.editor
    926 		 */
    927 		remove: function( id ) {
    928 			id = this.id( id );
    929 			delete workflows[ id ];
    930 		},
    931 		/** @namespace wp.media.editor.send */
    932 		send: {
    933 			/**
    934 			 * Called when sending an attachment to the editor
    935 			 *   from the medial modal.
    936 			 *
    937 			 * @param {Object} props Attachment details (align, link, size, etc).
    938 			 * @param {Object} attachment The attachment object, media version of Post.
    939 			 * @return {Promise}
    940 			 */
    941 			attachment: function( props, attachment ) {
    942 				var caption = attachment.caption,
    943 					options, html;
    944 
    945 				// If captions are disabled, clear the caption.
    946 				if ( ! wp.media.view.settings.captions ) {
    947 					delete attachment.caption;
    948 				}
    949 
    950 				props = wp.media.string.props( props, attachment );
    951 
    952 				options = {
    953 					id:           attachment.id,
    954 					post_content: attachment.description,
    955 					post_excerpt: caption
    956 				};
    957 
    958 				if ( props.linkUrl ) {
    959 					options.url = props.linkUrl;
    960 				}
    961 
    962 				if ( 'image' === attachment.type ) {
    963 					html = wp.media.string.image( props );
    964 
    965 					_.each({
    966 						align: 'align',
    967 						size:  'image-size',
    968 						alt:   'image_alt'
    969 					}, function( option, prop ) {
    970 						if ( props[ prop ] ) {
    971 							options[ option ] = props[ prop ];
    972 						}
    973 					});
    974 				} else if ( 'video' === attachment.type ) {
    975 					html = wp.media.string.video( props, attachment );
    976 				} else if ( 'audio' === attachment.type ) {
    977 					html = wp.media.string.audio( props, attachment );
    978 				} else {
    979 					html = wp.media.string.link( props );
    980 					options.post_title = props.title;
    981 				}
    982 
    983 				return wp.media.post( 'send-attachment-to-editor', {
    984 					nonce:      wp.media.view.settings.nonce.sendToEditor,
    985 					attachment: options,
    986 					html:       html,
    987 					post_id:    wp.media.view.settings.post.id
    988 				});
    989 			},
    990 			/**
    991 			 * Called when 'Insert From URL' source is not an image. Example: YouTube url.
    992 			 *
    993 			 * @param {Object} embed
    994 			 * @return {Promise}
    995 			 */
    996 			link: function( embed ) {
    997 				return wp.media.post( 'send-link-to-editor', {
    998 					nonce:     wp.media.view.settings.nonce.sendToEditor,
    999 					src:       embed.linkUrl,
   1000 					link_text: embed.linkText,
   1001 					html:      wp.media.string.link( embed ),
   1002 					post_id:   wp.media.view.settings.post.id
   1003 				});
   1004 			}
   1005 		},
   1006 		/**
   1007 		 * Open a workflow
   1008 		 *
   1009 		 * @param {string} [id=undefined] Optional. A slug used to identify the workflow.
   1010 		 * @param {Object} [options={}]
   1011 		 *
   1012 		 * @this wp.media.editor
   1013 		 *
   1014 		 * @return {wp.media.view.MediaFrame}
   1015 		 */
   1016 		open: function( id, options ) {
   1017 			var workflow;
   1018 
   1019 			options = options || {};
   1020 
   1021 			id = this.id( id );
   1022 			this.activeEditor = id;
   1023 
   1024 			workflow = this.get( id );
   1025 
   1026 			// Redo workflow if state has changed.
   1027 			if ( ! workflow || ( workflow.options && options.state !== workflow.options.state ) ) {
   1028 				workflow = this.add( id, options );
   1029 			}
   1030 
   1031 			wp.media.frame = workflow;
   1032 
   1033 			return workflow.open();
   1034 		},
   1035 
   1036 		/**
   1037 		 * Bind click event for .insert-media using event delegation
   1038 		 */
   1039 		init: function() {
   1040 			$(document.body)
   1041 				.on( 'click.add-media-button', '.insert-media', function( event ) {
   1042 					var elem = $( event.currentTarget ),
   1043 						editor = elem.data('editor'),
   1044 						options = {
   1045 							frame:    'post',
   1046 							state:    'insert',
   1047 							title:    wp.media.view.l10n.addMedia,
   1048 							multiple: true
   1049 						};
   1050 
   1051 					event.preventDefault();
   1052 
   1053 					if ( elem.hasClass( 'gallery' ) ) {
   1054 						options.state = 'gallery';
   1055 						options.title = wp.media.view.l10n.createGalleryTitle;
   1056 					}
   1057 
   1058 					wp.media.editor.open( editor, options );
   1059 				});
   1060 
   1061 			// Initialize and render the Editor drag-and-drop uploader.
   1062 			new wp.media.view.EditorUploader().render();
   1063 		}
   1064 	};
   1065 
   1066 	_.bindAll( wp.media.editor, 'open' );
   1067 	$( wp.media.editor.init );
   1068 }(jQuery, _));