1 /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */
5 isTouchDevice = ( 'ontouchend' in document );
7 // Link any localized strings.
8 l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
11 media.view.settings = l10n.settings || {};
14 // Copy the `post` setting over to the model settings.
15 media.model.settings.post = media.view.settings.post;
17 // Check if the browser supports CSS 3.0 transitions
18 $.support.transition = (function(){
19 var style = document.documentElement.style,
21 WebkitTransition: 'webkitTransitionEnd',
22 MozTransition: 'transitionend',
23 OTransition: 'oTransitionEnd otransitionend',
24 transition: 'transitionend'
27 transition = _.find( _.keys( transitions ), function( transition ) {
28 return ! _.isUndefined( style[ transition ] );
31 return transition && {
32 end: transitions[ transition ]
37 * A shared event bus used to provide events into
38 * the media workflows that 3rd-party devs can use to hook
41 media.events = _.extend( {}, Backbone.Events );
44 * Makes it easier to bind events using transitions.
46 * @param {string} selector
47 * @param {Number} sensitivity
50 media.transition = function( selector, sensitivity ) {
51 var deferred = $.Deferred();
53 sensitivity = sensitivity || 2000;
55 if ( $.support.transition ) {
56 if ( ! (selector instanceof $) ) {
57 selector = $( selector );
60 // Resolve the deferred when the first element finishes animating.
61 selector.first().one( $.support.transition.end, deferred.resolve );
63 // Just in case the event doesn't trigger, fire a callback.
64 _.delay( deferred.resolve, sensitivity );
66 // Otherwise, execute on the spot.
71 return deferred.promise();
75 * ========================================================================
77 * ========================================================================
81 * wp.media.controller.Region
84 * @augments Backbone.Model
86 * @param {Object} [options={}]
88 media.controller.Region = function( options ) {
89 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
92 // Use Backbone's self-propagating `extend` inheritance method.
93 media.controller.Region.extend = Backbone.Model.extend;
95 _.extend( media.controller.Region.prototype, {
99 * @param {string} mode
101 * @fires this.view#{this.id}:activate:{this._mode}
102 * @fires this.view#{this.id}:activate
103 * @fires this.view#{this.id}:deactivate:{this._mode}
104 * @fires this.view#{this.id}:deactivate
106 * @returns {wp.media.controller.Region} Returns itself to allow chaining.
108 mode: function( mode ) {
112 // Bail if we're trying to change to the current mode.
113 if ( mode === this._mode ) {
118 * Region mode deactivation event.
120 * @event this.view#{this.id}:deactivate:{this._mode}
121 * @event this.view#{this.id}:deactivate
123 this.trigger('deactivate');
129 * Region mode activation event.
131 * @event this.view#{this.id}:activate:{this._mode}
132 * @event this.view#{this.id}:activate
134 this.trigger('activate');
140 * @param {string} mode
142 * @fires this.view#{this.id}:create:{this._mode}
143 * @fires this.view#{this.id}:create
144 * @fires this.view#{this.id}:render:{this._mode}
145 * @fires this.view#{this.id}:render
147 * @returns {wp.media.controller.Region} Returns itself to allow chaining
149 render: function( mode ) {
150 // If the mode isn't active, activate it.
151 if ( mode && mode !== this._mode ) {
152 return this.mode( mode );
155 var set = { view: null },
159 * Create region view event.
161 * Region view creation takes place in an event callback on the frame.
163 * @event this.view#{this.id}:create:{this._mode}
164 * @event this.view#{this.id}:create
166 this.trigger( 'create', set );
170 * Render region view event.
172 * Region view creation takes place in an event callback on the frame.
174 * @event this.view#{this.id}:create:{this._mode}
175 * @event this.view#{this.id}:create
177 this.trigger( 'render', view );
185 * Get the region's view.
187 * @returns {wp.media.View}
190 return this.view.views.first( this.selector );
194 * Set the region's view as a subview of the frame.
196 * @param {Array|Object} views
197 * @param {Object} [options={}]
198 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining
200 set: function( views, options ) {
204 return this.view.views.set( this.selector, views, options );
208 * Trigger regional view events on the frame.
210 * @param {string} event
211 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining.
213 trigger: function( event ) {
216 if ( ! this._mode ) {
220 args = _.toArray( arguments );
221 base = this.id + ':' + event;
223 // Trigger `{this.id}:{event}:{this._mode}` event on the frame.
224 args[0] = base + ':' + this._mode;
225 this.view.trigger.apply( this.view, args );
227 // Trigger `{this.id}:{event}` event on the frame.
229 this.view.trigger.apply( this.view, args );
235 * wp.media.controller.StateMachine
238 * @augments Backbone.Model
240 * @mixes Backbone.Events
242 * @param {Array} states
244 media.controller.StateMachine = function( states ) {
245 this.states = new Backbone.Collection( states );
248 // Use Backbone's self-propagating `extend` inheritance method.
249 media.controller.StateMachine.extend = Backbone.Model.extend;
251 _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
255 * If no `id` is provided, returns the active state.
257 * Implicitly creates states.
259 * Ensure that the `states` collection exists so the `StateMachine`
260 * can be used as a mixin.
263 * @returns {wp.media.controller.State} Returns a State model
264 * from the StateMachine collection
266 state: function( id ) {
267 this.states = this.states || new Backbone.Collection();
269 // Default to the active state.
270 id = id || this._state;
272 if ( id && ! this.states.get( id ) ) {
273 this.states.add({ id: id });
275 return this.states.get( id );
279 * Sets the active state.
281 * Bail if we're trying to select the current state, if we haven't
282 * created the `states` collection, or are trying to select a state
283 * that does not exist.
287 * @fires wp.media.controller.State#deactivate
288 * @fires wp.media.controller.State#activate
290 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining
292 setState: function( id ) {
293 var previous = this.state();
295 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) {
300 previous.trigger('deactivate');
301 this._lastState = previous.id;
305 this.state().trigger('activate');
311 * Returns the previous active state.
313 * Call the `state()` method with no parameters to retrieve the current
316 * @returns {wp.media.controller.State} Returns a State model
317 * from the StateMachine collection
319 lastState: function() {
320 if ( this._lastState ) {
321 return this.state( this._lastState );
326 // Map all event binding and triggering on a StateMachine to its `states` collection.
327 _.each([ 'on', 'off', 'trigger' ], function( method ) {
329 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining.
331 media.controller.StateMachine.prototype[ method ] = function() {
332 // Ensure that the `states` collection exists so the `StateMachine`
333 // can be used as a mixin.
334 this.states = this.states || new Backbone.Collection();
335 // Forward the method to the `states` collection.
336 this.states[ method ].apply( this.states, arguments );
342 * wp.media.controller.State
344 * A state is a step in a workflow that when set will trigger the controllers
345 * for the regions to be updated as specified in the frame. This is the base
346 * class that the various states used in wp.media extend.
349 * @augments Backbone.Model
351 media.controller.State = Backbone.Model.extend({
352 constructor: function() {
353 this.on( 'activate', this._preActivate, this );
354 this.on( 'activate', this.activate, this );
355 this.on( 'activate', this._postActivate, this );
356 this.on( 'deactivate', this._deactivate, this );
357 this.on( 'deactivate', this.deactivate, this );
358 this.on( 'reset', this.reset, this );
359 this.on( 'ready', this._ready, this );
360 this.on( 'ready', this.ready, this );
362 * Call parent constructor with passed arguments
364 Backbone.Model.apply( this, arguments );
365 this.on( 'change:menu', this._updateMenu, this );
370 ready: function() {},
374 activate: function() {},
378 deactivate: function() {},
382 reset: function() {},
392 _preActivate: function() {
398 _postActivate: function() {
399 this.on( 'change:menu', this._menu, this );
400 this.on( 'change:titleMode', this._title, this );
401 this.on( 'change:content', this._content, this );
402 this.on( 'change:toolbar', this._toolbar, this );
404 this.frame.on( 'title:render:default', this._renderTitle, this );
415 _deactivate: function() {
418 this.frame.off( 'title:render:default', this._renderTitle, this );
420 this.off( 'change:menu', this._menu, this );
421 this.off( 'change:titleMode', this._title, this );
422 this.off( 'change:content', this._content, this );
423 this.off( 'change:toolbar', this._toolbar, this );
429 this.frame.title.render( this.get('titleMode') || 'default' );
434 _renderTitle: function( view ) {
435 view.$el.text( this.get('title') || '' );
440 _router: function() {
441 var router = this.frame.router,
442 mode = this.get('router'),
445 this.frame.$el.toggleClass( 'hide-router', ! mode );
450 this.frame.router.render( mode );
453 if ( view && view.select ) {
454 view.select( this.frame.content.mode() );
461 var menu = this.frame.menu,
462 mode = this.get('menu'),
465 this.frame.$el.toggleClass( 'hide-menu', ! mode );
473 if ( view && view.select ) {
474 view.select( this.id );
480 _updateMenu: function() {
481 var previous = this.previous('menu'),
482 menu = this.get('menu');
485 this.frame.off( 'menu:render:' + previous, this._renderMenu, this );
489 this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
495 _renderMenu: function( view ) {
496 var menuItem = this.get('menuItem'),
497 title = this.get('title'),
498 priority = this.get('priority');
500 if ( ! menuItem && title ) {
501 menuItem = { text: title };
504 menuItem.priority = priority;
512 view.set( this.id, menuItem );
516 _.each(['toolbar','content'], function( region ) {
520 media.controller.State.prototype[ '_' + region ] = function() {
521 var mode = this.get( region );
523 this.frame[ region ].render( mode );
528 media.selectionSync = {
529 syncSelection: function() {
530 var selection = this.get('selection'),
531 manager = this.frame._selection;
533 if ( ! this.get('syncSelection') || ! manager || ! selection ) {
537 // If the selection supports multiple items, validate the stored
538 // attachments based on the new selection's conditions. Record
539 // the attachments that are not included; we'll maintain a
540 // reference to those. Other attachments are considered in flux.
541 if ( selection.multiple ) {
542 selection.reset( [], { silent: true });
543 selection.validateAll( manager.attachments );
544 manager.difference = _.difference( manager.attachments.models, selection.models );
547 // Sync the selection's single item with the master.
548 selection.single( manager.single );
552 * Record the currently active attachments, which is a combination
553 * of the selection's attachments and the set of selected
554 * attachments that this specific selection considered invalid.
555 * Reset the difference and record the single attachment.
557 recordSelection: function() {
558 var selection = this.get('selection'),
559 manager = this.frame._selection;
561 if ( ! this.get('syncSelection') || ! manager || ! selection ) {
565 if ( selection.multiple ) {
566 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
567 manager.difference = [];
569 manager.attachments.add( selection.toArray() );
572 manager.single = selection._single;
577 * A state for choosing an attachment from the media library.
580 * @augments wp.media.controller.State
581 * @augments Backbone.Model
583 media.controller.Library = media.controller.State.extend({
586 title: l10n.mediaLibraryTitle,
587 // Selection defaults. @see media.model.Selection
589 // Initial region modes.
594 // Attachments browser defaults. @see media.view.AttachmentsBrowser
601 // Uses a user setting to override the content mode.
602 contentUserSetting: true,
603 // Sync the selection from the last state when 'multiple' matches.
608 * If a library isn't provided, query all media items.
609 * If a selection instance isn't provided, create one.
611 initialize: function() {
612 var selection = this.get('selection'),
615 if ( ! this.get('library') ) {
616 this.set( 'library', media.query() );
619 if ( ! (selection instanceof media.model.Selection) ) {
623 props = this.get('library').props.toJSON();
624 props = _.omit( props, 'orderby', 'query' );
627 // If the `selection` attribute is set to an object,
628 // it will use those values as the selection instance's
629 // `props` model. Otherwise, it will copy the library's
631 this.set( 'selection', new media.model.Selection( null, {
632 multiple: this.get('multiple'),
637 this.resetDisplays();
640 activate: function() {
641 this.syncSelection();
643 wp.Uploader.queue.on( 'add', this.uploading, this );
645 this.get('selection').on( 'add remove reset', this.refreshContent, this );
647 if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
648 this.frame.on( 'content:activate', this.saveContentMode, this );
649 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
653 deactivate: function() {
654 this.recordSelection();
656 this.frame.off( 'content:activate', this.saveContentMode, this );
658 // Unbind all event handlers that use this state as the context
659 // from the selection.
660 this.get('selection').off( null, null, this );
662 wp.Uploader.queue.off( null, null, this );
666 this.get('selection').reset();
667 this.resetDisplays();
668 this.refreshContent();
671 resetDisplays: function() {
672 var defaultProps = media.view.settings.defaultProps;
674 this._defaultDisplaySettings = {
675 align: defaultProps.align || getUserSetting( 'align', 'none' ),
676 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ),
677 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' )
682 * @param {wp.media.model.Attachment} attachment
683 * @returns {Backbone.Model}
685 display: function( attachment ) {
686 var displays = this._displays;
688 if ( ! displays[ attachment.cid ] ) {
689 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) );
691 return displays[ attachment.cid ];
695 * @param {wp.media.model.Attachment} attachment
698 defaultDisplaySettings: function( attachment ) {
699 var settings = this._defaultDisplaySettings;
700 if ( settings.canEmbed = this.canEmbed( attachment ) ) {
701 settings.link = 'embed';
707 * @param {wp.media.model.Attachment} attachment
710 canEmbed: function( attachment ) {
711 // If uploading, we know the filename but not the mime type.
712 if ( ! attachment.get('uploading') ) {
713 var type = attachment.get('type');
714 if ( type !== 'audio' && type !== 'video' ) {
719 return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() );
724 * If the state is active, no items are selected, and the current
725 * content mode is not an option in the state's router (provided
726 * the state has a router), reset the content mode to the default.
728 refreshContent: function() {
729 var selection = this.get('selection'),
731 router = frame.router.get(),
732 mode = frame.content.mode();
734 if ( this.active && ! selection.length && router && ! router.get( mode ) ) {
735 this.frame.content.render( this.get('content') );
740 * If the uploader was selected, navigate to the browser.
742 * Automatically select any uploading attachments.
744 * Selections that don't support multiple attachments automatically
745 * limit themselves to one attachment (in this case, the last
746 * attachment in the upload queue).
748 * @param {wp.media.model.Attachment} attachment
750 uploading: function( attachment ) {
751 var content = this.frame.content;
753 if ( 'upload' === content.mode() ) {
754 this.frame.content.mode('browse');
757 if ( this.get( 'autoSelect' ) ) {
758 this.get('selection').add( attachment );
759 this.frame.trigger( 'library:selection:add' );
764 * Only track the browse router on library states.
766 saveContentMode: function() {
767 if ( 'browse' !== this.get('router') ) {
771 var mode = this.frame.content.mode(),
772 view = this.frame.router.get();
774 if ( view && view.get( mode ) ) {
775 setUserSetting( 'libraryContent', mode );
780 _.extend( media.controller.Library.prototype, media.selectionSync );
783 * A state for editing the settings of an image within a content editor.
786 * @augments wp.media.controller.State
787 * @augments Backbone.Model
789 media.controller.ImageDetails = media.controller.State.extend({
790 defaults: _.defaults({
792 title: l10n.imageDetailsTitle,
793 // Initial region modes.
794 content: 'image-details',
797 toolbar: 'image-details',
801 }, media.controller.Library.prototype.defaults ),
803 initialize: function( options ) {
804 this.image = options.image;
805 media.controller.State.prototype.initialize.apply( this, arguments );
808 activate: function() {
809 this.frame.modal.$el.addClass('image-details');
814 * A state for editing a gallery's images and settings.
817 * @augments wp.media.controller.Library
818 * @augments wp.media.controller.State
819 * @augments Backbone.Model
821 media.controller.GalleryEdit = media.controller.Library.extend({
824 title: l10n.editGalleryTitle,
825 // Selection defaults. @see media.model.Selection
827 // Attachments browser defaults. @see media.view.AttachmentsBrowser
831 // Initial region modes.
833 toolbar: 'gallery-edit',
836 displaySettings: true,
838 idealColumnWidth: 170,
842 // Don't sync the selection, as the Edit Gallery library
843 // *is* the selection.
847 initialize: function() {
848 // If we haven't been provided a `library`, create a `Selection`.
849 if ( ! this.get('library') )
850 this.set( 'library', new media.model.Selection() );
852 // The single `Attachment` view to be used in the `Attachments` view.
853 if ( ! this.get('AttachmentView') )
854 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
855 media.controller.Library.prototype.initialize.apply( this, arguments );
858 activate: function() {
859 var library = this.get('library');
861 // Limit the library to images only.
862 library.props.set( 'type', 'image' );
864 // Watch for uploaded attachments.
865 this.get('library').observe( wp.Uploader.queue );
867 this.frame.on( 'content:render:browse', this.gallerySettings, this );
869 media.controller.Library.prototype.activate.apply( this, arguments );
872 deactivate: function() {
873 // Stop watching for uploaded attachments.
874 this.get('library').unobserve( wp.Uploader.queue );
876 this.frame.off( 'content:render:browse', this.gallerySettings, this );
878 media.controller.Library.prototype.deactivate.apply( this, arguments );
881 gallerySettings: function( browser ) {
882 if ( ! this.get('displaySettings') ) {
886 var library = this.get('library');
888 if ( ! library || ! browser ) {
892 library.gallery = library.gallery || new Backbone.Model();
894 browser.sidebar.set({
895 gallery: new media.view.Settings.Gallery({
897 model: library.gallery,
902 browser.toolbar.set( 'reverse', {
903 text: l10n.reverseOrder,
907 library.reset( library.toArray().reverse() );
914 * A state for adding an image to a gallery.
917 * @augments wp.media.controller.Library
918 * @augments wp.media.controller.State
919 * @augments Backbone.Model
921 media.controller.GalleryAdd = media.controller.Library.extend({
922 defaults: _.defaults({
923 id: 'gallery-library',
924 title: l10n.addToGalleryTitle,
925 // Selection defaults. @see media.model.Selection
927 // Attachments browser defaults. @see media.view.AttachmentsBrowser
928 filterable: 'uploaded',
929 // Initial region modes.
931 toolbar: 'gallery-add',
934 // Don't sync the selection, as the Edit Gallery library
935 // *is* the selection.
937 }, media.controller.Library.prototype.defaults ),
939 initialize: function() {
940 // If we haven't been provided a `library`, create a `Selection`.
941 if ( ! this.get('library') )
942 this.set( 'library', media.query({ type: 'image' }) );
944 media.controller.Library.prototype.initialize.apply( this, arguments );
947 activate: function() {
948 var library = this.get('library'),
949 edit = this.frame.state('gallery-edit').get('library');
951 if ( this.editLibrary && this.editLibrary !== edit )
952 library.unobserve( this.editLibrary );
954 // Accepts attachments that exist in the original library and
955 // that do not exist in gallery's library.
956 library.validator = function( attachment ) {
957 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
960 // Reset the library to ensure that all attachments are re-added
961 // to the collection. Do so silently, as calling `observe` will
962 // trigger the `reset` event.
963 library.reset( library.mirroring.models, { silent: true });
964 library.observe( edit );
965 this.editLibrary = edit;
967 media.controller.Library.prototype.activate.apply( this, arguments );
972 * wp.media.controller.CollectionEdit
975 * @augments wp.media.controller.Library
976 * @augments wp.media.controller.State
977 * @augments Backbone.Model
979 media.controller.CollectionEdit = media.controller.Library.extend({
981 // Selection defaults. @see media.model.Selection
983 // Attachments browser defaults. @see media.view.AttachmentsBrowser
986 // Region mode defaults.
991 idealColumnWidth: 170,
996 // Don't sync the selection, as the Edit {Collection} library
997 // *is* the selection.
1001 initialize: function() {
1002 var collectionType = this.get('collectionType');
1004 if ( 'video' === this.get( 'type' ) ) {
1005 collectionType = 'video-' + collectionType;
1008 this.set( 'id', collectionType + '-edit' );
1009 this.set( 'toolbar', collectionType + '-edit' );
1011 // If we haven't been provided a `library`, create a `Selection`.
1012 if ( ! this.get('library') ) {
1013 this.set( 'library', new media.model.Selection() );
1015 // The single `Attachment` view to be used in the `Attachments` view.
1016 if ( ! this.get('AttachmentView') ) {
1017 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
1019 media.controller.Library.prototype.initialize.apply( this, arguments );
1022 activate: function() {
1023 var library = this.get('library');
1025 // Limit the library to images only.
1026 library.props.set( 'type', this.get( 'type' ) );
1028 // Watch for uploaded attachments.
1029 this.get('library').observe( wp.Uploader.queue );
1031 this.frame.on( 'content:render:browse', this.renderSettings, this );
1033 media.controller.Library.prototype.activate.apply( this, arguments );
1036 deactivate: function() {
1037 // Stop watching for uploaded attachments.
1038 this.get('library').unobserve( wp.Uploader.queue );
1040 this.frame.off( 'content:render:browse', this.renderSettings, this );
1042 media.controller.Library.prototype.deactivate.apply( this, arguments );
1045 renderSettings: function( browser ) {
1046 var library = this.get('library'),
1047 collectionType = this.get('collectionType'),
1048 dragInfoText = this.get('dragInfoText'),
1049 SettingsView = this.get('SettingsView'),
1052 if ( ! library || ! browser ) {
1056 library[ collectionType ] = library[ collectionType ] || new Backbone.Model();
1058 obj[ collectionType ] = new SettingsView({
1060 model: library[ collectionType ],
1064 browser.sidebar.set( obj );
1066 if ( dragInfoText ) {
1067 browser.toolbar.set( 'dragInfo', new media.View({
1068 el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0],
1073 browser.toolbar.set( 'reverse', {
1074 text: l10n.reverseOrder,
1078 library.reset( library.toArray().reverse() );
1085 * wp.media.controller.CollectionAdd
1088 * @augments wp.media.controller.Library
1089 * @augments wp.media.controller.State
1090 * @augments Backbone.Model
1092 media.controller.CollectionAdd = media.controller.Library.extend({
1093 defaults: _.defaults( {
1094 // Selection defaults. @see media.model.Selection
1096 // Attachments browser defaults. @see media.view.AttachmentsBrowser
1097 filterable: 'uploaded',
1100 syncSelection: false
1101 }, media.controller.Library.prototype.defaults ),
1103 initialize: function() {
1104 var collectionType = this.get('collectionType');
1106 if ( 'video' === this.get( 'type' ) ) {
1107 collectionType = 'video-' + collectionType;
1110 this.set( 'id', collectionType + '-library' );
1111 this.set( 'toolbar', collectionType + '-add' );
1112 this.set( 'menu', collectionType );
1114 // If we haven't been provided a `library`, create a `Selection`.
1115 if ( ! this.get('library') ) {
1116 this.set( 'library', media.query({ type: this.get('type') }) );
1118 media.controller.Library.prototype.initialize.apply( this, arguments );
1121 activate: function() {
1122 var library = this.get('library'),
1123 editLibrary = this.get('editLibrary'),
1124 edit = this.frame.state( this.get('collectionType') + '-edit' ).get('library');
1126 if ( editLibrary && editLibrary !== edit ) {
1127 library.unobserve( editLibrary );
1130 // Accepts attachments that exist in the original library and
1131 // that do not exist in gallery's library.
1132 library.validator = function( attachment ) {
1133 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
1136 // Reset the library to ensure that all attachments are re-added
1137 // to the collection. Do so silently, as calling `observe` will
1138 // trigger the `reset` event.
1139 library.reset( library.mirroring.models, { silent: true });
1140 library.observe( edit );
1141 this.set('editLibrary', edit);
1143 media.controller.Library.prototype.activate.apply( this, arguments );
1148 * A state for selecting a featured image for a post.
1151 * @augments wp.media.controller.Library
1152 * @augments wp.media.controller.State
1153 * @augments Backbone.Model
1155 media.controller.FeaturedImage = media.controller.Library.extend({
1156 defaults: _.defaults({
1157 id: 'featured-image',
1158 title: l10n.setFeaturedImageTitle,
1159 // Selection defaults. @see media.model.Selection
1161 // Attachments browser defaults. @see media.view.AttachmentsBrowser
1162 filterable: 'uploaded',
1163 // Region mode defaults.
1164 toolbar: 'featured-image',
1168 }, media.controller.Library.prototype.defaults ),
1170 initialize: function() {
1171 var library, comparator;
1173 // If we haven't been provided a `library`, create a `Selection`.
1174 if ( ! this.get('library') ) {
1175 this.set( 'library', media.query({ type: 'image' }) );
1178 media.controller.Library.prototype.initialize.apply( this, arguments );
1180 library = this.get('library');
1181 comparator = library.comparator;
1183 // Overload the library's comparator to push items that are not in
1184 // the mirrored query to the front of the aggregate collection.
1185 library.comparator = function( a, b ) {
1186 var aInQuery = !! this.mirroring.get( a.cid ),
1187 bInQuery = !! this.mirroring.get( b.cid );
1189 if ( ! aInQuery && bInQuery ) {
1191 } else if ( aInQuery && ! bInQuery ) {
1194 return comparator.apply( this, arguments );
1198 // Add all items in the selection to the library, so any featured
1199 // images that are not initially loaded still appear.
1200 library.observe( this.get('selection') );
1203 activate: function() {
1204 this.updateSelection();
1205 this.frame.on( 'open', this.updateSelection, this );
1207 media.controller.Library.prototype.activate.apply( this, arguments );
1210 deactivate: function() {
1211 this.frame.off( 'open', this.updateSelection, this );
1213 media.controller.Library.prototype.deactivate.apply( this, arguments );
1216 updateSelection: function() {
1217 var selection = this.get('selection'),
1218 id = media.view.settings.post.featuredImageId,
1221 if ( '' !== id && -1 !== id ) {
1222 attachment = media.model.Attachment.get( id );
1226 selection.reset( attachment ? [ attachment ] : [] );
1231 * A state for replacing an image.
1234 * @augments wp.media.controller.Library
1235 * @augments wp.media.controller.State
1236 * @augments Backbone.Model
1238 media.controller.ReplaceImage = media.controller.Library.extend({
1239 defaults: _.defaults({
1240 id: 'replace-image',
1241 title: l10n.replaceImageTitle,
1242 // Selection defaults. @see media.model.Selection
1244 // Attachments browser defaults. @see media.view.AttachmentsBrowser
1245 filterable: 'uploaded',
1246 // Region mode defaults.
1252 }, media.controller.Library.prototype.defaults ),
1254 initialize: function( options ) {
1255 var library, comparator;
1257 this.image = options.image;
1258 // If we haven't been provided a `library`, create a `Selection`.
1259 if ( ! this.get('library') ) {
1260 this.set( 'library', media.query({ type: 'image' }) );
1263 media.controller.Library.prototype.initialize.apply( this, arguments );
1265 library = this.get('library');
1266 comparator = library.comparator;
1268 // Overload the library's comparator to push items that are not in
1269 // the mirrored query to the front of the aggregate collection.
1270 library.comparator = function( a, b ) {
1271 var aInQuery = !! this.mirroring.get( a.cid ),
1272 bInQuery = !! this.mirroring.get( b.cid );
1274 if ( ! aInQuery && bInQuery ) {
1276 } else if ( aInQuery && ! bInQuery ) {
1279 return comparator.apply( this, arguments );
1283 // Add all items in the selection to the library, so any featured
1284 // images that are not initially loaded still appear.
1285 library.observe( this.get('selection') );
1288 activate: function() {
1289 this.updateSelection();
1290 media.controller.Library.prototype.activate.apply( this, arguments );
1293 updateSelection: function() {
1294 var selection = this.get('selection'),
1295 attachment = this.image.attachment;
1297 selection.reset( attachment ? [ attachment ] : [] );
1302 * A state for editing (cropping, etc.) an image.
1305 * @augments wp.media.controller.State
1306 * @augments Backbone.Model
1308 media.controller.EditImage = media.controller.State.extend({
1311 title: l10n.editImage,
1312 // Region mode defaults.
1314 toolbar: 'edit-image',
1315 content: 'edit-image',
1320 activate: function() {
1321 this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar );
1324 deactivate: function() {
1325 this.stopListening( this.frame );
1328 toolbar: function() {
1329 var frame = this.frame,
1330 lastState = frame.lastState(),
1331 previous = lastState && lastState.id;
1333 frame.toolbar.set( new media.view.Toolbar({
1342 frame.setState( previous );
1354 * wp.media.controller.MediaLibrary
1357 * @augments wp.media.controller.Library
1358 * @augments wp.media.controller.State
1359 * @augments Backbone.Model
1361 media.controller.MediaLibrary = media.controller.Library.extend({
1362 defaults: _.defaults({
1363 // Attachments browser defaults. @see media.view.AttachmentsBrowser
1364 filterable: 'uploaded',
1366 displaySettings: false,
1368 syncSelection: false
1369 }, media.controller.Library.prototype.defaults ),
1371 initialize: function( options ) {
1372 this.media = options.media;
1373 this.type = options.type;
1374 this.set( 'library', media.query({ type: this.type }) );
1376 media.controller.Library.prototype.initialize.apply( this, arguments );
1379 activate: function() {
1380 if ( media.frame.lastMime ) {
1381 this.set( 'library', media.query({ type: media.frame.lastMime }) );
1382 delete media.frame.lastMime;
1384 media.controller.Library.prototype.activate.apply( this, arguments );
1389 * wp.media.controller.Embed
1392 * @augments wp.media.controller.State
1393 * @augments Backbone.Model
1395 media.controller.Embed = media.controller.State.extend({
1398 title: l10n.insertFromUrlTitle,
1399 // Region mode defaults.
1402 toolbar: 'main-embed',
1410 // The amount of time used when debouncing the scan.
1413 initialize: function(options) {
1414 this.metadata = options.metadata;
1415 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
1416 this.props = new Backbone.Model( this.metadata || { url: '' });
1417 this.props.on( 'change:url', this.debouncedScan, this );
1418 this.props.on( 'change:url', this.refresh, this );
1419 this.on( 'scan', this.scanImage, this );
1423 * @fires wp.media.controller.Embed#scan
1433 // Scan is triggered with the list of `attributes` to set on the
1434 // state, useful for the 'type' attribute and 'scanners' attribute,
1435 // an array of promise objects for asynchronous scan operations.
1436 if ( this.props.get('url') ) {
1437 this.trigger( 'scan', attributes );
1440 if ( attributes.scanners.length ) {
1441 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
1442 scanners.always( function() {
1443 if ( embed.get('scanners') === scanners ) {
1444 embed.set( 'loading', false );
1448 attributes.scanners = null;
1451 attributes.loading = !! attributes.scanners;
1452 this.set( attributes );
1455 * @param {Object} attributes
1457 scanImage: function( attributes ) {
1458 var frame = this.frame,
1460 url = this.props.get('url'),
1461 image = new Image(),
1462 deferred = $.Deferred();
1464 attributes.scanners.push( deferred.promise() );
1466 // Try to load the image and find its width/height.
1467 image.onload = function() {
1470 if ( state !== frame.state() || url !== state.props.get('url') ) {
1480 height: image.height
1484 image.onerror = deferred.reject;
1488 refresh: function() {
1489 this.frame.toolbar.get().refresh();
1493 this.props.clear().set({ url: '' });
1495 if ( this.active ) {
1502 * wp.media.controller.Cropper
1504 * Allows for a cropping step.
1507 * @augments wp.media.controller.State
1508 * @augments Backbone.Model
1510 media.controller.Cropper = media.controller.State.extend({
1513 title: l10n.cropImage,
1514 // Region mode defaults.
1522 activate: function() {
1523 this.frame.on( 'content:create:crop', this.createCropContent, this );
1524 this.frame.on( 'close', this.removeCropper, this );
1525 this.set('selection', new Backbone.Collection(this.frame._selection.single));
1528 deactivate: function() {
1529 this.frame.toolbar.mode('browse');
1532 createCropContent: function() {
1533 this.cropperView = new wp.media.view.Cropper({controller: this,
1534 attachment: this.get('selection').first() });
1535 this.cropperView.on('image-loaded', this.createCropToolbar, this);
1536 this.frame.content.set(this.cropperView);
1539 removeCropper: function() {
1540 this.imgSelect.cancelSelection();
1541 this.imgSelect.setOptions({remove: true});
1542 this.imgSelect.update();
1543 this.cropperView.remove();
1545 createCropToolbar: function() {
1546 var canSkipCrop, toolbarOptions;
1548 canSkipCrop = this.get('canSkipCrop') || false;
1551 controller: this.frame,
1555 text: l10n.cropImage,
1557 requires: { library: false, selection: false },
1561 selection = this.controller.state().get('selection').first();
1563 selection.set({cropDetails: this.controller.state().imgSelect.getSelection()});
1565 this.$el.text(l10n.cropping);
1566 this.$el.attr('disabled', true);
1567 this.controller.state().doCrop( selection ).done( function( croppedImage ) {
1568 self.controller.trigger('cropped', croppedImage );
1569 self.controller.close();
1570 }).fail( function() {
1571 self.controller.trigger('content:error:crop');
1578 if ( canSkipCrop ) {
1579 _.extend( toolbarOptions.items, {
1582 text: l10n.skipCropping,
1584 requires: { library: false, selection: false },
1586 var selection = this.controller.state().get('selection').first();
1587 this.controller.state().cropperView.remove();
1588 this.controller.trigger('skippedcrop', selection);
1589 this.controller.close();
1595 this.frame.toolbar.set( new wp.media.view.Toolbar(toolbarOptions) );
1598 doCrop: function( attachment ) {
1599 return wp.ajax.post( 'custom-header-crop', {
1600 nonce: attachment.get('nonces').edit,
1601 id: attachment.get('id'),
1602 cropDetails: attachment.get('cropDetails')
1608 * ========================================================================
1610 * ========================================================================
1617 * The base view class.
1619 * Undelegating events, removing events from the model, and
1620 * removing events from the controller mirror the code for
1621 * `Backbone.View.dispose` in Backbone 0.9.8 development.
1623 * This behavior has since been removed, and should not be used
1624 * outside of the media manager.
1627 * @augments wp.Backbone.View
1628 * @augments Backbone.View
1630 media.View = wp.Backbone.View.extend({
1631 constructor: function( options ) {
1632 if ( options && options.controller ) {
1633 this.controller = options.controller;
1635 wp.Backbone.View.apply( this, arguments );
1638 * @returns {wp.media.View} Returns itself to allow chaining
1640 dispose: function() {
1641 // Undelegating events, removing events from the model, and
1642 // removing events from the controller mirror the code for
1643 // `Backbone.View.dispose` in Backbone 0.9.8 development.
1644 this.undelegateEvents();
1646 if ( this.model && this.model.off ) {
1647 this.model.off( null, null, this );
1650 if ( this.collection && this.collection.off ) {
1651 this.collection.off( null, null, this );
1654 // Unbind controller events.
1655 if ( this.controller && this.controller.off ) {
1656 this.controller.off( null, null, this );
1662 * @returns {wp.media.View} Returns itself to allow chaining
1664 remove: function() {
1667 * call 'remove' directly on the parent class
1669 return wp.Backbone.View.prototype.remove.apply( this, arguments );
1674 * wp.media.view.Frame
1676 * A frame is a composite view consisting of one or more regions and one or more
1677 * states. Only one state can be active at any given moment.
1680 * @augments wp.media.View
1681 * @augments wp.Backbone.View
1682 * @augments Backbone.View
1683 * @mixes wp.media.controller.StateMachine
1685 media.view.Frame = media.View.extend({
1686 initialize: function() {
1687 _.defaults( this.options, {
1690 this._createRegions();
1691 this._createStates();
1692 this._createModes();
1695 _createRegions: function() {
1696 // Clone the regions array.
1697 this.regions = this.regions ? this.regions.slice() : [];
1699 // Initialize regions.
1700 _.each( this.regions, function( region ) {
1701 this[ region ] = new media.controller.Region({
1704 selector: '.media-frame-' + region
1709 * @fires wp.media.controller.State#ready
1711 _createStates: function() {
1712 // Create the default `states` collection.
1713 this.states = new Backbone.Collection( null, {
1714 model: media.controller.State
1717 // Ensure states have a reference to the frame.
1718 this.states.on( 'add', function( model ) {
1720 model.trigger('ready');
1723 if ( this.options.states ) {
1724 this.states.add( this.options.states );
1727 _createModes: function() {
1728 // Store active "modes" that the frame is in. Unrelated to region modes.
1729 this.activeModes = new Backbone.Collection();
1730 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) );
1732 _.each( this.options.mode, function( mode ) {
1733 this.activateMode( mode );
1737 * @returns {wp.media.view.Frame} Returns itself to allow chaining
1740 this.states.invoke( 'trigger', 'reset' );
1744 * Map activeMode collection events to the frame.
1746 triggerModeEvents: function( model, collection, options ) {
1747 var collectionEvent,
1750 remove: 'deactivate'
1753 // Probably a better way to do this.
1754 _.each( options, function( value, key ) {
1756 collectionEvent = key;
1760 if ( ! _.has( modeEventMap, collectionEvent ) ) {
1764 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent];
1765 this.trigger( eventToTrigger );
1768 * Activate a mode on the frame.
1770 * @param string mode Mode ID.
1771 * @returns {this} Returns itself to allow chaining.
1773 activateMode: function( mode ) {
1774 // Bail if the mode is already active.
1775 if ( this.isModeActive( mode ) ) {
1778 this.activeModes.add( [ { id: mode } ] );
1779 // Add a CSS class to the frame so elements can be styled for the mode.
1780 this.$el.addClass( 'mode-' + mode );
1785 * Deactivate a mode on the frame.
1787 * @param string mode Mode ID.
1788 * @returns {this} Returns itself to allow chaining.
1790 deactivateMode: function( mode ) {
1791 // Bail if the mode isn't active.
1792 if ( ! this.isModeActive( mode ) ) {
1795 this.activeModes.remove( this.activeModes.where( { id: mode } ) );
1796 this.$el.removeClass( 'mode-' + mode );
1798 * Frame mode deactivation event.
1800 * @event this#{mode}:deactivate
1802 this.trigger( mode + ':deactivate' );
1807 * Check if a mode is enabled on the frame.
1809 * @param string mode Mode ID.
1812 isModeActive: function( mode ) {
1813 return Boolean( this.activeModes.where( { id: mode } ).length );
1817 // Make the `Frame` a `StateMachine`.
1818 _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1821 * wp.media.view.MediaFrame
1823 * Type of frame used to create the media modal.
1826 * @augments wp.media.view.Frame
1827 * @augments wp.media.View
1828 * @augments wp.Backbone.View
1829 * @augments Backbone.View
1830 * @mixes wp.media.controller.StateMachine
1832 media.view.MediaFrame = media.view.Frame.extend({
1833 className: 'media-frame',
1834 template: media.template('media-frame'),
1835 regions: ['menu','title','content','toolbar','router'],
1838 'click div.media-frame-title h1': 'toggleMenu'
1842 * @global wp.Uploader
1844 initialize: function() {
1845 media.view.Frame.prototype.initialize.apply( this, arguments );
1847 _.defaults( this.options, {
1853 // Ensure core UI is enabled.
1854 this.$el.addClass('wp-core-ui');
1856 // Initialize modal container view.
1857 if ( this.options.modal ) {
1858 this.modal = new media.view.Modal({
1860 title: this.options.title
1863 this.modal.content( this );
1866 // Force the uploader off if the upload limit has been exceeded or
1867 // if the browser isn't supported.
1868 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) {
1869 this.options.uploader = false;
1872 // Initialize window-wide uploader.
1873 if ( this.options.uploader ) {
1874 this.uploader = new media.view.UploaderWindow({
1877 dropzone: this.modal ? this.modal.$el : this.$el,
1881 this.views.set( '.media-frame-uploader', this.uploader );
1884 this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1886 // Bind default title creation.
1887 this.on( 'title:create:default', this.createTitle, this );
1888 this.title.mode('default');
1890 this.on( 'title:render', function( view ) {
1891 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
1894 // Bind default menu.
1895 this.on( 'menu:create:default', this.createMenu, this );
1898 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
1900 render: function() {
1901 // Activate the default state if no active state exists.
1902 if ( ! this.state() && this.options.state ) {
1903 this.setState( this.options.state );
1906 * call 'render' directly on the parent class
1908 return media.view.Frame.prototype.render.apply( this, arguments );
1911 * @param {Object} title
1912 * @this wp.media.controller.Region
1914 createTitle: function( title ) {
1915 title.view = new media.View({
1921 * @param {Object} menu
1922 * @this wp.media.controller.Region
1924 createMenu: function( menu ) {
1925 menu.view = new media.view.Menu({
1930 toggleMenu: function() {
1931 this.$el.find( '.media-menu' ).toggleClass( 'visible' );
1935 * @param {Object} toolbar
1936 * @this wp.media.controller.Region
1938 createToolbar: function( toolbar ) {
1939 toolbar.view = new media.view.Toolbar({
1944 * @param {Object} router
1945 * @this wp.media.controller.Region
1947 createRouter: function( router ) {
1948 router.view = new media.view.Router({
1953 * @param {Object} options
1955 createIframeStates: function( options ) {
1956 var settings = media.view.settings,
1957 tabs = settings.tabs,
1958 tabUrl = settings.tabUrl,
1961 if ( ! tabs || ! tabUrl ) {
1965 // Add the post ID to the tab URL if it exists.
1966 $postId = $('#post_ID');
1967 if ( $postId.length ) {
1968 tabUrl += '&post_id=' + $postId.val();
1971 // Generate the tab states.
1972 _.each( tabs, function( title, id ) {
1973 this.state( 'iframe:' + id ).set( _.defaults({
1975 src: tabUrl + '&tab=' + id,
1982 this.on( 'content:create:iframe', this.iframeContent, this );
1983 this.on( 'menu:render:default', this.iframeMenu, this );
1984 this.on( 'open', this.hijackThickbox, this );
1985 this.on( 'close', this.restoreThickbox, this );
1989 * @param {Object} content
1990 * @this wp.media.controller.Region
1992 iframeContent: function( content ) {
1993 this.$el.addClass('hide-toolbar');
1994 content.view = new media.view.Iframe({
1999 iframeMenu: function( view ) {
2006 _.each( media.view.settings.tabs, function( title, id ) {
2007 views[ 'iframe:' + id ] = {
2008 text: this.state( 'iframe:' + id ).get('title'),
2016 hijackThickbox: function() {
2019 if ( ! window.tb_remove || this._tb_remove ) {
2023 this._tb_remove = window.tb_remove;
2024 window.tb_remove = function() {
2027 frame.setState( frame.options.state );
2028 frame._tb_remove.call( window );
2032 restoreThickbox: function() {
2033 if ( ! this._tb_remove ) {
2037 window.tb_remove = this._tb_remove;
2038 delete this._tb_remove;
2042 // Map some of the modal's methods to the frame.
2043 _.each(['open','close','attach','detach','escape'], function( method ) {
2045 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
2047 media.view.MediaFrame.prototype[ method ] = function() {
2049 this.modal[ method ].apply( this.modal, arguments );
2056 * wp.media.view.MediaFrame.Select
2058 * Type of media frame that is used to select an item or items from the media library
2061 * @augments wp.media.view.MediaFrame
2062 * @augments wp.media.view.Frame
2063 * @augments wp.media.View
2064 * @augments wp.Backbone.View
2065 * @augments Backbone.View
2066 * @mixes wp.media.controller.StateMachine
2068 media.view.MediaFrame.Select = media.view.MediaFrame.extend({
2069 initialize: function() {
2071 * call 'initialize' directly on the parent class
2073 media.view.MediaFrame.prototype.initialize.apply( this, arguments );
2075 _.defaults( this.options, {
2082 this.createSelection();
2083 this.createStates();
2084 this.bindHandlers();
2088 * Attach a selection collection to the frame.
2090 * A selection is a collection of attachments used for a specific purpose
2091 * by a media frame. e.g. Selecting an attachment (or many) to insert into
2094 * @see media.model.Selection
2096 createSelection: function() {
2097 var selection = this.options.selection;
2099 if ( ! (selection instanceof media.model.Selection) ) {
2100 this.options.selection = new media.model.Selection( selection, {
2101 multiple: this.options.multiple
2106 attachments: new media.model.Attachments(),
2112 * Create the default states on the frame.
2114 createStates: function() {
2115 var options = this.options;
2117 if ( this.options.states ) {
2121 // Add the default states.
2124 new media.controller.Library({
2125 library: media.query( options.library ),
2126 multiple: options.multiple,
2127 title: options.title,
2134 * Bind region mode event callbacks.
2136 * @see media.controller.Region.render
2138 bindHandlers: function() {
2139 this.on( 'router:create:browse', this.createRouter, this );
2140 this.on( 'router:render:browse', this.browseRouter, this );
2141 this.on( 'content:create:browse', this.browseContent, this );
2142 this.on( 'content:render:upload', this.uploadContent, this );
2143 this.on( 'toolbar:create:select', this.createSelectToolbar, this );
2147 * Render callback for the router region in the `browse` mode.
2149 * @param {wp.media.view.Router} routerView
2151 browseRouter: function( routerView ) {
2154 text: l10n.uploadFilesTitle,
2158 text: l10n.mediaLibraryTitle,
2165 * Render callback for the content region in the `browse` mode.
2167 * @param {wp.media.controller.Region} contentRegion
2169 browseContent: function( contentRegion ) {
2170 var state = this.state();
2172 this.$el.removeClass('hide-toolbar');
2174 // Browse our library of attachments.
2175 contentRegion.view = new media.view.AttachmentsBrowser({
2177 collection: state.get('library'),
2178 selection: state.get('selection'),
2180 sortable: state.get('sortable'),
2181 search: state.get('searchable'),
2182 filters: state.get('filterable'),
2183 display: state.has('display') ? state.get('display') : state.get('displaySettings'),
2184 dragInfo: state.get('dragInfo'),
2186 idealColumnWidth: state.get('idealColumnWidth'),
2187 suggestedWidth: state.get('suggestedWidth'),
2188 suggestedHeight: state.get('suggestedHeight'),
2190 AttachmentView: state.get('AttachmentView')
2195 * Render callback for the content region in the `upload` mode.
2197 uploadContent: function() {
2198 this.$el.removeClass( 'hide-toolbar' );
2199 this.content.set( new media.view.UploaderInline({
2207 * @param {Object} toolbar
2208 * @param {Object} [options={}]
2209 * @this wp.media.controller.Region
2211 createSelectToolbar: function( toolbar, options ) {
2212 options = options || this.options.button || {};
2213 options.controller = this;
2215 toolbar.view = new media.view.Toolbar.Select( options );
2220 * wp.media.view.MediaFrame.Post
2223 * @augments wp.media.view.MediaFrame.Select
2224 * @augments wp.media.view.MediaFrame
2225 * @augments wp.media.view.Frame
2226 * @augments wp.media.View
2227 * @augments wp.Backbone.View
2228 * @augments Backbone.View
2229 * @mixes wp.media.controller.StateMachine
2231 media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
2232 initialize: function() {
2235 count: media.view.settings.attachmentCounts.audio,
2239 count: media.view.settings.attachmentCounts.video,
2240 state: 'video-playlist'
2244 _.defaults( this.options, {
2251 * call 'initialize' directly on the parent class
2253 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
2254 this.createIframeStates();
2258 createStates: function() {
2259 var options = this.options;
2261 // Add the default states.
2264 new media.controller.Library({
2266 title: l10n.insertMediaTitle,
2268 toolbar: 'main-insert',
2270 library: media.query( options.library ),
2271 multiple: options.multiple ? 'reset' : false,
2274 // If the user isn't allowed to edit fields,
2275 // can they still edit it locally?
2276 allowLocalEdits: true,
2278 // Show the attachment display settings.
2279 displaySettings: true,
2280 // Update user settings when users adjust the
2281 // attachment display settings.
2282 displayUserSettings: true
2285 new media.controller.Library({
2287 title: l10n.createGalleryTitle,
2289 toolbar: 'main-gallery',
2290 filterable: 'uploaded',
2294 library: media.query( _.defaults({
2296 }, options.library ) )
2300 new media.controller.Embed( { metadata: options.metadata } ),
2302 new media.controller.EditImage( { model: options.editImage } ),
2305 new media.controller.GalleryEdit({
2306 library: options.selection,
2307 editing: options.editing,
2311 new media.controller.GalleryAdd(),
2313 new media.controller.Library({
2315 title: l10n.createPlaylistTitle,
2317 toolbar: 'main-playlist',
2318 filterable: 'uploaded',
2322 library: media.query( _.defaults({
2324 }, options.library ) )
2328 new media.controller.CollectionEdit({
2330 collectionType: 'playlist',
2331 title: l10n.editPlaylistTitle,
2332 SettingsView: media.view.Settings.Playlist,
2333 library: options.selection,
2334 editing: options.editing,
2336 dragInfoText: l10n.playlistDragInfo,
2340 new media.controller.CollectionAdd({
2342 collectionType: 'playlist',
2343 title: l10n.addToPlaylistTitle
2346 new media.controller.Library({
2347 id: 'video-playlist',
2348 title: l10n.createVideoPlaylistTitle,
2350 toolbar: 'main-video-playlist',
2351 filterable: 'uploaded',
2355 library: media.query( _.defaults({
2357 }, options.library ) )
2360 new media.controller.CollectionEdit({
2362 collectionType: 'playlist',
2363 title: l10n.editVideoPlaylistTitle,
2364 SettingsView: media.view.Settings.Playlist,
2365 library: options.selection,
2366 editing: options.editing,
2367 menu: 'video-playlist',
2368 dragInfoText: l10n.videoPlaylistDragInfo,
2372 new media.controller.CollectionAdd({
2374 collectionType: 'playlist',
2375 title: l10n.addToVideoPlaylistTitle
2379 if ( media.view.settings.post.featuredImageId ) {
2380 this.states.add( new media.controller.FeaturedImage() );
2384 bindHandlers: function() {
2385 var handlers, checkCounts;
2387 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
2389 this.on( 'activate', this.activate, this );
2391 // Only bother checking media type counts if one of the counts is zero
2392 checkCounts = _.find( this.counts, function( type ) {
2393 return type.count === 0;
2396 if ( typeof checkCounts !== 'undefined' ) {
2397 this.listenTo( media.model.Attachments.all, 'change:type', this.mediaTypeCounts );
2400 this.on( 'menu:create:gallery', this.createMenu, this );
2401 this.on( 'menu:create:playlist', this.createMenu, this );
2402 this.on( 'menu:create:video-playlist', this.createMenu, this );
2403 this.on( 'toolbar:create:main-insert', this.createToolbar, this );
2404 this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
2405 this.on( 'toolbar:create:main-playlist', this.createToolbar, this );
2406 this.on( 'toolbar:create:main-video-playlist', this.createToolbar, this );
2407 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
2408 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
2412 'default': 'mainMenu',
2413 'gallery': 'galleryMenu',
2414 'playlist': 'playlistMenu',
2415 'video-playlist': 'videoPlaylistMenu'
2419 'embed': 'embedContent',
2420 'edit-image': 'editImageContent',
2421 'edit-selection': 'editSelectionContent'
2425 'main-insert': 'mainInsertToolbar',
2426 'main-gallery': 'mainGalleryToolbar',
2427 'gallery-edit': 'galleryEditToolbar',
2428 'gallery-add': 'galleryAddToolbar',
2429 'main-playlist': 'mainPlaylistToolbar',
2430 'playlist-edit': 'playlistEditToolbar',
2431 'playlist-add': 'playlistAddToolbar',
2432 'main-video-playlist': 'mainVideoPlaylistToolbar',
2433 'video-playlist-edit': 'videoPlaylistEditToolbar',
2434 'video-playlist-add': 'videoPlaylistAddToolbar'
2438 _.each( handlers, function( regionHandlers, region ) {
2439 _.each( regionHandlers, function( callback, handler ) {
2440 this.on( region + ':render:' + handler, this[ callback ], this );
2445 activate: function() {
2446 // Hide menu items for states tied to particular media types if there are no items
2447 _.each( this.counts, function( type ) {
2448 if ( type.count < 1 ) {
2449 this.menuItemVisibility( type.state, 'hide' );
2454 mediaTypeCounts: function( model, attr ) {
2455 if ( typeof this.counts[ attr ] !== 'undefined' && this.counts[ attr ].count < 1 ) {
2456 this.counts[ attr ].count++;
2457 this.menuItemVisibility( this.counts[ attr ].state, 'show' );
2463 * @param {wp.Backbone.View} view
2465 mainMenu: function( view ) {
2467 'library-separator': new media.View({
2468 className: 'separator',
2474 menuItemVisibility: function( state, visibility ) {
2475 var menu = this.menu.get();
2476 if ( visibility === 'hide' ) {
2478 } else if ( visibility === 'show' ) {
2483 * @param {wp.Backbone.View} view
2485 galleryMenu: function( view ) {
2486 var lastState = this.lastState(),
2487 previous = lastState && lastState.id,
2492 text: l10n.cancelGalleryTitle,
2496 frame.setState( previous );
2501 // Keep focus inside media modal
2502 // after canceling a gallery
2503 this.controller.modal.focusManager.focus();
2506 separateCancel: new media.View({
2507 className: 'separator',
2513 playlistMenu: function( view ) {
2514 var lastState = this.lastState(),
2515 previous = lastState && lastState.id,
2520 text: l10n.cancelPlaylistTitle,
2524 frame.setState( previous );
2530 separateCancel: new media.View({
2531 className: 'separator',
2537 videoPlaylistMenu: function( view ) {
2538 var lastState = this.lastState(),
2539 previous = lastState && lastState.id,
2544 text: l10n.cancelVideoPlaylistTitle,
2548 frame.setState( previous );
2554 separateCancel: new media.View({
2555 className: 'separator',
2562 embedContent: function() {
2563 var view = new media.view.Embed({
2568 this.content.set( view );
2570 if ( ! isTouchDevice ) {
2575 editSelectionContent: function() {
2576 var state = this.state(),
2577 selection = state.get('selection'),
2580 view = new media.view.AttachmentsBrowser({
2582 collection: selection,
2583 selection: selection,
2589 AttachmentView: media.view.Attachment.EditSelection
2592 view.toolbar.set( 'backToLibrary', {
2593 text: l10n.returnToLibrary,
2597 this.controller.content.mode('browse');
2601 // Browse our library of attachments.
2602 this.content.set( view );
2605 editImageContent: function() {
2606 var image = this.state().get('image'),
2607 view = new media.view.EditImage( { model: image, controller: this } ).render();
2609 this.content.set( view );
2611 // after creating the wrapper view, load the actual editor via an ajax call
2619 * @param {wp.Backbone.View} view
2621 selectionStatusToolbar: function( view ) {
2622 var editable = this.state().get('editable');
2624 view.set( 'selection', new media.view.Selection({
2626 collection: this.state().get('selection'),
2629 // If the selection is editable, pass the callback to
2630 // switch the content mode.
2631 editable: editable && function() {
2632 this.controller.content.mode('edit-selection');
2638 * @param {wp.Backbone.View} view
2640 mainInsertToolbar: function( view ) {
2641 var controller = this;
2643 this.selectionStatusToolbar( view );
2645 view.set( 'insert', {
2648 text: l10n.insertIntoPost,
2649 requires: { selection: true },
2652 * @fires wp.media.controller.State#insert
2655 var state = controller.state(),
2656 selection = state.get('selection');
2659 state.trigger( 'insert', selection ).reset();
2665 * @param {wp.Backbone.View} view
2667 mainGalleryToolbar: function( view ) {
2668 var controller = this;
2670 this.selectionStatusToolbar( view );
2672 view.set( 'gallery', {
2674 text: l10n.createNewGallery,
2676 requires: { selection: true },
2679 var selection = controller.state().get('selection'),
2680 edit = controller.state('gallery-edit'),
2681 models = selection.where({ type: 'image' });
2683 edit.set( 'library', new media.model.Selection( models, {
2684 props: selection.props.toJSON(),
2688 this.controller.setState('gallery-edit');
2690 // Keep focus inside media modal
2691 // after jumping to gallery view
2692 this.controller.modal.focusManager.focus();
2697 mainPlaylistToolbar: function( view ) {
2698 var controller = this;
2700 this.selectionStatusToolbar( view );
2702 view.set( 'playlist', {
2704 text: l10n.createNewPlaylist,
2706 requires: { selection: true },
2709 var selection = controller.state().get('selection'),
2710 edit = controller.state('playlist-edit'),
2711 models = selection.where({ type: 'audio' });
2713 edit.set( 'library', new media.model.Selection( models, {
2714 props: selection.props.toJSON(),
2718 this.controller.setState('playlist-edit');
2720 // Keep focus inside media modal
2721 // after jumping to playlist view
2722 this.controller.modal.focusManager.focus();
2727 mainVideoPlaylistToolbar: function( view ) {
2728 var controller = this;
2730 this.selectionStatusToolbar( view );
2732 view.set( 'video-playlist', {
2734 text: l10n.createNewVideoPlaylist,
2736 requires: { selection: true },
2739 var selection = controller.state().get('selection'),
2740 edit = controller.state('video-playlist-edit'),
2741 models = selection.where({ type: 'video' });
2743 edit.set( 'library', new media.model.Selection( models, {
2744 props: selection.props.toJSON(),
2748 this.controller.setState('video-playlist-edit');
2750 // Keep focus inside media modal
2751 // after jumping to video playlist view
2752 this.controller.modal.focusManager.focus();
2757 featuredImageToolbar: function( toolbar ) {
2758 this.createSelectToolbar( toolbar, {
2759 text: l10n.setFeaturedImage,
2760 state: this.options.state
2764 mainEmbedToolbar: function( toolbar ) {
2765 toolbar.view = new media.view.Toolbar.Embed({
2770 galleryEditToolbar: function() {
2771 var editing = this.state().get('editing');
2772 this.toolbar.set( new media.view.Toolbar({
2777 text: editing ? l10n.updateGallery : l10n.insertGallery,
2779 requires: { library: true },
2782 * @fires wp.media.controller.State#update
2785 var controller = this.controller,
2786 state = controller.state();
2789 state.trigger( 'update', state.get('library') );
2791 // Restore and reset the default state.
2792 controller.setState( controller.options.state );
2800 galleryAddToolbar: function() {
2801 this.toolbar.set( new media.view.Toolbar({
2806 text: l10n.addToGallery,
2808 requires: { selection: true },
2811 * @fires wp.media.controller.State#reset
2814 var controller = this.controller,
2815 state = controller.state(),
2816 edit = controller.state('gallery-edit');
2818 edit.get('library').add( state.get('selection').models );
2819 state.trigger('reset');
2820 controller.setState('gallery-edit');
2827 playlistEditToolbar: function() {
2828 var editing = this.state().get('editing');
2829 this.toolbar.set( new media.view.Toolbar({
2834 text: editing ? l10n.updatePlaylist : l10n.insertPlaylist,
2836 requires: { library: true },
2839 * @fires wp.media.controller.State#update
2842 var controller = this.controller,
2843 state = controller.state();
2846 state.trigger( 'update', state.get('library') );
2848 // Restore and reset the default state.
2849 controller.setState( controller.options.state );
2857 playlistAddToolbar: function() {
2858 this.toolbar.set( new media.view.Toolbar({
2863 text: l10n.addToPlaylist,
2865 requires: { selection: true },
2868 * @fires wp.media.controller.State#reset
2871 var controller = this.controller,
2872 state = controller.state(),
2873 edit = controller.state('playlist-edit');
2875 edit.get('library').add( state.get('selection').models );
2876 state.trigger('reset');
2877 controller.setState('playlist-edit');
2884 videoPlaylistEditToolbar: function() {
2885 var editing = this.state().get('editing');
2886 this.toolbar.set( new media.view.Toolbar({
2891 text: editing ? l10n.updateVideoPlaylist : l10n.insertVideoPlaylist,
2893 requires: { library: true },
2896 var controller = this.controller,
2897 state = controller.state(),
2898 library = state.get('library');
2900 library.type = 'video';
2903 state.trigger( 'update', library );
2905 // Restore and reset the default state.
2906 controller.setState( controller.options.state );
2914 videoPlaylistAddToolbar: function() {
2915 this.toolbar.set( new media.view.Toolbar({
2920 text: l10n.addToVideoPlaylist,
2922 requires: { selection: true },
2925 var controller = this.controller,
2926 state = controller.state(),
2927 edit = controller.state('video-playlist-edit');
2929 edit.get('library').add( state.get('selection').models );
2930 state.trigger('reset');
2931 controller.setState('video-playlist-edit');
2940 * wp.media.view.MediaFrame.ImageDetails
2943 * @augments wp.media.view.MediaFrame.Select
2944 * @augments wp.media.view.MediaFrame
2945 * @augments wp.media.view.Frame
2946 * @augments wp.media.View
2947 * @augments wp.Backbone.View
2948 * @augments Backbone.View
2949 * @mixes wp.media.controller.StateMachine
2951 media.view.MediaFrame.ImageDetails = media.view.MediaFrame.Select.extend({
2955 menu: 'image-details',
2956 content: 'image-details',
2957 toolbar: 'image-details',
2959 title: l10n.imageDetailsTitle,
2963 initialize: function( options ) {
2964 this.image = new media.model.PostImage( options.metadata );
2965 this.options.selection = new media.model.Selection( this.image.attachment, { multiple: false } );
2966 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
2969 bindHandlers: function() {
2970 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
2971 this.on( 'menu:create:image-details', this.createMenu, this );
2972 this.on( 'content:create:image-details', this.imageDetailsContent, this );
2973 this.on( 'content:render:edit-image', this.editImageContent, this );
2974 this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this );
2975 // override the select toolbar
2976 this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this );
2979 createStates: function() {
2981 new media.controller.ImageDetails({
2985 new media.controller.ReplaceImage({
2986 id: 'replace-image',
2987 library: media.query( { type: 'image' } ),
2990 title: l10n.imageReplaceTitle,
2993 displaySettings: true
2995 new media.controller.EditImage( {
2997 selection: this.options.selection
3002 imageDetailsContent: function( options ) {
3003 options.view = new media.view.ImageDetails({
3005 model: this.state().image,
3006 attachment: this.state().image.attachment
3010 editImageContent: function() {
3011 var state = this.state(),
3012 model = state.get('image'),
3019 view = new media.view.EditImage( { model: model, controller: this } ).render();
3021 this.content.set( view );
3023 // after bringing in the frame, load the actual editor via an ajax call
3028 renderImageDetailsToolbar: function() {
3029 this.toolbar.set( new media.view.Toolbar({
3038 var controller = this.controller,
3039 state = controller.state();
3043 // not sure if we want to use wp.media.string.image which will create a shortcode or
3044 // perhaps wp.html.string to at least to build the <img />
3045 state.trigger( 'update', controller.image.toJSON() );
3047 // Restore and reset the default state.
3048 controller.setState( controller.options.state );
3056 renderReplaceImageToolbar: function() {
3058 lastState = frame.lastState(),
3059 previous = lastState && lastState.id;
3061 this.toolbar.set( new media.view.Toolbar({
3069 frame.setState( previous );
3082 var controller = this.controller,
3083 state = controller.state(),
3084 selection = state.get( 'selection' ),
3085 attachment = selection.single();
3089 controller.image.changeAttachment( attachment, state.display( attachment ) );
3091 // not sure if we want to use wp.media.string.image which will create a shortcode or
3092 // perhaps wp.html.string to at least to build the <img />
3093 state.trigger( 'replace', controller.image.toJSON() );
3095 // Restore and reset the default state.
3096 controller.setState( controller.options.state );
3107 * wp.media.view.Modal
3110 * @augments wp.media.View
3111 * @augments wp.Backbone.View
3112 * @augments Backbone.View
3114 media.view.Modal = media.View.extend({
3116 template: media.template('media-modal'),
3123 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
3124 'keydown': 'keydown'
3127 initialize: function() {
3128 _.defaults( this.options, {
3129 container: document.body,
3135 this.focusManager = new media.view.FocusManager({
3142 prepare: function() {
3144 title: this.options.title
3149 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3151 attach: function() {
3152 if ( this.views.attached ) {
3156 if ( ! this.views.rendered ) {
3160 this.$el.appendTo( this.options.container );
3162 // Manually mark the view as attached and trigger ready.
3163 this.views.attached = true;
3166 return this.propagate('attach');
3170 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3172 detach: function() {
3173 if ( this.$el.is(':visible') ) {
3178 this.views.attached = false;
3179 return this.propagate('detach');
3183 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3187 options = this.options,
3190 if ( $el.is(':visible') ) {
3194 if ( ! this.views.attached ) {
3198 // If the `freeze` option is set, record the window's scroll position.
3199 if ( options.freeze ) {
3201 scrollTop: $( window ).scrollTop()
3205 // Disable page scrolling.
3206 $( 'body' ).addClass( 'modal-open' );
3210 // Try to close the onscreen keyboard
3211 if ( 'ontouchend' in document ) {
3212 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) {
3213 mceEditor.iframeElement.focus();
3214 mceEditor.iframeElement.blur();
3216 setTimeout( function() {
3217 mceEditor.iframeElement.blur();
3224 return this.propagate('open');
3228 * @param {Object} options
3229 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3231 close: function( options ) {
3232 var freeze = this._freeze;
3234 if ( ! this.views.attached || ! this.$el.is(':visible') ) {
3238 // Enable page scrolling.
3239 $( 'body' ).removeClass( 'modal-open' );
3241 // Hide modal and remove restricted media modal tab focus once it's closed
3242 this.$el.hide().undelegate( 'keydown' );
3244 // Put focus back in useful location once modal is closed
3245 $('#wpbody-content').focus();
3247 this.propagate('close');
3249 // If the `freeze` option is set, restore the container's scroll position.
3251 $( window ).scrollTop( freeze.scrollTop );
3254 if ( options && options.escape ) {
3255 this.propagate('escape');
3261 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3263 escape: function() {
3264 return this.close({ escape: true });
3267 * @param {Object} event
3269 escapeHandler: function( event ) {
3270 event.preventDefault();
3275 * @param {Array|Object} content Views to register to '.media-modal-content'
3276 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3278 content: function( content ) {
3279 this.views.set( '.media-modal-content', content );
3284 * Triggers a modal event and if the `propagate` option is set,
3285 * forwards events to the modal's controller.
3287 * @param {string} id
3288 * @returns {wp.media.view.Modal} Returns itself to allow chaining
3290 propagate: function( id ) {
3293 if ( this.options.propagate ) {
3294 this.controller.trigger( id );
3300 * @param {Object} event
3302 keydown: function( event ) {
3303 // Close the modal when escape is pressed.
3304 if ( 27 === event.which && this.$el.is(':visible') ) {
3306 event.stopImmediatePropagation();
3312 * wp.media.view.FocusManager
3315 * @augments wp.media.View
3316 * @augments wp.Backbone.View
3317 * @augments Backbone.View
3319 media.view.FocusManager = media.View.extend({
3322 'keydown': 'constrainTabbing'
3325 focus: function() { // Reset focus on first left menu item
3326 this.$('.media-menu-item').first().focus();
3329 * @param {Object} event
3331 constrainTabbing: function( event ) {
3334 // Look for the tab key.
3335 if ( 9 !== event.keyCode ) {
3339 tabbables = this.$( ':tabbable' );
3341 // Keep tab focus within media modal while it's open
3342 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
3343 tabbables.first().focus();
3345 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
3346 tabbables.last().focus();
3354 * wp.media.view.UploaderWindow
3357 * @augments wp.media.View
3358 * @augments wp.Backbone.View
3359 * @augments Backbone.View
3361 media.view.UploaderWindow = media.View.extend({
3363 className: 'uploader-window',
3364 template: media.template('uploader-window'),
3366 initialize: function() {
3369 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
3371 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
3373 browser: this.$browser,
3377 // Ensure the dropzone is a jQuery collection.
3378 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) {
3379 uploader.dropzone = $( uploader.dropzone );
3382 this.controller.on( 'activate', this.refresh, this );
3384 this.controller.on( 'detach', function() {
3385 this.$browser.remove();
3389 refresh: function() {
3390 if ( this.uploader ) {
3391 this.uploader.refresh();
3396 var postId = media.view.settings.post.id,
3399 // If the uploader already exists, bail.
3400 if ( this.uploader ) {
3405 this.options.uploader.params.post_id = postId;
3407 this.uploader = new wp.Uploader( this.options.uploader );
3409 dropzone = this.uploader.dropzone;
3410 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
3411 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
3413 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) );
3416 _ready: function() {
3417 this.controller.trigger( 'uploader:ready' );
3421 var $el = this.$el.show();
3423 // Ensure that the animation is triggered by waiting until
3424 // the transparent element is painted into the DOM.
3425 _.defer( function() {
3426 $el.css({ opacity: 1 });
3431 var $el = this.$el.css({ opacity: 0 });
3433 media.transition( $el ).done( function() {
3434 // Transition end events are subject to race conditions.
3435 // Make sure that the value is set as intended.
3436 if ( '0' === $el.css('opacity') ) {
3441 // https://core.trac.wordpress.org/ticket/27341
3442 _.delay( function() {
3443 if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
3451 * wp.media.view.EditorUploader
3454 * @augments wp.media.View
3455 * @augments wp.Backbone.View
3456 * @augments Backbone.View
3458 media.view.EditorUploader = media.View.extend({
3460 className: 'uploader-editor',
3461 template: media.template( 'uploader-editor' ),
3464 overContainer: false,
3465 overDropzone: false,
3468 initialize: function() {
3471 this.initialized = false;
3473 // Bail if not enabled or UA does not support drag'n'drop or File API.
3474 if ( ! window.tinyMCEPreInit || ! window.tinyMCEPreInit.dragDropUpload || ! this.browserSupport() ) {
3478 this.$document = $(document);
3479 this.dropzones = [];
3482 this.$document.on( 'drop', '.uploader-editor', _.bind( this.drop, this ) );
3483 this.$document.on( 'dragover', '.uploader-editor', _.bind( this.dropzoneDragover, this ) );
3484 this.$document.on( 'dragleave', '.uploader-editor', _.bind( this.dropzoneDragleave, this ) );
3485 this.$document.on( 'click', '.uploader-editor', _.bind( this.click, this ) );
3487 this.$document.on( 'dragover', _.bind( this.containerDragover, this ) );
3488 this.$document.on( 'dragleave', _.bind( this.containerDragleave, this ) );
3490 this.$document.on( 'dragstart dragend drop', function( event ) {
3491 self.localDrag = event.type === 'dragstart';
3494 this.initialized = true;
3498 browserSupport: function() {
3499 var supports = false, div = document.createElement('div');
3501 supports = ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div );
3502 supports = supports && !! ( window.File && window.FileList && window.FileReader );
3506 isDraggingFile: function( event ) {
3507 if ( this.draggingFile !== null ) {
3508 return this.draggingFile;
3511 if ( _.isUndefined( event.originalEvent ) || _.isUndefined( event.originalEvent.dataTransfer ) ) {
3515 this.draggingFile = _.indexOf( event.originalEvent.dataTransfer.types, 'Files' ) > -1 &&
3516 _.indexOf( event.originalEvent.dataTransfer.types, 'text/plain' ) === -1;
3518 return this.draggingFile;
3521 refresh: function( e ) {
3523 for ( dropzone_id in this.dropzones ) {
3524 // Hide the dropzones only if dragging has left the screen.
3525 this.dropzones[ dropzone_id ].toggle( this.overContainer || this.overDropzone );
3528 if ( ! _.isUndefined( e ) ) {
3529 $( e.target ).closest( '.uploader-editor' ).toggleClass( 'droppable', this.overDropzone );
3532 if ( ! this.overContainer && ! this.overDropzone ) {
3533 this.draggingFile = null;
3539 render: function() {
3540 if ( ! this.initialized ) {
3544 media.View.prototype.render.apply( this, arguments );
3545 $( '.wp-editor-wrap, #wp-fullscreen-body' ).each( _.bind( this.attach, this ) );
3549 attach: function( index, editor ) {
3550 // Attach a dropzone to an editor.
3551 var dropzone = this.$el.clone();
3552 this.dropzones.push( dropzone );
3553 $( editor ).append( dropzone );
3557 drop: function( event ) {
3560 this.containerDragleave( event );
3561 this.dropzoneDragleave( event );
3563 this.files = event.originalEvent.dataTransfer.files;
3564 if ( this.files.length < 1 ) {
3568 // Set the active editor to the drop target.
3569 $wrap = $( event.target ).parents( '.wp-editor-wrap' );
3570 if ( $wrap.length > 0 && $wrap[0].id ) {
3571 window.wpActiveEditor = $wrap[0].id.slice( 3, -5 );
3574 if ( ! this.workflow ) {
3575 this.workflow = wp.media.editor.open( 'content', {
3578 title: wp.media.view.l10n.addMedia,
3581 this.workflow.on( 'uploader:ready', this.addFiles, this );
3583 this.workflow.state().reset();
3584 this.addFiles.apply( this );
3585 this.workflow.open();
3591 addFiles: function() {
3592 if ( this.files.length ) {
3593 this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) );
3599 containerDragover: function( event ) {
3600 if ( this.localDrag || ! this.isDraggingFile( event ) ) {
3604 this.overContainer = true;
3608 containerDragleave: function() {
3609 this.overContainer = false;
3611 // Throttle dragleave because it's called when bouncing from some elements to others.
3612 _.delay( _.bind( this.refresh, this ), 50 );
3615 dropzoneDragover: function( event ) {
3616 if ( this.localDrag || ! this.isDraggingFile( event ) ) {
3620 this.overDropzone = true;
3621 this.refresh( event );
3625 dropzoneDragleave: function( e ) {
3626 this.overDropzone = false;
3627 _.delay( _.bind( this.refresh, this, e ), 50 );
3630 click: function( e ) {
3631 // In the rare case where the dropzone gets stuck, hide it on click.
3632 this.containerDragleave( e );
3633 this.dropzoneDragleave( e );
3634 this.localDrag = false;
3639 * wp.media.view.UploaderInline
3642 * @augments wp.media.View
3643 * @augments wp.Backbone.View
3644 * @augments Backbone.View
3646 media.view.UploaderInline = media.View.extend({
3648 className: 'uploader-inline',
3649 template: media.template('uploader-inline'),
3652 'click .close': 'hide'
3655 initialize: function() {
3656 _.defaults( this.options, {
3662 if ( ! this.options.$browser && this.controller.uploader ) {
3663 this.options.$browser = this.controller.uploader.$browser;
3666 if ( _.isUndefined( this.options.postId ) ) {
3667 this.options.postId = media.view.settings.post.id;
3670 if ( this.options.status ) {
3671 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
3672 controller: this.controller
3677 prepare: function() {
3678 var suggestedWidth = this.controller.state().get('suggestedWidth'),
3679 suggestedHeight = this.controller.state().get('suggestedHeight'),
3682 data.message = this.options.message;
3683 data.canClose = this.options.canClose;
3685 if ( suggestedWidth && suggestedHeight ) {
3686 data.suggestedWidth = suggestedWidth;
3687 data.suggestedHeight = suggestedHeight;
3693 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
3695 dispose: function() {
3696 if ( this.disposing ) {
3698 * call 'dispose' directly on the parent class
3700 return media.View.prototype.dispose.apply( this, arguments );
3703 // Run remove on `dispose`, so we can be sure to refresh the
3704 // uploader with a view-less DOM. Track whether we're disposing
3705 // so we don't trigger an infinite loop.
3706 this.disposing = true;
3707 return this.remove();
3710 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
3712 remove: function() {
3714 * call 'remove' directly on the parent class
3716 var result = media.View.prototype.remove.apply( this, arguments );
3718 _.defer( _.bind( this.refresh, this ) );
3722 refresh: function() {
3723 var uploader = this.controller.uploader;
3730 * @returns {wp.media.view.UploaderInline}
3733 var $browser = this.options.$browser,
3736 if ( this.controller.uploader ) {
3737 $placeholder = this.$('.browser');
3739 // Check if we've already replaced the placeholder.
3740 if ( $placeholder[0] === $browser[0] ) {
3744 $browser.detach().text( $placeholder.text() );
3745 $browser[0].className = $placeholder[0].className;
3746 $placeholder.replaceWith( $browser.show() );
3753 this.$el.removeClass( 'hidden' );
3756 this.$el.addClass( 'hidden' );
3762 * wp.media.view.UploaderStatus
3765 * @augments wp.media.View
3766 * @augments wp.Backbone.View
3767 * @augments Backbone.View
3769 media.view.UploaderStatus = media.View.extend({
3770 className: 'media-uploader-status',
3771 template: media.template('uploader-status'),
3774 'click .upload-dismiss-errors': 'dismiss'
3777 initialize: function() {
3778 this.queue = wp.Uploader.queue;
3779 this.queue.on( 'add remove reset', this.visibility, this );
3780 this.queue.on( 'add remove reset change:percent', this.progress, this );
3781 this.queue.on( 'add remove reset change:uploading', this.info, this );
3783 this.errors = wp.Uploader.errors;
3784 this.errors.reset();
3785 this.errors.on( 'add remove reset', this.visibility, this );
3786 this.errors.on( 'add', this.error, this );
3789 * @global wp.Uploader
3790 * @returns {wp.media.view.UploaderStatus}
3792 dispose: function() {
3793 wp.Uploader.queue.off( null, null, this );
3795 * call 'dispose' directly on the parent class
3797 media.View.prototype.dispose.apply( this, arguments );
3801 visibility: function() {
3802 this.$el.toggleClass( 'uploading', !! this.queue.length );
3803 this.$el.toggleClass( 'errors', !! this.errors.length );
3804 this.$el.toggle( !! this.queue.length || !! this.errors.length );
3809 '$bar': '.media-progress-bar div',
3810 '$index': '.upload-index',
3811 '$total': '.upload-total',
3812 '$filename': '.upload-filename'
3813 }, function( selector, key ) {
3814 this[ key ] = this.$( selector );
3822 progress: function() {
3823 var queue = this.queue,
3826 if ( ! $bar || ! queue.length ) {
3830 $bar.width( ( queue.reduce( function( memo, attachment ) {
3831 if ( ! attachment.get('uploading') ) {
3835 var percent = attachment.get('percent');
3836 return memo + ( _.isNumber( percent ) ? percent : 100 );
3837 }, 0 ) / queue.length ) + '%' );
3841 var queue = this.queue,
3844 if ( ! queue.length ) {
3848 active = this.queue.find( function( attachment, i ) {
3850 return attachment.get('uploading');
3853 this.$index.text( index + 1 );
3854 this.$total.text( queue.length );
3855 this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
3858 * @param {string} filename
3861 filename: function( filename ) {
3862 return media.truncate( _.escape( filename ), 24 );
3865 * @param {Backbone.Model} error
3867 error: function( error ) {
3868 this.views.add( '.upload-errors', new media.view.UploaderStatusError({
3869 filename: this.filename( error.get('file').name ),
3870 message: error.get('message')
3875 * @global wp.Uploader
3877 * @param {Object} event
3879 dismiss: function( event ) {
3880 var errors = this.views.get('.upload-errors');
3882 event.preventDefault();
3885 _.invoke( errors, 'remove' );
3887 wp.Uploader.errors.reset();
3892 * wp.media.view.UploaderStatusError
3895 * @augments wp.media.View
3896 * @augments wp.Backbone.View
3897 * @augments Backbone.View
3899 media.view.UploaderStatusError = media.View.extend({
3900 className: 'upload-error',
3901 template: media.template('uploader-status-error')
3905 * wp.media.view.Toolbar
3908 * @augments wp.media.View
3909 * @augments wp.Backbone.View
3910 * @augments Backbone.View
3912 media.view.Toolbar = media.View.extend({
3914 className: 'media-toolbar',
3916 initialize: function() {
3917 var state = this.controller.state(),
3918 selection = this.selection = state.get('selection'),
3919 library = this.library = state.get('library');
3923 // The toolbar is composed of two `PriorityList` views.
3924 this.primary = new media.view.PriorityList();
3925 this.secondary = new media.view.PriorityList();
3926 this.primary.$el.addClass('media-toolbar-primary search-form');
3927 this.secondary.$el.addClass('media-toolbar-secondary');
3929 this.views.set([ this.secondary, this.primary ]);
3931 if ( this.options.items ) {
3932 this.set( this.options.items, { silent: true });
3935 if ( ! this.options.silent ) {
3940 selection.on( 'add remove reset', this.refresh, this );
3944 library.on( 'add remove reset', this.refresh, this );
3948 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining
3950 dispose: function() {
3951 if ( this.selection ) {
3952 this.selection.off( null, null, this );
3955 if ( this.library ) {
3956 this.library.off( null, null, this );
3959 * call 'dispose' directly on the parent class
3961 return media.View.prototype.dispose.apply( this, arguments );
3969 * @param {string} id
3970 * @param {Backbone.View|Object} view
3971 * @param {Object} [options={}]
3972 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
3974 set: function( id, view, options ) {
3976 options = options || {};
3978 // Accept an object with an `id` : `view` mapping.
3979 if ( _.isObject( id ) ) {
3980 _.each( id, function( view, id ) {
3981 this.set( id, view, { silent: true });
3985 if ( ! ( view instanceof Backbone.View ) ) {
3986 view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
3987 view = new media.view.Button( view ).render();
3990 view.controller = view.controller || this.controller;
3992 this._views[ id ] = view;
3994 list = view.options.priority < 0 ? 'secondary' : 'primary';
3995 this[ list ].set( id, view, options );
3998 if ( ! options.silent ) {
4005 * @param {string} id
4006 * @returns {wp.media.view.Button}
4008 get: function( id ) {
4009 return this._views[ id ];
4012 * @param {string} id
4013 * @param {Object} options
4014 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
4016 unset: function( id, options ) {
4017 delete this._views[ id ];
4018 this.primary.unset( id, options );
4019 this.secondary.unset( id, options );
4021 if ( ! options || ! options.silent ) {
4027 refresh: function() {
4028 var state = this.controller.state(),
4029 library = state.get('library'),
4030 selection = state.get('selection');
4032 _.each( this._views, function( button ) {
4033 if ( ! button.model || ! button.options || ! button.options.requires ) {
4037 var requires = button.options.requires,
4040 // Prevent insertion of attachments if any of them are still uploading
4041 disabled = _.some( selection.models, function( attachment ) {
4042 return attachment.get('uploading') === true;
4045 if ( requires.selection && selection && ! selection.length ) {
4047 } else if ( requires.library && library && ! library.length ) {
4050 button.model.set( 'disabled', disabled );
4056 * wp.media.view.Toolbar.Select
4059 * @augments wp.media.view.Toolbar
4060 * @augments wp.media.View
4061 * @augments wp.Backbone.View
4062 * @augments Backbone.View
4064 media.view.Toolbar.Select = media.view.Toolbar.extend({
4065 initialize: function() {
4066 var options = this.options;
4068 _.bindAll( this, 'clickSelect' );
4070 _.defaults( options, {
4077 // Does the button rely on the selection?
4083 options.items = _.defaults( options.items || {}, {
4088 click: this.clickSelect,
4089 requires: options.requires
4093 * call 'initialize' directly on the parent class
4095 media.view.Toolbar.prototype.initialize.apply( this, arguments );
4098 clickSelect: function() {
4099 var options = this.options,
4100 controller = this.controller;
4102 if ( options.close ) {
4106 if ( options.event ) {
4107 controller.state().trigger( options.event );
4110 if ( options.state ) {
4111 controller.setState( options.state );
4114 if ( options.reset ) {
4121 * wp.media.view.Toolbar.Embed
4124 * @augments wp.media.view.Toolbar.Select
4125 * @augments wp.media.view.Toolbar
4126 * @augments wp.media.View
4127 * @augments wp.Backbone.View
4128 * @augments Backbone.View
4130 media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
4131 initialize: function() {
4132 _.defaults( this.options, {
4133 text: l10n.insertIntoPost,
4137 * call 'initialize' directly on the parent class
4139 media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
4142 refresh: function() {
4143 var url = this.controller.state().props.get('url');
4144 this.get('select').model.set( 'disabled', ! url || url === 'http://' );
4146 * call 'refresh' directly on the parent class
4148 media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
4153 * wp.media.view.Button
4156 * @augments wp.media.View
4157 * @augments wp.Backbone.View
4158 * @augments Backbone.View
4160 media.view.Button = media.View.extend({
4162 className: 'media-button',
4163 attributes: { href: '#' },
4176 initialize: function() {
4178 * Create a model with the provided `defaults`.
4180 * @member {Backbone.Model}
4182 this.model = new Backbone.Model( this.defaults );
4184 // If any of the `options` have a key from `defaults`, apply its
4185 // value to the `model` and remove it from the `options object.
4186 _.each( this.defaults, function( def, key ) {
4187 var value = this.options[ key ];
4188 if ( _.isUndefined( value ) ) {
4192 this.model.set( key, value );
4193 delete this.options[ key ];
4196 this.model.on( 'change', this.render, this );
4199 * @returns {wp.media.view.Button} Returns itself to allow chaining
4201 render: function() {
4202 var classes = [ 'button', this.className ],
4203 model = this.model.toJSON();
4205 if ( model.style ) {
4206 classes.push( 'button-' + model.style );
4210 classes.push( 'button-' + model.size );
4213 classes = _.uniq( classes.concat( this.options.classes ) );
4214 this.el.className = classes.join(' ');
4216 this.$el.attr( 'disabled', model.disabled );
4217 this.$el.text( this.model.get('text') );
4222 * @param {Object} event
4224 click: function( event ) {
4225 if ( '#' === this.attributes.href ) {
4226 event.preventDefault();
4229 if ( this.options.click && ! this.model.get('disabled') ) {
4230 this.options.click.apply( this, arguments );
4236 * wp.media.view.ButtonGroup
4239 * @augments wp.media.View
4240 * @augments wp.Backbone.View
4241 * @augments Backbone.View
4243 media.view.ButtonGroup = media.View.extend({
4245 className: 'button-group button-large media-button-group',
4247 initialize: function() {
4249 * @member {wp.media.view.Button[]}
4251 this.buttons = _.map( this.options.buttons || [], function( button ) {
4252 if ( button instanceof Backbone.View ) {
4255 return new media.view.Button( button ).render();
4259 delete this.options.buttons;
4261 if ( this.options.classes ) {
4262 this.$el.addClass( this.options.classes );
4267 * @returns {wp.media.view.ButtonGroup}
4269 render: function() {
4270 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
4276 * wp.media.view.PriorityList
4279 * @augments wp.media.View
4280 * @augments wp.Backbone.View
4281 * @augments Backbone.View
4283 media.view.PriorityList = media.View.extend({
4286 initialize: function() {
4289 this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
4290 delete this.options.views;
4292 if ( ! this.options.silent ) {
4297 * @param {string} id
4298 * @param {wp.media.View|Object} view
4299 * @param {Object} options
4300 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining
4302 set: function( id, view, options ) {
4303 var priority, views, index;
4305 options = options || {};
4307 // Accept an object with an `id` : `view` mapping.
4308 if ( _.isObject( id ) ) {
4309 _.each( id, function( view, id ) {
4310 this.set( id, view );
4315 if ( ! (view instanceof Backbone.View) ) {
4316 view = this.toView( view, id, options );
4318 view.controller = view.controller || this.controller;
4322 priority = view.options.priority || 10;
4323 views = this.views.get() || [];
4325 _.find( views, function( existing, i ) {
4326 if ( existing.options.priority > priority ) {
4332 this._views[ id ] = view;
4333 this.views.add( view, {
4334 at: _.isNumber( index ) ? index : views.length || 0
4340 * @param {string} id
4341 * @returns {wp.media.View}
4343 get: function( id ) {
4344 return this._views[ id ];
4347 * @param {string} id
4348 * @returns {wp.media.view.PriorityList}
4350 unset: function( id ) {
4351 var view = this.get( id );
4357 delete this._views[ id ];
4361 * @param {Object} options
4362 * @returns {wp.media.View}
4364 toView: function( options ) {
4365 return new media.View( options );
4370 * wp.media.view.MenuItem
4373 * @augments wp.media.View
4374 * @augments wp.Backbone.View
4375 * @augments Backbone.View
4377 media.view.MenuItem = media.View.extend({
4379 className: 'media-menu-item',
4389 * @param {Object} event
4391 _click: function( event ) {
4392 var clickOverride = this.options.click;
4395 event.preventDefault();
4398 if ( clickOverride ) {
4399 clickOverride.call( this );
4404 // When selecting a tab along the left side,
4405 // focus should be transferred into the main panel
4406 if ( ! isTouchDevice ) {
4407 $('.media-frame-content input').first().focus();
4412 var state = this.options.state;
4415 this.controller.setState( state );
4416 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
4420 * @returns {wp.media.view.MenuItem} returns itself to allow chaining
4422 render: function() {
4423 var options = this.options;
4425 if ( options.text ) {
4426 this.$el.text( options.text );
4427 } else if ( options.html ) {
4428 this.$el.html( options.html );
4436 * wp.media.view.Menu
4439 * @augments wp.media.view.PriorityList
4440 * @augments wp.media.View
4441 * @augments wp.Backbone.View
4442 * @augments Backbone.View
4444 media.view.Menu = media.view.PriorityList.extend({
4446 className: 'media-menu',
4448 ItemView: media.view.MenuItem,
4451 /* TODO: alternatively hide on any click anywhere
4457 this.$el.removeClass( 'visible' );
4462 * @param {Object} options
4463 * @param {string} id
4464 * @returns {wp.media.View}
4466 toView: function( options, id ) {
4467 options = options || {};
4468 options[ this.property ] = options[ this.property ] || id;
4469 return new this.ItemView( options ).render();
4474 * call 'ready' directly on the parent class
4476 media.view.PriorityList.prototype.ready.apply( this, arguments );
4482 * call 'set' directly on the parent class
4484 media.view.PriorityList.prototype.set.apply( this, arguments );
4490 * call 'unset' directly on the parent class
4492 media.view.PriorityList.prototype.unset.apply( this, arguments );
4496 visibility: function() {
4497 var region = this.region,
4498 view = this.controller[ region ].get(),
4499 views = this.views.get(),
4500 hide = ! views || views.length < 2;
4502 if ( this === view ) {
4503 this.controller.$el.toggleClass( 'hide-' + region, hide );
4507 * @param {string} id
4509 select: function( id ) {
4510 var view = this.get( id );
4517 view.$el.addClass('active');
4520 deselect: function() {
4521 this.$el.children().removeClass('active');
4524 hide: function( id ) {
4525 var view = this.get( id );
4531 view.$el.addClass('hidden');
4534 show: function( id ) {
4535 var view = this.get( id );
4541 view.$el.removeClass('hidden');
4546 * wp.media.view.RouterItem
4549 * @augments wp.media.view.MenuItem
4550 * @augments wp.media.View
4551 * @augments wp.Backbone.View
4552 * @augments Backbone.View
4554 media.view.RouterItem = media.view.MenuItem.extend({
4556 * On click handler to activate the content region's corresponding mode.
4559 var contentMode = this.options.contentMode;
4560 if ( contentMode ) {
4561 this.controller.content.mode( contentMode );
4567 * wp.media.view.Router
4570 * @augments wp.media.view.Menu
4571 * @augments wp.media.view.PriorityList
4572 * @augments wp.media.View
4573 * @augments wp.Backbone.View
4574 * @augments Backbone.View
4576 media.view.Router = media.view.Menu.extend({
4578 className: 'media-router',
4579 property: 'contentMode',
4580 ItemView: media.view.RouterItem,
4583 initialize: function() {
4584 this.controller.on( 'content:render', this.update, this );
4586 * call 'initialize' directly on the parent class
4588 media.view.Menu.prototype.initialize.apply( this, arguments );
4591 update: function() {
4592 var mode = this.controller.content.mode();
4594 this.select( mode );
4600 * wp.media.view.Sidebar
4603 * @augments wp.media.view.PriorityList
4604 * @augments wp.media.View
4605 * @augments wp.Backbone.View
4606 * @augments Backbone.View
4608 media.view.Sidebar = media.view.PriorityList.extend({
4609 className: 'media-sidebar'
4613 * wp.media.view.Attachment
4616 * @augments wp.media.View
4617 * @augments wp.Backbone.View
4618 * @augments Backbone.View
4620 media.view.Attachment = media.View.extend({
4622 className: 'attachment',
4623 template: media.template('attachment'),
4625 attributes: function() {
4629 'aria-label': this.model.get( 'title' ),
4630 'aria-checked': false,
4631 'data-id': this.model.get( 'id' )
4636 'click .js--select-attachment': 'toggleSelectionHandler',
4637 'change [data-setting]': 'updateSetting',
4638 'change [data-setting] input': 'updateSetting',
4639 'change [data-setting] select': 'updateSetting',
4640 'change [data-setting] textarea': 'updateSetting',
4641 'click .close': 'removeFromLibrary',
4642 'click .check': 'checkClickHandler',
4643 'click a': 'preventDefault',
4644 'keydown': 'toggleSelectionHandler'
4649 initialize: function() {
4650 var selection = this.options.selection,
4651 options = _.defaults( this.options, {
4652 rerenderOnModelChange: true
4655 if ( options.rerenderOnModelChange ) {
4656 this.model.on( 'change', this.render, this );
4658 this.model.on( 'change:percent', this.progress, this );
4660 this.model.on( 'change:title', this._syncTitle, this );
4661 this.model.on( 'change:caption', this._syncCaption, this );
4662 this.model.on( 'change:artist', this._syncArtist, this );
4663 this.model.on( 'change:album', this._syncAlbum, this );
4665 // Update the selection.
4666 this.model.on( 'add', this.select, this );
4667 this.model.on( 'remove', this.deselect, this );
4669 selection.on( 'reset', this.updateSelect, this );
4670 // Update the model's details view.
4671 this.model.on( 'selection:single selection:unsingle', this.details, this );
4672 this.details( this.model, this.controller.state().get('selection') );
4676 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
4678 dispose: function() {
4679 var selection = this.options.selection;
4681 // Make sure all settings are saved before removing the view.
4685 selection.off( null, null, this );
4688 * call 'dispose' directly on the parent class
4690 media.View.prototype.dispose.apply( this, arguments );
4694 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
4696 render: function() {
4697 var options = _.defaults( this.model.toJSON(), {
4698 orientation: 'landscape',
4714 options.buttons = this.buttons;
4715 options.describe = this.controller.state().get('describe');
4717 if ( 'image' === options.type ) {
4718 options.size = this.imageSize();
4722 if ( options.nonces ) {
4723 options.can.remove = !! options.nonces['delete'];
4724 options.can.save = !! options.nonces.update;
4727 if ( this.controller.state().get('allowLocalEdits') ) {
4728 options.allowLocalEdits = true;
4731 if ( options.uploading && ! options.percent ) {
4732 options.percent = 0;
4735 this.views.detach();
4736 this.$el.html( this.template( options ) );
4738 this.$el.toggleClass( 'uploading', options.uploading );
4740 if ( options.uploading ) {
4741 this.$bar = this.$('.media-progress-bar div');
4746 // Check if the model is selected.
4747 this.updateSelect();
4749 // Update the save status.
4752 this.views.render();
4757 progress: function() {
4758 if ( this.$bar && this.$bar.length ) {
4759 this.$bar.width( this.model.get('percent') + '%' );
4764 * @param {Object} event
4766 toggleSelectionHandler: function( event ) {
4769 // Don't do anything inside inputs.
4770 if ( 'INPUT' === event.target.nodeName ) {
4774 // Catch arrow events
4775 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
4776 this.controller.trigger( 'attachment:keydown:arrow', event );
4780 // Catch enter and space events
4781 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
4785 // In the grid view, bubble up an edit:attachment event to the controller.
4786 if ( this.controller.isModeActive( 'grid' ) ) {
4787 if ( this.controller.isModeActive( 'edit' ) ) {
4788 // Pass the current target to restore focus when closing
4789 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
4791 // Don't scroll the view and don't attempt to submit anything.
4792 event.stopPropagation();
4796 if ( this.controller.isModeActive( 'select' ) ) {
4801 if ( event.shiftKey ) {
4803 } else if ( event.ctrlKey || event.metaKey ) {
4807 this.toggleSelection({
4811 this.controller.trigger( 'selection:toggle' );
4813 // Don't scroll the view and don't attempt to submit anything.
4814 event.stopPropagation();
4817 * @param {Object} options
4819 toggleSelection: function( options ) {
4820 var collection = this.collection,
4821 selection = this.options.selection,
4823 method = options && options.method,
4824 single, models, singleIndex, modelIndex;
4826 if ( ! selection ) {
4830 single = selection.single();
4831 method = _.isUndefined( method ) ? selection.multiple : method;
4833 // If the `method` is set to `between`, select all models that
4834 // exist between the current and the selected model.
4835 if ( 'between' === method && single && selection.multiple ) {
4836 // If the models are the same, short-circuit.
4837 if ( single === model ) {
4841 singleIndex = collection.indexOf( single );
4842 modelIndex = collection.indexOf( this.model );
4844 if ( singleIndex < modelIndex ) {
4845 models = collection.models.slice( singleIndex, modelIndex + 1 );
4847 models = collection.models.slice( modelIndex, singleIndex + 1 );
4850 selection.add( models );
4851 selection.single( model );
4854 // If the `method` is set to `toggle`, just flip the selection
4855 // status, regardless of whether the model is the single model.
4856 } else if ( 'toggle' === method ) {
4857 selection[ this.selected() ? 'remove' : 'add' ]( model );
4858 selection.single( model );
4860 } else if ( 'add' === method ) {
4861 selection.add( model );
4862 selection.single( model );
4866 // Fixes bug that loses focus when selecting a featured image
4871 if ( method !== 'add' ) {
4875 if ( this.selected() ) {
4876 // If the model is the single model, remove it.
4877 // If it is not the same as the single model,
4878 // it now becomes the single model.
4879 selection[ single === model ? 'remove' : 'single' ]( model );
4881 // If the model is not selected, run the `method` on the
4882 // selection. By default, we `reset` the selection, but the
4883 // `method` can be set to `add` the model to the selection.
4884 selection[ method ]( model );
4885 selection.single( model );
4889 updateSelect: function() {
4890 this[ this.selected() ? 'select' : 'deselect' ]();
4893 * @returns {unresolved|Boolean}
4895 selected: function() {
4896 var selection = this.options.selection;
4898 return !! selection.get( this.model.cid );
4902 * @param {Backbone.Model} model
4903 * @param {Backbone.Collection} collection
4905 select: function( model, collection ) {
4906 var selection = this.options.selection,
4907 controller = this.controller;
4909 // Check if a selection exists and if it's the collection provided.
4910 // If they're not the same collection, bail; we're in another
4911 // selection's event loop.
4912 if ( ! selection || ( collection && collection !== selection ) ) {
4916 // Bail if the model is already selected.
4917 if ( this.$el.hasClass( 'selected' ) ) {
4921 // Add 'selected' class to model, set aria-checked to true.
4922 this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
4923 // Make the checkbox tabable, except in media grid (bulk select mode).
4924 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
4925 this.$( '.check' ).attr( 'tabindex', '0' );
4929 * @param {Backbone.Model} model
4930 * @param {Backbone.Collection} collection
4932 deselect: function( model, collection ) {
4933 var selection = this.options.selection;
4935 // Check if a selection exists and if it's the collection provided.
4936 // If they're not the same collection, bail; we're in another
4937 // selection's event loop.
4938 if ( ! selection || ( collection && collection !== selection ) ) {
4941 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
4942 .find( '.check' ).attr( 'tabindex', '-1' );
4945 * @param {Backbone.Model} model
4946 * @param {Backbone.Collection} collection
4948 details: function( model, collection ) {
4949 var selection = this.options.selection,
4952 if ( selection !== collection ) {
4956 details = selection.single();
4957 this.$el.toggleClass( 'details', details === this.model );
4960 * @param {Object} event
4962 preventDefault: function( event ) {
4963 event.preventDefault();
4966 * @param {string} size
4969 imageSize: function( size ) {
4970 var sizes = this.model.get('sizes');
4972 size = size || 'medium';
4974 // Use the provided image size if possible.
4975 if ( sizes && sizes[ size ] ) {
4976 return _.clone( sizes[ size ] );
4979 url: this.model.get('url'),
4980 width: this.model.get('width'),
4981 height: this.model.get('height'),
4982 orientation: this.model.get('orientation')
4987 * @param {Object} event
4989 updateSetting: function( event ) {
4990 var $setting = $( event.target ).closest('[data-setting]'),
4993 if ( ! $setting.length ) {
4997 setting = $setting.data('setting');
4998 value = event.target.value;
5000 if ( this.model.get( setting ) !== value ) {
5001 this.save( setting, value );
5006 * Pass all the arguments to the model's save method.
5008 * Records the aggregate status of all save requests and updates the
5009 * view's classes accordingly.
5013 save = this._save = this._save || { status: 'ready' },
5014 request = this.model.save.apply( this.model, arguments ),
5015 requests = save.requests ? $.when( request, save.requests ) : request;
5017 // If we're waiting to remove 'Saved.', stop.
5018 if ( save.savedTimer ) {
5019 clearTimeout( save.savedTimer );
5022 this.updateSave('waiting');
5023 save.requests = requests;
5024 requests.always( function() {
5025 // If we've performed another request since this one, bail.
5026 if ( save.requests !== requests ) {
5030 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
5031 save.savedTimer = setTimeout( function() {
5032 view.updateSave('ready');
5033 delete save.savedTimer;
5038 * @param {string} status
5039 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
5041 updateSave: function( status ) {
5042 var save = this._save = this._save || { status: 'ready' };
5044 if ( status && status !== save.status ) {
5045 this.$el.removeClass( 'save-' + save.status );
5046 save.status = status;
5049 this.$el.addClass( 'save-' + save.status );
5053 updateAll: function() {
5054 var $settings = this.$('[data-setting]'),
5058 changed = _.chain( $settings ).map( function( el ) {
5059 var $input = $('input, textarea, select, [value]', el ),
5062 if ( ! $input.length ) {
5066 setting = $(el).data('setting');
5067 value = $input.val();
5069 // Record the value if it changed.
5070 if ( model.get( setting ) !== value ) {
5071 return [ setting, value ];
5073 }).compact().object().value();
5075 if ( ! _.isEmpty( changed ) ) {
5076 model.save( changed );
5080 * @param {Object} event
5082 removeFromLibrary: function( event ) {
5083 // Stop propagation so the model isn't selected.
5084 event.stopPropagation();
5086 this.collection.remove( this.model );
5090 * Add the model if it isn't in the selection, if it is in the selection,
5093 * @param {[type]} event [description]
5094 * @return {[type]} [description]
5096 checkClickHandler: function ( event ) {
5097 var selection = this.options.selection;
5098 if ( ! selection ) {
5101 event.stopPropagation();
5102 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
5103 selection.remove( this.model );
5104 // Move focus back to the attachment tile (from the check).
5107 selection.add( this.model );
5112 // Ensure settings remain in sync between attachment views.
5114 caption: '_syncCaption',
5115 title: '_syncTitle',
5116 artist: '_syncArtist',
5118 }, function( method, setting ) {
5120 * @param {Backbone.Model} model
5121 * @param {string} value
5122 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
5124 media.view.Attachment.prototype[ method ] = function( model, value ) {
5125 var $setting = this.$('[data-setting="' + setting + '"]');
5127 if ( ! $setting.length ) {
5131 // If the updated value is in sync with the value in the DOM, there
5132 // is no need to re-render. If we're currently editing the value,
5133 // it will automatically be in sync, suppressing the re-render for
5134 // the view we're editing, while updating any others.
5135 if ( value === $setting.find('input, textarea, select, [value]').val() ) {
5139 return this.render();
5144 * wp.media.view.Attachment.Library
5147 * @augments wp.media.view.Attachment
5148 * @augments wp.media.View
5149 * @augments wp.Backbone.View
5150 * @augments Backbone.View
5152 media.view.Attachment.Library = media.view.Attachment.extend({
5159 * wp.media.view.Attachment.EditLibrary
5162 * @augments wp.media.view.Attachment
5163 * @augments wp.media.View
5164 * @augments wp.Backbone.View
5165 * @augments Backbone.View
5167 media.view.Attachment.EditLibrary = media.view.Attachment.extend({
5174 * wp.media.view.Attachments
5177 * @augments wp.media.View
5178 * @augments wp.Backbone.View
5179 * @augments Backbone.View
5181 media.view.Attachments = media.View.extend({
5183 className: 'attachments',
5189 initialize: function() {
5190 this.el.id = _.uniqueId('__attachments-view-');
5192 _.defaults( this.options, {
5193 refreshSensitivity: isTouchDevice ? 300 : 200,
5194 refreshThreshold: 3,
5195 AttachmentView: media.view.Attachment,
5198 idealColumnWidth: $( window ).width() < 640 ? 135 : 150
5201 this._viewsByCid = {};
5202 this.$window = $( window );
5203 this.resizeEvent = 'resize.media-modal-columns';
5205 this.collection.on( 'add', function( attachment ) {
5206 this.views.add( this.createAttachmentView( attachment ), {
5207 at: this.collection.indexOf( attachment )
5211 this.collection.on( 'remove', function( attachment ) {
5212 var view = this._viewsByCid[ attachment.cid ];
5213 delete this._viewsByCid[ attachment.cid ];
5220 this.collection.on( 'reset', this.render, this );
5222 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus );
5224 // Throttle the scroll handler and bind this.
5225 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
5227 this.options.scrollElement = this.options.scrollElement || this.el;
5228 $( this.options.scrollElement ).on( 'scroll', this.scroll );
5230 this.initSortable();
5232 _.bindAll( this, 'setColumns' );
5234 if ( this.options.resize ) {
5235 this.on( 'ready', this.bindEvents );
5236 this.controller.on( 'open', this.setColumns );
5238 // Call this.setColumns() after this view has been rendered in the DOM so
5239 // attachments get proper width applied.
5240 _.defer( this.setColumns, this );
5244 bindEvents: function() {
5245 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) );
5248 attachmentFocus: function() {
5249 this.$( 'li:first' ).focus();
5252 restoreFocus: function() {
5253 this.$( 'li.selected:first' ).focus();
5256 arrowEvent: function( event ) {
5257 var attachments = this.$el.children( 'li' ),
5258 perRow = this.columns,
5259 index = attachments.filter( ':focus' ).index(),
5260 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow );
5262 if ( index === -1 ) {
5267 if ( 37 === event.keyCode ) {
5268 if ( 0 === index ) {
5271 attachments.eq( index - 1 ).focus();
5275 if ( 38 === event.keyCode ) {
5279 attachments.eq( index - perRow ).focus();
5283 if ( 39 === event.keyCode ) {
5284 if ( attachments.length === index ) {
5287 attachments.eq( index + 1 ).focus();
5291 if ( 40 === event.keyCode ) {
5292 if ( Math.ceil( attachments.length / perRow ) === row ) {
5295 attachments.eq( index + perRow ).focus();
5299 dispose: function() {
5300 this.collection.props.off( null, null, this );
5301 if ( this.options.resize ) {
5302 this.$window.off( this.resizeEvent );
5306 * call 'dispose' directly on the parent class
5308 media.View.prototype.dispose.apply( this, arguments );
5311 setColumns: function() {
5312 var prev = this.columns,
5313 width = this.$el.width();
5316 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1;
5318 if ( ! prev || prev !== this.columns ) {
5319 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns );
5324 initSortable: function() {
5325 var collection = this.collection;
5327 if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
5331 this.$el.sortable( _.extend({
5332 // If the `collection` has a `comparator`, disable sorting.
5333 disabled: !! collection.comparator,
5335 // Prevent attachments from being dragged outside the bounding
5337 containment: this.$el,
5339 // Change the position of the attachment as soon as the
5340 // mouse pointer overlaps a thumbnail.
5341 tolerance: 'pointer',
5343 // Record the initial `index` of the dragged model.
5344 start: function( event, ui ) {
5345 ui.item.data('sortableIndexStart', ui.item.index());
5348 // Update the model's index in the collection.
5349 // Do so silently, as the view is already accurate.
5350 update: function( event, ui ) {
5351 var model = collection.at( ui.item.data('sortableIndexStart') ),
5352 comparator = collection.comparator;
5354 // Temporarily disable the comparator to prevent `add`
5356 delete collection.comparator;
5358 // Silently shift the model to its new index.
5359 collection.remove( model, {
5362 collection.add( model, {
5367 // Restore the comparator.
5368 collection.comparator = comparator;
5370 // Fire the `reset` event to ensure other collections sync.
5371 collection.trigger( 'reset', collection );
5373 // If the collection is sorted by menu order,
5374 // update the menu order.
5375 collection.saveMenuOrder();
5377 }, this.options.sortable ) );
5379 // If the `orderby` property is changed on the `collection`,
5380 // check to see if we have a `comparator`. If so, disable sorting.
5381 collection.props.on( 'change:orderby', function() {
5382 this.$el.sortable( 'option', 'disabled', !! collection.comparator );
5385 this.collection.props.on( 'change:orderby', this.refreshSortable, this );
5386 this.refreshSortable();
5389 refreshSortable: function() {
5390 if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
5394 // If the `collection` has a `comparator`, disable sorting.
5395 var collection = this.collection,
5396 orderby = collection.props.get('orderby'),
5397 enabled = 'menuOrder' === orderby || ! collection.comparator;
5399 this.$el.sortable( 'option', 'disabled', ! enabled );
5403 * @param {wp.media.model.Attachment} attachment
5404 * @returns {wp.media.View}
5406 createAttachmentView: function( attachment ) {
5407 var view = new this.options.AttachmentView({
5408 controller: this.controller,
5410 collection: this.collection,
5411 selection: this.options.selection
5414 return this._viewsByCid[ attachment.cid ] = view;
5417 prepare: function() {
5418 // Create all of the Attachment views, and replace
5419 // the list in a single DOM operation.
5420 if ( this.collection.length ) {
5421 this.views.set( this.collection.map( this.createAttachmentView, this ) );
5423 // If there are no elements, clear the views and load some.
5426 this.collection.more().done( this.scroll );
5431 // Trigger the scroll event to check if we're within the
5432 // threshold to query for additional attachments.
5436 scroll: function() {
5438 el = this.options.scrollElement,
5439 scrollTop = el.scrollTop,
5442 // The scroll event occurs on the document, but the element
5443 // that should be checked is the document body.
5444 if ( el == document ) {
5446 scrollTop = $(document).scrollTop();
5449 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) {
5453 toolbar = this.views.parent.toolbar;
5455 // Show the spinner only if we are close to the bottom.
5456 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) {
5457 toolbar.get('spinner').show();
5460 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) {
5461 this.collection.more().done(function() {
5463 toolbar.get('spinner').hide();
5470 * wp.media.view.Search
5473 * @augments wp.media.View
5474 * @augments wp.Backbone.View
5475 * @augments Backbone.View
5477 media.view.Search = media.View.extend({
5479 className: 'search',
5480 id: 'media-search-input',
5484 placeholder: l10n.search
5495 * @returns {wp.media.view.Search} Returns itself to allow chaining
5497 render: function() {
5498 this.el.value = this.model.escape('search');
5502 search: function( event ) {
5503 if ( event.target.value ) {
5504 this.model.set( 'search', event.target.value );
5506 this.model.unset('search');
5512 * wp.media.view.AttachmentFilters
5515 * @augments wp.media.View
5516 * @augments wp.Backbone.View
5517 * @augments Backbone.View
5519 media.view.AttachmentFilters = media.View.extend({
5521 className: 'attachment-filters',
5522 id: 'media-attachment-filters',
5530 initialize: function() {
5531 this.createFilters();
5532 _.extend( this.filters, this.options.filters );
5534 // Build `<option>` elements.
5535 this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
5537 el: $( '<option></option>' ).val( value ).html( filter.text )[0],
5538 priority: filter.priority || 50
5540 }, this ).sortBy('priority').pluck('el').value() );
5542 this.model.on( 'change', this.select, this );
5549 createFilters: function() {
5554 * When the selection changes, set the Query properties
5555 * accordingly for the selected filter.
5557 change: function() {
5558 var filter = this.filters[ this.el.value ];
5560 this.model.set( filter.props );
5564 select: function() {
5565 var model = this.model,
5567 props = model.toJSON();
5569 _.find( this.filters, function( filter, id ) {
5570 var equal = _.all( filter.props, function( prop, key ) {
5571 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
5579 this.$el.val( value );
5584 * wp.media.view.AttachmentFilters.Uploaded
5587 * @augments wp.media.view.AttachmentFilters
5588 * @augments wp.media.View
5589 * @augments wp.Backbone.View
5590 * @augments Backbone.View
5592 media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
5593 createFilters: function() {
5594 var type = this.model.get('type'),
5595 types = media.view.settings.mimeTypes,
5598 if ( types && type ) {
5599 text = types[ type ];
5604 text: text || l10n.allMediaItems,
5614 text: l10n.uploadedToThisPost,
5616 uploadedTo: media.view.settings.post.id,
5617 orderby: 'menuOrder',
5627 * wp.media.view.AttachmentFilters.All
5630 * @augments wp.media.view.AttachmentFilters
5631 * @augments wp.media.View
5632 * @augments wp.Backbone.View
5633 * @augments Backbone.View
5635 media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
5636 createFilters: function() {
5639 _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
5653 text: l10n.allMediaItems,
5664 if ( media.view.settings.post.id ) {
5665 filters.uploaded = {
5666 text: l10n.uploadedToThisPost,
5670 uploadedTo: media.view.settings.post.id,
5671 orderby: 'menuOrder',
5678 filters.unattached = {
5679 text: l10n.unattached,
5684 orderby: 'menuOrder',
5690 if ( media.view.settings.mediaTrash &&
5691 this.controller.isModeActive( 'grid' ) ) {
5706 this.filters = filters;
5711 * wp.media.view.AttachmentsBrowser
5714 * @augments wp.media.View
5715 * @augments wp.Backbone.View
5716 * @augments Backbone.View
5718 media.view.AttachmentsBrowser = media.View.extend({
5720 className: 'attachments-browser',
5722 initialize: function() {
5723 _.defaults( this.options, {
5728 AttachmentView: media.view.Attachment.Library
5731 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
5733 this.createToolbar();
5734 if ( this.options.sidebar ) {
5735 this.createSidebar();
5737 this.createUploader();
5738 this.createAttachments();
5739 this.updateContent();
5741 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) {
5742 this.$el.addClass( 'hide-sidebar' );
5744 if ( 'errors' === this.options.sidebar ) {
5745 this.$el.addClass( 'sidebar-for-errors' );
5749 this.collection.on( 'add remove reset', this.updateContent, this );
5752 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining
5754 dispose: function() {
5755 this.options.selection.off( null, null, this );
5756 media.View.prototype.dispose.apply( this, arguments );
5760 createToolbar: function() {
5761 var LibraryViewSwitcher, Filters, toolbarOptions;
5764 controller: this.controller
5767 if ( this.controller.isModeActive( 'grid' ) ) {
5768 toolbarOptions.className = 'media-toolbar wp-filter';
5772 * @member {wp.media.view.Toolbar}
5774 this.toolbar = new media.view.Toolbar( toolbarOptions );
5776 this.views.add( this.toolbar );
5778 this.toolbar.set( 'spinner', new media.view.Spinner({
5782 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
5783 // "Filters" will return a <select>, need to render
5784 // screen reader text before
5785 this.toolbar.set( 'filtersLabel', new media.view.Label({
5786 value: l10n.filterByType,
5788 'for': 'media-attachment-filters'
5793 if ( 'uploaded' === this.options.filters ) {
5794 this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
5795 controller: this.controller,
5796 model: this.collection.props,
5800 Filters = new media.view.AttachmentFilters.All({
5801 controller: this.controller,
5802 model: this.collection.props,
5806 this.toolbar.set( 'filters', Filters.render() );
5810 // Feels odd to bring the global media library switcher into the Attachment
5811 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
5812 // which the controller can tap into and add this view?
5813 if ( this.controller.isModeActive( 'grid' ) ) {
5814 LibraryViewSwitcher = media.View.extend({
5815 className: 'view-switch media-grid-view-switch',
5816 template: media.template( 'media-library-view-switcher')
5819 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({
5820 controller: this.controller,
5824 // DateFilter is a <select>, screen reader text needs to be rendered before
5825 this.toolbar.set( 'dateFilterLabel', new media.view.Label({
5826 value: l10n.filterByDate,
5828 'for': 'media-attachment-date-filters'
5832 this.toolbar.set( 'dateFilter', new media.view.DateFilter({
5833 controller: this.controller,
5834 model: this.collection.props,
5838 // BulkSelection is a <div> with subviews, including screen reader text
5839 this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
5840 text: l10n.bulkSelect,
5841 controller: this.controller,
5845 this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
5849 text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
5850 controller: this.controller,
5853 var model, changed = [], self = this,
5854 selection = this.controller.state().get( 'selection' ),
5855 library = this.controller.state().get( 'library' );
5857 if ( ! selection.length ) {
5861 if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) {
5865 if ( media.view.settings.mediaTrash &&
5866 'trash' !== selection.at( 0 ).get( 'status' ) &&
5867 ! confirm( l10n.warnBulkTrash ) ) {
5872 while ( selection.length > 0 ) {
5873 model = selection.at( 0 );
5874 if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
5875 model.set( 'status', 'inherit' );
5876 changed.push( model.save() );
5877 selection.remove( model );
5878 } else if ( media.view.settings.mediaTrash ) {
5879 model.set( 'status', 'trash' );
5880 changed.push( model.save() );
5881 selection.remove( model );
5887 if ( changed.length ) {
5888 $.when.apply( null, changed ).then( function() {
5889 library._requery( true );
5890 self.controller.trigger( 'selection:action:done' );
5893 this.controller.trigger( 'selection:action:done' );
5899 if ( this.options.search ) {
5900 // Search is an input, screen reader text needs to be rendered before
5901 this.toolbar.set( 'searchLabel', new media.view.Label({
5902 value: l10n.searchMediaLabel,
5904 'for': 'media-search-input'
5908 this.toolbar.set( 'search', new media.view.Search({
5909 controller: this.controller,
5910 model: this.collection.props,
5915 if ( this.options.dragInfo ) {
5916 this.toolbar.set( 'dragInfo', new media.View({
5917 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
5922 if ( this.options.suggestedWidth && this.options.suggestedHeight ) {
5923 this.toolbar.set( 'suggestedDimensions', new media.View({
5924 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0],
5930 updateContent: function() {
5934 if ( this.controller.isModeActive( 'grid' ) ) {
5935 noItemsView = view.attachmentsNoResults;
5937 noItemsView = view.uploader;
5940 if ( ! this.collection.length ) {
5941 this.toolbar.get( 'spinner' ).show();
5942 this.dfd = this.collection.more().done( function() {
5943 if ( ! view.collection.length ) {
5944 noItemsView.$el.removeClass( 'hidden' );
5946 noItemsView.$el.addClass( 'hidden' );
5948 view.toolbar.get( 'spinner' ).hide();
5951 noItemsView.$el.addClass( 'hidden' );
5952 view.toolbar.get( 'spinner' ).hide();
5956 createUploader: function() {
5957 this.uploader = new media.view.UploaderInline({
5958 controller: this.controller,
5960 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound,
5961 canClose: this.controller.isModeActive( 'grid' )
5964 this.uploader.hide();
5965 this.views.add( this.uploader );
5968 toggleUploader: function() {
5969 if ( this.uploader.$el.hasClass( 'hidden' ) ) {
5970 this.uploader.show();
5972 this.uploader.hide();
5976 createAttachments: function() {
5977 this.attachments = new media.view.Attachments({
5978 controller: this.controller,
5979 collection: this.collection,
5980 selection: this.options.selection,
5982 sortable: this.options.sortable,
5983 scrollElement: this.options.scrollElement,
5984 idealColumnWidth: this.options.idealColumnWidth,
5986 // The single `Attachment` view to be used in the `Attachments` view.
5987 AttachmentView: this.options.AttachmentView
5990 // Add keydown listener to the instance of the Attachments view
5991 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent );
5992 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus );
5994 this.views.add( this.attachments );
5997 if ( this.controller.isModeActive( 'grid' ) ) {
5998 this.attachmentsNoResults = new media.View({
5999 controller: this.controller,
6003 this.attachmentsNoResults.$el.addClass( 'hidden no-media' );
6004 this.attachmentsNoResults.$el.html( l10n.noMedia );
6006 this.views.add( this.attachmentsNoResults );
6010 createSidebar: function() {
6011 var options = this.options,
6012 selection = options.selection,
6013 sidebar = this.sidebar = new media.view.Sidebar({
6014 controller: this.controller
6017 this.views.add( sidebar );
6019 if ( this.controller.uploader ) {
6020 sidebar.set( 'uploads', new media.view.UploaderStatus({
6021 controller: this.controller,
6026 selection.on( 'selection:single', this.createSingle, this );
6027 selection.on( 'selection:unsingle', this.disposeSingle, this );
6029 if ( selection.single() ) {
6030 this.createSingle();
6034 createSingle: function() {
6035 var sidebar = this.sidebar,
6036 single = this.options.selection.single();
6038 sidebar.set( 'details', new media.view.Attachment.Details({
6039 controller: this.controller,
6044 sidebar.set( 'compat', new media.view.AttachmentCompat({
6045 controller: this.controller,
6050 if ( this.options.display ) {
6051 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
6052 controller: this.controller,
6053 model: this.model.display( single ),
6056 userSettings: this.model.get('displayUserSettings')
6060 // Show the sidebar on mobile
6061 if ( this.model.id === 'insert' ) {
6062 sidebar.$el.addClass( 'visible' );
6066 disposeSingle: function() {
6067 var sidebar = this.sidebar;
6068 sidebar.unset('details');
6069 sidebar.unset('compat');
6070 sidebar.unset('display');
6071 // Hide the sidebar on mobile
6072 sidebar.$el.removeClass( 'visible' );
6077 * wp.media.view.Selection
6080 * @augments wp.media.View
6081 * @augments wp.Backbone.View
6082 * @augments Backbone.View
6084 media.view.Selection = media.View.extend({
6086 className: 'media-selection',
6087 template: media.template('media-selection'),
6090 'click .edit-selection': 'edit',
6091 'click .clear-selection': 'clear'
6094 initialize: function() {
6095 _.defaults( this.options, {
6101 * @member {wp.media.view.Attachments.Selection}
6103 this.attachments = new media.view.Attachments.Selection({
6104 controller: this.controller,
6105 collection: this.collection,
6106 selection: this.collection,
6107 model: new Backbone.Model()
6110 this.views.set( '.selection-view', this.attachments );
6111 this.collection.on( 'add remove reset', this.refresh, this );
6112 this.controller.on( 'content:activate', this.refresh, this );
6119 refresh: function() {
6120 // If the selection hasn't been rendered, bail.
6121 if ( ! this.$el.children().length ) {
6125 var collection = this.collection,
6126 editing = 'edit-selection' === this.controller.content.mode();
6128 // If nothing is selected, display nothing.
6129 this.$el.toggleClass( 'empty', ! collection.length );
6130 this.$el.toggleClass( 'one', 1 === collection.length );
6131 this.$el.toggleClass( 'editing', editing );
6133 this.$('.count').text( l10n.selected.replace('%d', collection.length) );
6136 edit: function( event ) {
6137 event.preventDefault();
6138 if ( this.options.editable ) {
6139 this.options.editable.call( this, this.collection );
6143 clear: function( event ) {
6144 event.preventDefault();
6145 this.collection.reset();
6147 // Keep focus inside media modal
6148 // after clear link is selected
6149 this.controller.modal.focusManager.focus();
6155 * wp.media.view.Attachment.Selection
6158 * @augments wp.media.view.Attachment
6159 * @augments wp.media.View
6160 * @augments wp.Backbone.View
6161 * @augments Backbone.View
6163 media.view.Attachment.Selection = media.view.Attachment.extend({
6164 className: 'attachment selection',
6166 // On click, just select the model, instead of removing the model from
6168 toggleSelection: function() {
6169 this.options.selection.single( this.model );
6174 * wp.media.view.Attachments.Selection
6177 * @augments wp.media.view.Attachments
6178 * @augments wp.media.View
6179 * @augments wp.Backbone.View
6180 * @augments Backbone.View
6182 media.view.Attachments.Selection = media.view.Attachments.extend({
6184 initialize: function() {
6185 _.defaults( this.options, {
6189 // The single `Attachment` view to be used in the `Attachments` view.
6190 AttachmentView: media.view.Attachment.Selection
6193 * call 'initialize' directly on the parent class
6195 return media.view.Attachments.prototype.initialize.apply( this, arguments );
6200 * wp.media.view.Attachments.EditSelection
6203 * @augments wp.media.view.Attachment.Selection
6204 * @augments wp.media.view.Attachment
6205 * @augments wp.media.View
6206 * @augments wp.Backbone.View
6207 * @augments Backbone.View
6209 media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
6217 * wp.media.view.Settings
6220 * @augments wp.media.View
6221 * @augments wp.Backbone.View
6222 * @augments Backbone.View
6224 media.view.Settings = media.View.extend({
6226 'click button': 'updateHandler',
6227 'change input': 'updateHandler',
6228 'change select': 'updateHandler',
6229 'change textarea': 'updateHandler'
6232 initialize: function() {
6233 this.model = this.model || new Backbone.Model();
6234 this.model.on( 'change', this.updateChanges, this );
6237 prepare: function() {
6239 model: this.model.toJSON()
6243 * @returns {wp.media.view.Settings} Returns itself to allow chaining
6245 render: function() {
6246 media.View.prototype.render.apply( this, arguments );
6247 // Select the correct values.
6248 _( this.model.attributes ).chain().keys().each( this.update, this );
6252 * @param {string} key
6254 update: function( key ) {
6255 var value = this.model.get( key ),
6256 $setting = this.$('[data-setting="' + key + '"]'),
6259 // Bail if we didn't find a matching setting.
6260 if ( ! $setting.length ) {
6264 // Attempt to determine how the setting is rendered and update
6265 // the selected value.
6267 // Handle dropdowns.
6268 if ( $setting.is('select') ) {
6269 $value = $setting.find('[value="' + value + '"]');
6271 if ( $value.length ) {
6272 $setting.find('option').prop( 'selected', false );
6273 $value.prop( 'selected', true );
6275 // If we can't find the desired value, record what *is* selected.
6276 this.model.set( key, $setting.find(':selected').val() );
6279 // Handle button groups.
6280 } else if ( $setting.hasClass('button-group') ) {
6281 $buttons = $setting.find('button').removeClass('active');
6282 $buttons.filter( '[value="' + value + '"]' ).addClass('active');
6284 // Handle text inputs and textareas.
6285 } else if ( $setting.is('input[type="text"], textarea') ) {
6286 if ( ! $setting.is(':focus') ) {
6287 $setting.val( value );
6289 // Handle checkboxes.
6290 } else if ( $setting.is('input[type="checkbox"]') ) {
6291 $setting.prop( 'checked', !! value && 'false' !== value );
6295 * @param {Object} event
6297 updateHandler: function( event ) {
6298 var $setting = $( event.target ).closest('[data-setting]'),
6299 value = event.target.value,
6302 event.preventDefault();
6304 if ( ! $setting.length ) {
6308 // Use the correct value for checkboxes.
6309 if ( $setting.is('input[type="checkbox"]') ) {
6310 value = $setting[0].checked;
6313 // Update the corresponding setting.
6314 this.model.set( $setting.data('setting'), value );
6316 // If the setting has a corresponding user setting,
6317 // update that as well.
6318 if ( userSetting = $setting.data('userSetting') ) {
6319 setUserSetting( userSetting, value );
6323 updateChanges: function( model ) {
6324 if ( model.hasChanged() ) {
6325 _( model.changed ).chain().keys().each( this.update, this );
6331 * wp.media.view.Settings.AttachmentDisplay
6334 * @augments wp.media.view.Settings
6335 * @augments wp.media.View
6336 * @augments wp.Backbone.View
6337 * @augments Backbone.View
6339 media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
6340 className: 'attachment-display-settings',
6341 template: media.template('attachment-display-settings'),
6343 initialize: function() {
6344 var attachment = this.options.attachment;
6346 _.defaults( this.options, {
6350 * call 'initialize' directly on the parent class
6352 media.view.Settings.prototype.initialize.apply( this, arguments );
6353 this.model.on( 'change:link', this.updateLinkTo, this );
6356 attachment.on( 'change:uploading', this.render, this );
6360 dispose: function() {
6361 var attachment = this.options.attachment;
6363 attachment.off( null, null, this );
6366 * call 'dispose' directly on the parent class
6368 media.view.Settings.prototype.dispose.apply( this, arguments );
6371 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining
6373 render: function() {
6374 var attachment = this.options.attachment;
6376 _.extend( this.options, {
6377 sizes: attachment.get('sizes'),
6378 type: attachment.get('type')
6382 * call 'render' directly on the parent class
6384 media.view.Settings.prototype.render.call( this );
6385 this.updateLinkTo();
6389 updateLinkTo: function() {
6390 var linkTo = this.model.get('link'),
6391 $input = this.$('.link-to-custom'),
6392 attachment = this.options.attachment;
6394 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
6395 $input.addClass( 'hidden' );
6400 if ( 'post' === linkTo ) {
6401 $input.val( attachment.get('link') );
6402 } else if ( 'file' === linkTo ) {
6403 $input.val( attachment.get('url') );
6404 } else if ( ! this.model.get('linkUrl') ) {
6405 $input.val('http://');
6408 $input.prop( 'readonly', 'custom' !== linkTo );
6411 $input.removeClass( 'hidden' );
6413 // If the input is visible, focus and select its contents.
6414 if ( ! isTouchDevice && $input.is(':visible') ) {
6415 $input.focus()[0].select();
6421 * wp.media.view.Settings.Gallery
6424 * @augments wp.media.view.Settings
6425 * @augments wp.media.View
6426 * @augments wp.Backbone.View
6427 * @augments Backbone.View
6429 media.view.Settings.Gallery = media.view.Settings.extend({
6430 className: 'collection-settings gallery-settings',
6431 template: media.template('gallery-settings')
6435 * wp.media.view.Settings.Playlist
6438 * @augments wp.media.view.Settings
6439 * @augments wp.media.View
6440 * @augments wp.Backbone.View
6441 * @augments Backbone.View
6443 media.view.Settings.Playlist = media.view.Settings.extend({
6444 className: 'collection-settings playlist-settings',
6445 template: media.template('playlist-settings')
6449 * wp.media.view.Attachment.Details
6452 * @augments wp.media.view.Attachment
6453 * @augments wp.media.View
6454 * @augments wp.Backbone.View
6455 * @augments Backbone.View
6457 media.view.Attachment.Details = media.view.Attachment.extend({
6459 className: 'attachment-details',
6460 template: media.template('attachment-details'),
6463 'change [data-setting]': 'updateSetting',
6464 'change [data-setting] input': 'updateSetting',
6465 'change [data-setting] select': 'updateSetting',
6466 'change [data-setting] textarea': 'updateSetting',
6467 'click .delete-attachment': 'deleteAttachment',
6468 'click .trash-attachment': 'trashAttachment',
6469 'click .untrash-attachment': 'untrashAttachment',
6470 'click .edit-attachment': 'editAttachment',
6471 'click .refresh-attachment': 'refreshAttachment',
6472 'keydown': 'toggleSelectionHandler'
6475 initialize: function() {
6476 this.options = _.defaults( this.options, {
6477 rerenderOnModelChange: false
6480 this.on( 'ready', this.initialFocus );
6482 * call 'initialize' directly on the parent class
6484 media.view.Attachment.prototype.initialize.apply( this, arguments );
6487 initialFocus: function() {
6488 if ( ! isTouchDevice ) {
6489 this.$( ':input' ).eq( 0 ).focus();
6493 * @param {Object} event
6495 deleteAttachment: function( event ) {
6496 event.preventDefault();
6498 if ( confirm( l10n.warnDelete ) ) {
6499 this.model.destroy();
6500 // Keep focus inside media modal
6501 // after image is deleted
6502 this.controller.modal.focusManager.focus();
6506 * @param {Object} event
6508 trashAttachment: function( event ) {
6509 var library = this.controller.library;
6510 event.preventDefault();
6512 if ( media.view.settings.mediaTrash &&
6513 'edit-metadata' === this.controller.content.mode() ) {
6515 this.model.set( 'status', 'trash' );
6516 this.model.save().done( function() {
6517 library._requery( true );
6520 this.model.destroy();
6524 * @param {Object} event
6526 untrashAttachment: function( event ) {
6527 var library = this.controller.library;
6528 event.preventDefault();
6530 this.model.set( 'status', 'inherit' );
6531 this.model.save().done( function() {
6532 library._requery( true );
6536 * @param {Object} event
6538 editAttachment: function( event ) {
6539 var editState = this.controller.states.get( 'edit-image' );
6540 if ( window.imageEdit && editState ) {
6541 event.preventDefault();
6543 editState.set( 'image', this.model );
6544 this.controller.setState( 'edit-image' );
6546 this.$el.addClass('needs-refresh');
6550 * @param {Object} event
6552 refreshAttachment: function( event ) {
6553 this.$el.removeClass('needs-refresh');
6554 event.preventDefault();
6558 * When reverse tabbing(shift+tab) out of the right details panel, deliver
6559 * the focus to the item in the list that was being edited.
6561 * @param {Object} event
6563 toggleSelectionHandler: function( event ) {
6564 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) {
6565 this.controller.trigger( 'attachment:details:shift-tab', event );
6569 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
6570 this.controller.trigger( 'attachment:keydown:arrow', event );
6577 * wp.media.view.AttachmentCompat
6579 * A view to display fields added via the `attachment_fields_to_edit` filter.
6582 * @augments wp.media.View
6583 * @augments wp.Backbone.View
6584 * @augments Backbone.View
6586 media.view.AttachmentCompat = media.View.extend({
6588 className: 'compat-item',
6591 'submit': 'preventDefault',
6592 'change input': 'save',
6593 'change select': 'save',
6594 'change textarea': 'save'
6597 initialize: function() {
6598 this.model.on( 'change:compat', this.render, this );
6601 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
6603 dispose: function() {
6604 if ( this.$(':focus').length ) {
6608 * call 'dispose' directly on the parent class
6610 return media.View.prototype.dispose.apply( this, arguments );
6613 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
6615 render: function() {
6616 var compat = this.model.get('compat');
6617 if ( ! compat || ! compat.item ) {
6621 this.views.detach();
6622 this.$el.html( compat.item );
6623 this.views.render();
6627 * @param {Object} event
6629 preventDefault: function( event ) {
6630 event.preventDefault();
6633 * @param {Object} event
6635 save: function( event ) {
6639 event.preventDefault();
6642 _.each( this.$el.serializeArray(), function( pair ) {
6643 data[ pair.name ] = pair.value;
6646 this.model.saveCompat( data );
6651 * wp.media.view.Iframe
6654 * @augments wp.media.View
6655 * @augments wp.Backbone.View
6656 * @augments Backbone.View
6658 media.view.Iframe = media.View.extend({
6659 className: 'media-iframe',
6661 * @returns {wp.media.view.Iframe} Returns itself to allow chaining
6663 render: function() {
6664 this.views.detach();
6665 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
6666 this.views.render();
6672 * wp.media.view.Embed
6675 * @augments wp.media.View
6676 * @augments wp.Backbone.View
6677 * @augments Backbone.View
6679 media.view.Embed = media.View.extend({
6680 className: 'media-embed',
6682 initialize: function() {
6684 * @member {wp.media.view.EmbedUrl}
6686 this.url = new media.view.EmbedUrl({
6687 controller: this.controller,
6688 model: this.model.props
6691 this.views.set([ this.url ]);
6693 this.model.on( 'change:type', this.refresh, this );
6694 this.model.on( 'change:loading', this.loading, this );
6698 * @param {Object} view
6700 settings: function( view ) {
6701 if ( this._settings ) {
6702 this._settings.remove();
6704 this._settings = view;
6705 this.views.add( view );
6708 refresh: function() {
6709 var type = this.model.get('type'),
6712 if ( 'image' === type ) {
6713 constructor = media.view.EmbedImage;
6714 } else if ( 'link' === type ) {
6715 constructor = media.view.EmbedLink;
6720 this.settings( new constructor({
6721 controller: this.controller,
6722 model: this.model.props,
6727 loading: function() {
6728 this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
6734 * @augments wp.media.View
6735 * @augments wp.Backbone.View
6736 * @augments Backbone.View
6738 media.view.Label = media.View.extend({
6740 className: 'screen-reader-text',
6742 initialize: function() {
6743 this.value = this.options.value;
6746 render: function() {
6747 this.$el.html( this.value );
6754 * wp.media.view.EmbedUrl
6757 * @augments wp.media.View
6758 * @augments wp.Backbone.View
6759 * @augments Backbone.View
6761 media.view.EmbedUrl = media.View.extend({
6763 className: 'embed-url',
6771 initialize: function() {
6774 this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') );
6775 this.input = this.$input[0];
6777 this.spinner = $('<span class="spinner" />')[0];
6778 this.$el.append([ this.input, this.spinner ]);
6780 this.model.on( 'change:url', this.render, this );
6782 if ( this.model.get( 'url' ) ) {
6783 _.delay( function () {
6784 self.model.trigger( 'change:url' );
6789 * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining
6791 render: function() {
6792 var $input = this.$input;
6794 if ( $input.is(':focus') ) {
6798 this.input.value = this.model.get('url') || 'http://';
6800 * Call `render` directly on parent class with passed arguments
6802 media.View.prototype.render.apply( this, arguments );
6807 if ( ! isTouchDevice ) {
6812 url: function( event ) {
6813 this.model.set( 'url', event.target.value );
6817 * If the input is visible, focus and select its contents.
6820 var $input = this.$input;
6821 if ( $input.is(':visible') ) {
6822 $input.focus()[0].select();
6828 * wp.media.view.EmbedLink
6831 * @augments wp.media.view.Settings
6832 * @augments wp.media.View
6833 * @augments wp.Backbone.View
6834 * @augments Backbone.View
6836 media.view.EmbedLink = media.view.Settings.extend({
6837 className: 'embed-link-settings',
6838 template: media.template('embed-link-settings'),
6840 initialize: function() {
6841 this.spinner = $('<span class="spinner" />');
6842 this.$el.append( this.spinner[0] );
6843 this.listenTo( this.model, 'change:url', this.updateoEmbed );
6846 updateoEmbed: function() {
6847 var url = this.model.get( 'url' );
6849 this.$('.setting.title').show();
6850 // clear out previous results
6851 this.$('.embed-container').hide().find('.embed-preview').html('');
6853 // only proceed with embed if the field contains more than 6 characters
6854 if ( url && url.length < 6 ) {
6858 this.spinner.show();
6860 setTimeout( _.bind( this.fetch, this ), 500 );
6864 // check if they haven't typed in 500 ms
6865 if ( $('#embed-url-field').val() !== this.model.get('url') ) {
6869 wp.ajax.send( 'parse-embed', {
6871 post_ID: media.view.settings.post.id,
6872 shortcode: '[embed]' + this.model.get('url') + '[/embed]'
6874 } ).done( _.bind( this.renderoEmbed, this ) );
6877 renderoEmbed: function( response ) {
6878 var html = ( response && response.body ) || '';
6880 this.spinner.hide();
6882 this.$('.setting.title').hide();
6883 this.$('.embed-container').show().find('.embed-preview').html( html );
6888 * wp.media.view.EmbedImage
6891 * @augments wp.media.view.Settings.AttachmentDisplay
6892 * @augments wp.media.view.Settings
6893 * @augments wp.media.View
6894 * @augments wp.Backbone.View
6895 * @augments Backbone.View
6897 media.view.EmbedImage = media.view.Settings.AttachmentDisplay.extend({
6898 className: 'embed-media-settings',
6899 template: media.template('embed-image-settings'),
6901 initialize: function() {
6903 * Call `initialize` directly on parent class with passed arguments
6905 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
6906 this.model.on( 'change:url', this.updateImage, this );
6909 updateImage: function() {
6910 this.$('img').attr( 'src', this.model.get('url') );
6915 * wp.media.view.ImageDetails
6918 * @augments wp.media.view.Settings.AttachmentDisplay
6919 * @augments wp.media.view.Settings
6920 * @augments wp.media.View
6921 * @augments wp.Backbone.View
6922 * @augments Backbone.View
6924 media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({
6925 className: 'image-details',
6926 template: media.template('image-details'),
6927 events: _.defaults( media.view.Settings.AttachmentDisplay.prototype.events, {
6928 'click .edit-attachment': 'editAttachment',
6929 'click .replace-attachment': 'replaceAttachment',
6930 'click .advanced-toggle': 'onToggleAdvanced',
6931 'change [data-setting="customWidth"]': 'onCustomSize',
6932 'change [data-setting="customHeight"]': 'onCustomSize',
6933 'keyup [data-setting="customWidth"]': 'onCustomSize',
6934 'keyup [data-setting="customHeight"]': 'onCustomSize'
6936 initialize: function() {
6937 // used in AttachmentDisplay.prototype.updateLinkTo
6938 this.options.attachment = this.model.attachment;
6939 this.listenTo( this.model, 'change:url', this.updateUrl );
6940 this.listenTo( this.model, 'change:link', this.toggleLinkSettings );
6941 this.listenTo( this.model, 'change:size', this.toggleCustomSize );
6943 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
6946 prepare: function() {
6947 var attachment = false;
6949 if ( this.model.attachment ) {
6950 attachment = this.model.attachment.toJSON();
6953 model: this.model.toJSON(),
6954 attachment: attachment
6958 render: function() {
6962 if ( this.model.attachment && 'pending' === this.model.dfd.state() ) {
6963 this.model.dfd.done( function() {
6964 media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
6966 } ).fail( function() {
6967 self.model.attachment = false;
6968 media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
6972 media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
6979 postRender: function() {
6980 setTimeout( _.bind( this.resetFocus, this ), 10 );
6981 this.toggleLinkSettings();
6982 if ( getUserSetting( 'advImgDetails' ) === 'show' ) {
6983 this.toggleAdvanced( true );
6985 this.trigger( 'post-render' );
6988 resetFocus: function() {
6989 this.$( '.link-to-custom' ).blur();
6990 this.$( '.embed-media-settings' ).scrollTop( 0 );
6993 updateUrl: function() {
6994 this.$( '.image img' ).attr( 'src', this.model.get( 'url' ) );
6995 this.$( '.url' ).val( this.model.get( 'url' ) );
6998 toggleLinkSettings: function() {
6999 if ( this.model.get( 'link' ) === 'none' ) {
7000 this.$( '.link-settings' ).addClass('hidden');
7002 this.$( '.link-settings' ).removeClass('hidden');
7006 toggleCustomSize: function() {
7007 if ( this.model.get( 'size' ) !== 'custom' ) {
7008 this.$( '.custom-size' ).addClass('hidden');
7010 this.$( '.custom-size' ).removeClass('hidden');
7014 onCustomSize: function( event ) {
7015 var dimension = $( event.target ).data('setting'),
7016 num = $( event.target ).val(),
7019 // Ignore bogus input
7020 if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) {
7021 event.preventDefault();
7025 if ( dimension === 'customWidth' ) {
7026 value = Math.round( 1 / this.model.get( 'aspectRatio' ) * num );
7027 this.model.set( 'customHeight', value, { silent: true } );
7028 this.$( '[data-setting="customHeight"]' ).val( value );
7030 value = Math.round( this.model.get( 'aspectRatio' ) * num );
7031 this.model.set( 'customWidth', value, { silent: true } );
7032 this.$( '[data-setting="customWidth"]' ).val( value );
7036 onToggleAdvanced: function( event ) {
7037 event.preventDefault();
7038 this.toggleAdvanced();
7041 toggleAdvanced: function( show ) {
7042 var $advanced = this.$el.find( '.advanced-section' ),
7045 if ( $advanced.hasClass('advanced-visible') || show === false ) {
7046 $advanced.removeClass('advanced-visible');
7047 $advanced.find('.advanced-settings').addClass('hidden');
7050 $advanced.addClass('advanced-visible');
7051 $advanced.find('.advanced-settings').removeClass('hidden');
7055 setUserSetting( 'advImgDetails', mode );
7058 editAttachment: function( event ) {
7059 var editState = this.controller.states.get( 'edit-image' );
7061 if ( window.imageEdit && editState ) {
7062 event.preventDefault();
7063 editState.set( 'image', this.model.attachment );
7064 this.controller.setState( 'edit-image' );
7068 replaceAttachment: function( event ) {
7069 event.preventDefault();
7070 this.controller.setState( 'replace-image' );
7075 * wp.media.view.Cropper
7077 * Uses the imgAreaSelect plugin to allow a user to crop an image.
7079 * Takes imgAreaSelect options from
7080 * wp.customize.HeaderControl.calculateImageSelectOptions via
7081 * wp.customize.HeaderControl.openMM.
7084 * @augments wp.media.View
7085 * @augments wp.Backbone.View
7086 * @augments Backbone.View
7088 media.view.Cropper = media.View.extend({
7089 className: 'crop-content',
7090 template: media.template('crop-content'),
7091 initialize: function() {
7092 _.bindAll(this, 'onImageLoad');
7095 this.controller.frame.on('content:error:crop', this.onError, this);
7096 this.$image = this.$el.find('.crop-image');
7097 this.$image.on('load', this.onImageLoad);
7098 $(window).on('resize.cropper', _.debounce(this.onImageLoad, 250));
7100 remove: function() {
7101 $(window).off('resize.cropper');
7104 wp.media.View.prototype.remove.apply(this, arguments);
7106 prepare: function() {
7108 title: l10n.cropYourImage,
7109 url: this.options.attachment.get('url')
7112 onImageLoad: function() {
7113 var imgOptions = this.controller.get('imgSelectOptions');
7114 if (typeof imgOptions === 'function') {
7115 imgOptions = imgOptions(this.options.attachment, this.controller);
7118 imgOptions = _.extend(imgOptions, {parent: this.$el});
7119 this.trigger('image-loaded');
7120 this.controller.imgSelect = this.$image.imgAreaSelect(imgOptions);
7122 onError: function() {
7123 var filename = this.options.attachment.get('filename');
7125 this.views.add( '.upload-errors', new media.view.UploaderStatusError({
7126 filename: media.view.UploaderStatus.prototype.filename(filename),
7127 message: _wpMediaViewsL10n.cropError
7132 media.view.EditImage = media.View.extend({
7134 className: 'image-editor',
7135 template: media.template('image-editor'),
7137 initialize: function( options ) {
7138 this.editor = window.imageEdit;
7139 this.controller = options.controller;
7140 media.View.prototype.initialize.apply( this, arguments );
7143 prepare: function() {
7144 return this.model.toJSON();
7147 render: function() {
7148 media.View.prototype.render.apply( this, arguments );
7152 loadEditor: function() {
7153 var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
7154 dfd.done( _.bind( this.focus, this ) );
7158 this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
7162 var lastState = this.controller.lastState();
7163 this.controller.setState( lastState );
7166 refresh: function() {
7172 lastState = this.controller.lastState();
7174 this.model.fetch().done( function() {
7175 self.controller.setState( lastState );
7182 * wp.media.view.Spinner
7185 * @augments wp.media.View
7186 * @augments wp.Backbone.View
7187 * @augments Backbone.View
7189 media.view.Spinner = media.View.extend({
7191 className: 'spinner',
7192 spinnerTimeout: false,
7196 if ( ! this.spinnerTimeout ) {
7197 this.spinnerTimeout = _.delay(function( $el ) {
7199 }, this.delay, this.$el );
7207 this.spinnerTimeout = clearTimeout( this.spinnerTimeout );