1 /* global getUserSetting, tinymce, QTags, wpActiveEditor */
3 // WordPress, TinyMCE, and Media
4 // -----------------------------
7 * Stores the editors' `wp.media.controller.Frame` instances.
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.
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
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 ] ) {
27 } else if ( 'false' === attrs[ key ] ) {
39 * Joins the `props` and `attachment` objects,
40 * outputting the proper object format based on the
43 * @global wp.media.view.settings
44 * @global getUserSetting()
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
50 props: function( props, attachment ) {
51 var link, linkUrl, size, sizes, fallbacks,
52 defaultProps = wp.media.view.settings.defaultProps;
54 // Final fallbacks run after all processing has been completed.
55 fallbacks = function( props ) {
56 // Generate alt fallbacks and strip tags.
57 if ( 'image' === props.type && ! props.alt ) {
58 props.alt = props.caption || props.title || '';
59 props.alt = props.alt.replace( /<\/?[^>]+>/g, '' );
60 props.alt = props.alt.replace( /[\r\n]+/g, ' ' );
66 props = props ? _.clone( props ) : {};
68 if ( attachment && attachment.type ) {
69 props.type = attachment.type;
72 if ( 'image' === props.type ) {
73 props = _.defaults( props || {}, {
74 align: defaultProps.align || getUserSetting( 'align', 'none' ),
75 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ),
81 // All attachment-specific settings follow.
83 return fallbacks( props );
86 props.title = props.title || attachment.title;
88 link = props.link || defaultProps.link || getUserSetting( 'urlbutton', 'file' );
89 if ( 'file' === link || 'embed' === link ) {
90 linkUrl = attachment.url;
91 } else if ( 'post' === link ) {
92 linkUrl = attachment.link;
93 } else if ( 'custom' === link ) {
94 linkUrl = props.linkUrl;
96 props.linkUrl = linkUrl || '';
98 // Format properties for images.
99 if ( 'image' === attachment.type ) {
100 props.classes.push( 'wp-image-' + attachment.id );
102 sizes = attachment.sizes;
103 size = sizes && sizes[ props.size ] ? sizes[ props.size ] : attachment;
105 _.extend( props, _.pick( attachment, 'align', 'caption', 'alt' ), {
109 captionId: 'attachment_' + attachment.id
111 } else if ( 'video' === attachment.type || 'audio' === attachment.type ) {
112 _.extend( props, _.pick( attachment, 'title', 'type', 'icon', 'mime' ) );
113 // Format properties for non-images.
115 props.title = props.title || attachment.filename;
116 props.rel = props.rel || 'attachment wp-att-' + attachment.id;
119 return fallbacks( props );
122 * Create link markup that is suitable for passing to the editor
124 * @global wp.html.string
126 * @param {Object} props Attachment details (align, link, size, etc).
127 * @param {Object} attachment The attachment object, media version of Post.
128 * @returns {string} The link markup
130 link: function( props, attachment ) {
133 props = wp.media.string.props( props, attachment );
137 content: props.title,
144 options.attrs.rel = props.rel;
147 return wp.html.string( options );
150 * Create an Audio shortcode string that is suitable for passing to the editor
152 * @param {Object} props Attachment details (align, link, size, etc).
153 * @param {Object} attachment The attachment object, media version of Post.
154 * @returns {string} The audio shortcode
156 audio: function( props, attachment ) {
157 return wp.media.string._audioVideo( 'audio', props, attachment );
160 * Create a Video shortcode string that is suitable for passing to the editor
162 * @param {Object} props Attachment details (align, link, size, etc).
163 * @param {Object} attachment The attachment object, media version of Post.
164 * @returns {string} The video shortcode
166 video: function( props, attachment ) {
167 return wp.media.string._audioVideo( 'video', props, attachment );
170 * Helper function to create a media shortcode string
174 * @global wp.shortcode
175 * @global wp.media.view.settings
177 * @param {string} type The shortcode tag name: 'audio' or 'video'.
178 * @param {Object} props Attachment details (align, link, size, etc).
179 * @param {Object} attachment The attachment object, media version of Post.
180 * @returns {string} The media shortcode
182 _audioVideo: function( type, props, attachment ) {
183 var shortcode, html, extension;
185 props = wp.media.string.props( props, attachment );
186 if ( props.link !== 'embed' )
187 return wp.media.string.link( props );
191 if ( 'video' === type ) {
192 if ( attachment.image && -1 === attachment.image.src.indexOf( attachment.icon ) ) {
193 shortcode.poster = attachment.image.src;
196 if ( attachment.width ) {
197 shortcode.width = attachment.width;
200 if ( attachment.height ) {
201 shortcode.height = attachment.height;
205 extension = attachment.filename.split('.').pop();
207 if ( _.contains( wp.media.view.settings.embedExts, extension ) ) {
208 shortcode[extension] = attachment.url;
210 // Render unsupported audio and video files as links.
211 return wp.media.string.link( props );
214 html = wp.shortcode.string({
222 * Create image markup, optionally with a link and/or wrapped in a caption shortcode,
223 * that is suitable for passing to the editor
226 * @global wp.shortcode
228 * @param {Object} props Attachment details (align, link, size, etc).
229 * @param {Object} attachment The attachment object, media version of Post.
232 image: function( props, attachment ) {
234 options, classes, shortcode, html;
236 props = wp.media.string.props( props, attachment );
237 classes = props.classes || [];
239 img.src = ! _.isUndefined( attachment ) ? attachment.url : props.url;
240 _.extend( img, _.pick( props, 'width', 'height', 'alt' ) );
242 // Only assign the align class to the image if we're not printing
243 // a caption, since the alignment is sent to the shortcode.
244 if ( props.align && ! props.caption ) {
245 classes.push( 'align' + props.align );
249 classes.push( 'size-' + props.size );
252 img['class'] = _.compact( classes ).join(' ');
254 // Generate `img` tag options.
261 // Generate the `a` element options, if they exist.
262 if ( props.linkUrl ) {
272 html = wp.html.string( options );
274 // Generate the caption shortcode.
275 if ( props.caption ) {
279 shortcode.width = img.width;
282 if ( props.captionId ) {
283 shortcode.id = props.captionId;
287 shortcode.align = 'align' + props.align;
290 html = wp.shortcode.string({
293 content: html + ' ' + props.caption
301 wp.media.collection = function(attributes) {
302 var collections = {};
304 return _.extend( attributes, {
305 coerce : wp.media.coerce,
307 * Retrieve attachments based on the properties of the passed shortcode
309 * @global wp.media.query
311 * @param {wp.shortcode} shortcode An instance of wp.shortcode().
312 * @returns {wp.media.model.Attachments} A Backbone.Collection containing
313 * the media items belonging to a collection.
314 * The query[ this.tag ] property is a Backbone.Model
315 * containing the 'props' for the collection.
317 attachments: function( shortcode ) {
318 var shortcodeString = shortcode.string(),
319 result = collections[ shortcodeString ],
320 attrs, args, query, others, self = this;
322 delete collections[ shortcodeString ];
326 // Fill the default shortcode attributes.
327 attrs = _.defaults( shortcode.attrs.named, this.defaults );
328 args = _.pick( attrs, 'orderby', 'order' );
330 args.type = this.type;
333 // Mark the `orderby` override attribute.
334 if ( undefined !== attrs.orderby ) {
335 attrs._orderByField = attrs.orderby;
338 if ( 'rand' === attrs.orderby ) {
339 attrs._orderbyRandom = true;
342 // Map the `orderby` attribute to the corresponding model property.
343 if ( ! attrs.orderby || /^menu_order(?: ID)?$/i.test( attrs.orderby ) ) {
344 args.orderby = 'menuOrder';
347 // Map the `ids` param to the correct query args.
349 args.post__in = attrs.ids.split(',');
350 args.orderby = 'post__in';
351 } else if ( attrs.include ) {
352 args.post__in = attrs.include.split(',');
355 if ( attrs.exclude ) {
356 args.post__not_in = attrs.exclude.split(',');
359 if ( ! args.post__in ) {
360 args.uploadedTo = attrs.id;
363 // Collect the attributes that were not included in `args`.
364 others = _.omit( attrs, 'id', 'ids', 'include', 'exclude', 'orderby', 'order' );
366 _.each( this.defaults, function( value, key ) {
367 others[ key ] = self.coerce( others, key );
370 query = wp.media.query( args );
371 query[ this.tag ] = new Backbone.Model( others );
375 * Triggered when clicking 'Insert {label}' or 'Update {label}'
377 * @global wp.shortcode
378 * @global wp.media.model.Attachments
380 * @param {wp.media.model.Attachments} attachments A Backbone.Collection containing
381 * the media items belonging to a collection.
382 * The query[ this.tag ] property is a Backbone.Model
383 * containing the 'props' for the collection.
384 * @returns {wp.shortcode}
386 shortcode: function( attachments ) {
387 var props = attachments.props.toJSON(),
388 attrs = _.pick( props, 'orderby', 'order' ),
389 shortcode, clone, self = this;
391 if ( attachments.type ) {
392 attrs.type = attachments.type;
393 delete attachments.type;
396 if ( attachments[this.tag] ) {
397 _.extend( attrs, attachments[this.tag].toJSON() );
400 // Convert all gallery shortcodes to use the `ids` property.
401 // Ignore `post__in` and `post__not_in`; the attachments in
402 // the collection will already reflect those properties.
403 attrs.ids = attachments.pluck('id');
405 // Copy the `uploadedTo` post ID.
406 if ( props.uploadedTo ) {
407 attrs.id = props.uploadedTo;
409 // Check if the gallery is randomly ordered.
410 delete attrs.orderby;
412 if ( attrs._orderbyRandom ) {
413 attrs.orderby = 'rand';
414 } else if ( attrs._orderByField && attrs._orderByField != 'rand' ) {
415 attrs.orderby = attrs._orderByField;
418 delete attrs._orderbyRandom;
419 delete attrs._orderByField;
421 // If the `ids` attribute is set and `orderby` attribute
422 // is the default value, clear it for cleaner output.
423 if ( attrs.ids && 'post__in' === attrs.orderby ) {
424 delete attrs.orderby;
427 // Remove default attributes from the shortcode.
428 _.each( this.defaults, function( value, key ) {
429 attrs[ key ] = self.coerce( attrs, key );
430 if ( value === attrs[ key ] ) {
435 shortcode = new wp.shortcode({
441 // Use a cloned version of the gallery.
442 clone = new wp.media.model.Attachments( attachments.models, {
445 clone[ this.tag ] = attachments[ this.tag ];
446 collections[ shortcode.string() ] = clone;
451 * Triggered when double-clicking a collection shortcode placeholder
454 * @global wp.shortcode
455 * @global wp.media.model.Selection
456 * @global wp.media.view.l10n
458 * @param {string} content Content that is searched for possible
459 * shortcode markup matching the passed tag name,
461 * @this wp.media.{prop}
463 * @returns {wp.media.view.MediaFrame.Select} A media workflow.
465 edit: function( content ) {
466 var shortcode = wp.shortcode.next( this.tag, content ),
467 defaultPostId = this.defaults.id,
468 attachments, selection, state;
470 // Bail if we didn't match the shortcode or all of the content.
471 if ( ! shortcode || shortcode.content !== content ) {
475 // Ignore the rest of the match object.
476 shortcode = shortcode.shortcode;
478 if ( _.isUndefined( shortcode.get('id') ) && ! _.isUndefined( defaultPostId ) ) {
479 shortcode.set( 'id', defaultPostId );
482 attachments = this.attachments( shortcode );
484 selection = new wp.media.model.Selection( attachments.models, {
485 props: attachments.props.toJSON(),
489 selection[ this.tag ] = attachments[ this.tag ];
491 // Fetch the query's attachments, and then break ties from the
492 // query to allow for sorting.
493 selection.more().done( function() {
494 // Break ties with the query.
495 selection.props.set({ query: false });
496 selection.unmirror();
497 selection.props.unset('orderby');
500 // Destroy the previous gallery frame.
502 this.frame.dispose();
505 if ( shortcode.attrs.named.type && 'video' === shortcode.attrs.named.type ) {
506 state = 'video-' + this.tag + '-edit';
508 state = this.tag + '-edit';
511 // Store the current frame.
512 this.frame = wp.media({
515 title: this.editTitle,
526 wp.media.gallery = new wp.media.collection({
529 editTitle : wp.media.view.l10n.editGalleryTitle,
538 id: wp.media.view.settings.post && wp.media.view.settings.post.id,
539 orderby : 'menu_order ID'
544 * wp.media.featuredImage
547 wp.media.featuredImage = {
549 * Get the featured image post ID
551 * @global wp.media.view.settings
553 * @returns {wp.media.view.settings.post.featuredImageId|number}
556 return wp.media.view.settings.post.featuredImageId;
559 * Set the featured image id, save the post thumbnail data and
560 * set the HTML in the post meta box to the new featured image.
562 * @global wp.media.view.settings
563 * @global wp.media.post
565 * @param {number} id The post ID of the featured image, or -1 to unset it.
567 set: function( id ) {
568 var settings = wp.media.view.settings;
570 settings.post.featuredImageId = id;
572 wp.media.post( 'set-post-thumbnail', {
574 post_id: settings.post.id,
575 thumbnail_id: settings.post.featuredImageId,
576 _wpnonce: settings.post.nonce
577 }).done( function( html ) {
578 $( '.inside', '#postimagediv' ).html( html );
582 * The Featured Image workflow
584 * @global wp.media.controller.FeaturedImage
585 * @global wp.media.view.l10n
587 * @this wp.media.featuredImage
589 * @returns {wp.media.view.MediaFrame.Select} A media workflow.
596 this._frame = wp.media({
597 state: 'featured-image',
598 states: [ new wp.media.controller.FeaturedImage() , new wp.media.controller.EditImage() ]
601 this._frame.on( 'toolbar:create:featured-image', function( toolbar ) {
603 * @this wp.media.view.MediaFrame.Select
605 this.createSelectToolbar( toolbar, {
606 text: wp.media.view.l10n.setFeaturedImage
610 this._frame.on( 'content:render:edit-image', function() {
611 var selection = this.state('featured-image').get('selection'),
612 view = new wp.media.view.EditImage( { model: selection.single(), controller: this } ).render();
614 this.content.set( view );
616 // after bringing in the frame, load the actual editor via an ajax call
621 this._frame.state('featured-image').on( 'select', this.select );
625 * 'select' callback for Featured Image workflow, triggered when
626 * the 'Set Featured Image' button is clicked in the media modal.
628 * @global wp.media.view.settings
630 * @this wp.media.controller.FeaturedImage
633 var selection = this.get('selection').single();
635 if ( ! wp.media.view.settings.post.featuredImageId ) {
639 wp.media.featuredImage.set( selection ? selection.id : -1 );
642 * Open the content media manager to the 'featured image' tab when
643 * the post thumbnail is clicked.
645 * Update the featured image id when the 'remove' link is clicked.
647 * @global wp.media.view.settings
650 $('#postimagediv').on( 'click', '#set-post-thumbnail', function( event ) {
651 event.preventDefault();
652 // Stop propagation to prevent thickbox from activating.
653 event.stopPropagation();
655 wp.media.featuredImage.frame().open();
656 }).on( 'click', '#remove-post-thumbnail', function() {
657 wp.media.view.settings.post.featuredImageId = -1;
662 $( wp.media.featuredImage.init );
670 * Send content to the editor
674 * @global wpActiveEditor
675 * @global tb_remove() - Possibly overloaded by legacy plugins
677 * @param {string} html Content to send to the editor
679 insert: function( html ) {
681 hasTinymce = ! _.isUndefined( window.tinymce ),
682 hasQuicktags = ! _.isUndefined( window.QTags ),
683 wpActiveEditor = window.wpActiveEditor;
685 // Delegate to the global `send_to_editor` if it exists.
686 // This attempts to play nice with any themes/plugins that have
687 // overridden the insert functionality.
688 if ( window.send_to_editor ) {
689 return window.send_to_editor.apply( this, arguments );
692 if ( ! wpActiveEditor ) {
693 if ( hasTinymce && tinymce.activeEditor ) {
694 editor = tinymce.activeEditor;
695 wpActiveEditor = window.wpActiveEditor = editor.id;
696 } else if ( ! hasQuicktags ) {
699 } else if ( hasTinymce ) {
700 editor = tinymce.get( wpActiveEditor );
703 if ( editor && ! editor.isHidden() ) {
704 editor.execCommand( 'mceInsertContent', false, html );
705 } else if ( hasQuicktags ) {
706 QTags.insertContent( html );
708 document.getElementById( wpActiveEditor ).value += html;
711 // If the old thickbox remove function exists, call it in case
712 // a theme/plugin overloaded it.
713 if ( window.tb_remove ) {
714 try { window.tb_remove(); } catch( e ) {}
719 * Setup 'workflow' and add to the 'workflows' cache. 'open' can
720 * subsequently be called upon it.
722 * @global wp.media.view.l10n
724 * @param {string} id A slug used to identify the workflow.
725 * @param {Object} [options={}]
727 * @this wp.media.editor
729 * @returns {wp.media.view.MediaFrame.Select} A media workflow.
731 add: function( id, options ) {
732 var workflow = this.get( id );
734 // only add once: if exists return existing
739 workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
742 title: wp.media.view.l10n.addMedia,
746 workflow.on( 'insert', function( selection ) {
747 var state = workflow.state();
749 selection = selection || state.get('selection');
754 $.when.apply( $, selection.map( function( attachment ) {
755 var display = state.display( attachment ).toJSON();
757 * @this wp.media.editor
759 return this.send.attachment( display, attachment.toJSON() );
760 }, this ) ).done( function() {
761 wp.media.editor.insert( _.toArray( arguments ).join('\n\n') );
765 workflow.state('gallery-edit').on( 'update', function( selection ) {
767 * @this wp.media.editor
769 this.insert( wp.media.gallery.shortcode( selection ).string() );
772 workflow.state('playlist-edit').on( 'update', function( selection ) {
774 * @this wp.media.editor
776 this.insert( wp.media.playlist.shortcode( selection ).string() );
779 workflow.state('video-playlist-edit').on( 'update', function( selection ) {
781 * @this wp.media.editor
783 this.insert( wp.media.playlist.shortcode( selection ).string() );
786 workflow.state('embed').on( 'select', function() {
788 * @this wp.media.editor
790 var state = workflow.state(),
791 type = state.get('type'),
792 embed = state.props.toJSON();
794 embed.url = embed.url || '';
796 if ( 'link' === type ) {
802 this.send.link( embed ).done( function( resp ) {
803 wp.media.editor.insert( resp );
806 } else if ( 'image' === type ) {
814 if ( 'none' === embed.link ) {
816 } else if ( 'file' === embed.link ) {
817 embed.linkUrl = embed.url;
820 this.insert( wp.media.string.image( embed ) );
824 workflow.state('featured-image').on( 'select', wp.media.featuredImage.select );
825 workflow.setState( workflow.options.state );
829 * Determines the proper current workflow id
831 * @global wpActiveEditor
834 * @param {string} [id=''] A slug used to identify the workflow.
836 * @returns {wpActiveEditor|string|tinymce.activeEditor.id}
843 // If an empty `id` is provided, default to `wpActiveEditor`.
846 // If that doesn't work, fall back to `tinymce.activeEditor.id`.
847 if ( ! id && ! _.isUndefined( window.tinymce ) && tinymce.activeEditor ) {
848 id = tinymce.activeEditor.id;
851 // Last but not least, fall back to the empty string.
856 * Return the workflow specified by id
858 * @param {string} id A slug used to identify the workflow.
860 * @this wp.media.editor
862 * @returns {wp.media.view.MediaFrame} A media workflow.
864 get: function( id ) {
866 return workflows[ id ];
869 * Remove the workflow represented by id from the workflow cache
871 * @param {string} id A slug used to identify the workflow.
873 * @this wp.media.editor
875 remove: function( id ) {
877 delete workflows[ id ];
884 * Called when sending an attachment to the editor
885 * from the medial modal.
887 * @global wp.media.view.settings
888 * @global wp.media.post
890 * @param {Object} props Attachment details (align, link, size, etc).
891 * @param {Object} attachment The attachment object, media version of Post.
894 attachment: function( props, attachment ) {
895 var caption = attachment.caption,
898 // If captions are disabled, clear the caption.
899 if ( ! wp.media.view.settings.captions ) {
900 delete attachment.caption;
903 props = wp.media.string.props( props, attachment );
907 post_content: attachment.description,
908 post_excerpt: caption
911 if ( props.linkUrl ) {
912 options.url = props.linkUrl;
915 if ( 'image' === attachment.type ) {
916 html = wp.media.string.image( props );
922 }, function( option, prop ) {
924 options[ option ] = props[ prop ];
926 } else if ( 'video' === attachment.type ) {
927 html = wp.media.string.video( props, attachment );
928 } else if ( 'audio' === attachment.type ) {
929 html = wp.media.string.audio( props, attachment );
931 html = wp.media.string.link( props );
932 options.post_title = props.title;
935 return wp.media.post( 'send-attachment-to-editor', {
936 nonce: wp.media.view.settings.nonce.sendToEditor,
939 post_id: wp.media.view.settings.post.id
943 * Called when 'Insert From URL' source is not an image. Example: YouTube url.
945 * @global wp.media.view.settings
947 * @param {Object} embed
950 link: function( embed ) {
951 return wp.media.post( 'send-link-to-editor', {
952 nonce: wp.media.view.settings.nonce.sendToEditor,
955 html: wp.media.string.link( embed ),
956 post_id: wp.media.view.settings.post.id
963 * @param {string} [id=undefined] Optional. A slug used to identify the workflow.
964 * @param {Object} [options={}]
966 * @this wp.media.editor
968 * @returns {wp.media.view.MediaFrame}
970 open: function( id, options ) {
973 options = options || {};
977 // Save a bookmark of the caret position in IE.
978 if ( ! _.isUndefined( window.tinymce ) ) {
979 editor = tinymce.get( id );
981 if ( tinymce.isIE && editor && ! editor.isHidden() ) {
983 editor.windowManager.insertimagebookmark = editor.selection.getBookmark();
987 workflow = this.get( id );
989 // Redo workflow if state has changed
990 if ( ! workflow || ( workflow.options && options.state !== workflow.options.state ) ) {
991 workflow = this.add( id, options );
994 return workflow.open();
998 * Bind click event for .insert-media using event delegation
1000 * @global wp.media.view.l10n
1004 .on( 'click', '.insert-media', function( event ) {
1005 var elem = $( event.currentTarget ),
1006 editor = elem.data('editor'),
1010 title: wp.media.view.l10n.addMedia,
1014 event.preventDefault();
1016 // Remove focus from the `.insert-media` button.
1017 // Prevents Opera from showing the outline of the button
1020 // See: http://core.trac.wordpress.org/ticket/22445
1023 if ( elem.hasClass( 'gallery' ) ) {
1024 options.state = 'gallery';
1025 options.title = wp.media.view.l10n.createGalleryTitle;
1028 wp.media.editor.open( editor, options );
1031 // Initialize and render the Editor drag-and-drop uploader.
1032 new wp.media.view.EditorUploader().render();
1036 _.bindAll( wp.media.editor, 'open' );
1037 $( wp.media.editor.init );