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