3 Attachment = media.model.Attachment,
4 Attachments = media.model.Attachments,
5 Query = media.model.Query,
8 // Link any localized strings.
9 l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
12 media.view.settings = l10n.settings || {};
15 // Copy the `post` setting over to the model settings.
16 media.model.settings.post = media.view.settings.post;
18 // Check if the browser supports CSS 3.0 transitions
19 $.support.transition = (function(){
20 var style = document.documentElement.style,
22 WebkitTransition: 'webkitTransitionEnd',
23 MozTransition: 'transitionend',
24 OTransition: 'oTransitionEnd otransitionend',
25 transition: 'transitionend'
28 transition = _.find( _.keys( transitions ), function( transition ) {
29 return ! _.isUndefined( style[ transition ] );
32 return transition && {
33 end: transitions[ transition ]
37 // Makes it easier to bind events using transitions.
38 media.transition = function( selector, sensitivity ) {
39 var deferred = $.Deferred();
41 sensitivity = sensitivity || 2000;
43 if ( $.support.transition ) {
44 if ( ! (selector instanceof $) )
45 selector = $( selector );
47 // Resolve the deferred when the first element finishes animating.
48 selector.first().one( $.support.transition.end, deferred.resolve );
50 // Just in case the event doesn't trigger, fire a callback.
51 _.delay( deferred.resolve, sensitivity );
53 // Otherwise, execute on the spot.
58 return deferred.promise();
62 * ========================================================================
64 * ========================================================================
68 * wp.media.controller.Region
70 media.controller.Region = function( options ) {
71 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
74 // Use Backbone's self-propagating `extend` inheritance method.
75 media.controller.Region.extend = Backbone.Model.extend;
77 _.extend( media.controller.Region.prototype, {
78 mode: function( mode ) {
82 // Bail if we're trying to change to the current mode.
83 if ( mode === this._mode )
86 this.trigger('deactivate');
89 this.trigger('activate');
93 render: function( mode ) {
94 // If no mode is provided, just re-render the current mode.
95 // If the provided mode isn't active, perform a full switch.
96 if ( mode && mode !== this._mode )
97 return this.mode( mode );
99 var set = { view: null },
102 this.trigger( 'create', set );
104 this.trigger( 'render', view );
111 return this.view.views.first( this.selector );
114 set: function( views, options ) {
117 return this.view.views.set( this.selector, views, options );
120 trigger: function( event ) {
125 var args = _.toArray( arguments );
126 base = this.id + ':' + event;
128 // Trigger `region:action:mode` event.
129 args[0] = base + ':' + this._mode;
130 this.view.trigger.apply( this.view, args );
132 // Trigger `region:action` event.
134 this.view.trigger.apply( this.view, args );
140 * wp.media.controller.StateMachine
142 media.controller.StateMachine = function( states ) {
143 this.states = new Backbone.Collection( states );
146 // Use Backbone's self-propagating `extend` inheritance method.
147 media.controller.StateMachine.extend = Backbone.Model.extend;
149 // Add events to the `StateMachine`.
150 _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
154 // If no `id` is provided, returns the active state.
156 // Implicitly creates states.
157 state: function( id ) {
158 // Ensure that the `states` collection exists so the `StateMachine`
159 // can be used as a mixin.
160 this.states = this.states || new Backbone.Collection();
162 // Default to the active state.
163 id = id || this._state;
165 if ( id && ! this.states.get( id ) )
166 this.states.add({ id: id });
167 return this.states.get( id );
170 // Sets the active state.
171 setState: function( id ) {
172 var previous = this.state();
174 // Bail if we're trying to select the current state, if we haven't
175 // created the `states` collection, or are trying to select a state
176 // that does not exist.
177 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) )
181 previous.trigger('deactivate');
182 this._lastState = previous.id;
186 this.state().trigger('activate');
191 // Returns the previous active state.
193 // Call the `state()` method with no parameters to retrieve the current
195 lastState: function() {
196 if ( this._lastState )
197 return this.state( this._lastState );
201 // Map methods from the `states` collection to the `StateMachine` itself.
202 _.each([ 'on', 'off', 'trigger' ], function( method ) {
203 media.controller.StateMachine.prototype[ method ] = function() {
204 // Ensure that the `states` collection exists so the `StateMachine`
205 // can be used as a mixin.
206 this.states = this.states || new Backbone.Collection();
207 // Forward the method to the `states` collection.
208 this.states[ method ].apply( this.states, arguments );
214 // wp.media.controller.State
215 // ---------------------------
216 media.controller.State = Backbone.Model.extend({
217 constructor: function() {
218 this.on( 'activate', this._preActivate, this );
219 this.on( 'activate', this.activate, this );
220 this.on( 'activate', this._postActivate, this );
221 this.on( 'deactivate', this._deactivate, this );
222 this.on( 'deactivate', this.deactivate, this );
223 this.on( 'reset', this.reset, this );
224 this.on( 'ready', this._ready, this );
225 this.on( 'ready', this.ready, this );
227 this.on( 'change:menu', this._updateMenu, this );
229 Backbone.Model.apply( this, arguments );
232 ready: function() {},
233 activate: function() {},
234 deactivate: function() {},
235 reset: function() {},
241 _preActivate: function() {
245 _postActivate: function() {
246 this.on( 'change:menu', this._menu, this );
247 this.on( 'change:titleMode', this._title, this );
248 this.on( 'change:content', this._content, this );
249 this.on( 'change:toolbar', this._toolbar, this );
251 this.frame.on( 'title:render:default', this._renderTitle, this );
261 _deactivate: function() {
264 this.frame.off( 'title:render:default', this._renderTitle, this );
266 this.off( 'change:menu', this._menu, this );
267 this.off( 'change:titleMode', this._title, this );
268 this.off( 'change:content', this._content, this );
269 this.off( 'change:toolbar', this._toolbar, this );
273 this.frame.title.render( this.get('titleMode') || 'default' );
276 _renderTitle: function( view ) {
277 view.$el.text( this.get('title') || '' );
280 _router: function() {
281 var router = this.frame.router,
282 mode = this.get('router'),
285 this.frame.$el.toggleClass( 'hide-router', ! mode );
289 this.frame.router.render( mode );
292 if ( view && view.select )
293 view.select( this.frame.content.mode() );
297 var menu = this.frame.menu,
298 mode = this.get('menu'),
307 if ( view && view.select )
308 view.select( this.id );
311 _updateMenu: function() {
312 var previous = this.previous('menu'),
313 menu = this.get('menu');
316 this.frame.off( 'menu:render:' + previous, this._renderMenu, this );
319 this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
322 _renderMenu: function( view ) {
323 var menuItem = this.get('menuItem'),
324 title = this.get('title'),
325 priority = this.get('priority');
327 if ( ! menuItem && title ) {
328 menuItem = { text: title };
331 menuItem.priority = priority;
337 view.set( this.id, menuItem );
341 _.each(['toolbar','content'], function( region ) {
342 media.controller.State.prototype[ '_' + region ] = function() {
343 var mode = this.get( region );
345 this.frame[ region ].render( mode );
349 // wp.media.controller.Library
350 // ---------------------------
351 media.controller.Library = media.controller.State.extend({
354 multiple: false, // false, 'add', 'reset'
364 title: l10n.mediaLibraryTitle,
366 // Uses a user setting to override the content mode.
367 contentUserSetting: true,
369 // Sync the selection from the last state when 'multiple' matches.
373 initialize: function() {
374 var selection = this.get('selection'),
377 // If a library isn't provided, query all media items.
378 if ( ! this.get('library') )
379 this.set( 'library', media.query() );
381 // If a selection instance isn't provided, create one.
382 if ( ! (selection instanceof media.model.Selection) ) {
386 props = this.get('library').props.toJSON();
387 props = _.omit( props, 'orderby', 'query' );
390 // If the `selection` attribute is set to an object,
391 // it will use those values as the selection instance's
392 // `props` model. Otherwise, it will copy the library's
394 this.set( 'selection', new media.model.Selection( null, {
395 multiple: this.get('multiple'),
400 if ( ! this.get('edge') )
401 this.set( 'edge', 120 );
403 if ( ! this.get('gutter') )
404 this.set( 'gutter', 8 );
406 this.resetDisplays();
409 activate: function() {
410 this.syncSelection();
412 wp.Uploader.queue.on( 'add', this.uploading, this );
414 this.get('selection').on( 'add remove reset', this.refreshContent, this );
416 this.on( 'insert', this._insertDisplaySettings, this );
418 if ( this.get('contentUserSetting') ) {
419 this.frame.on( 'content:activate', this.saveContentMode, this );
420 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
424 deactivate: function() {
425 this.recordSelection();
427 this.frame.off( 'content:activate', this.saveContentMode, this );
429 // Unbind all event handlers that use this state as the context
430 // from the selection.
431 this.get('selection').off( null, null, this );
433 wp.Uploader.queue.off( null, null, this );
437 this.get('selection').reset();
438 this.resetDisplays();
439 this.refreshContent();
442 resetDisplays: function() {
444 this._defaultDisplaySettings = {
445 align: getUserSetting( 'align', 'none' ),
446 size: getUserSetting( 'imgsize', 'medium' ),
447 link: getUserSetting( 'urlbutton', 'post' )
451 display: function( attachment ) {
452 var displays = this._displays;
454 if ( ! displays[ attachment.cid ] )
455 displays[ attachment.cid ] = new Backbone.Model( this._defaultDisplaySettings );
457 return displays[ attachment.cid ];
460 _insertDisplaySettings: function() {
461 var selection = this.get('selection'),
464 // If inserting one image, set those display properties as the
465 // default user setting.
466 if ( selection.length !== 1 )
469 display = this.display( selection.first() ).toJSON();
471 setUserSetting( 'align', display.align );
472 setUserSetting( 'imgsize', display.size );
473 setUserSetting( 'urlbutton', display.link );
476 syncSelection: function() {
477 var selection = this.get('selection'),
478 manager = this.frame._selection;
480 if ( ! this.get('syncSelection') || ! manager || ! selection )
483 // If the selection supports multiple items, validate the stored
484 // attachments based on the new selection's conditions. Record
485 // the attachments that are not included; we'll maintain a
486 // reference to those. Other attachments are considered in flux.
487 if ( selection.multiple ) {
488 selection.reset( [], { silent: true });
489 selection.validateAll( manager.attachments );
490 manager.difference = _.difference( manager.attachments.models, selection.models );
493 // Sync the selection's single item with the master.
494 selection.single( manager.single );
497 recordSelection: function() {
498 var selection = this.get('selection'),
499 manager = this.frame._selection,
502 if ( ! this.get('syncSelection') || ! manager || ! selection )
505 // Record the currently active attachments, which is a combination
506 // of the selection's attachments and the set of selected
507 // attachments that this specific selection considered invalid.
508 // Reset the difference and record the single attachment.
509 if ( selection.multiple ) {
510 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
511 manager.difference = [];
513 manager.attachments.add( selection.toArray() );
516 manager.single = selection._single;
519 refreshContent: function() {
520 var selection = this.get('selection'),
522 router = frame.router.get(),
523 mode = frame.content.mode();
525 if ( this.active && ! selection.length && ! router.get( mode ) )
526 this.frame.content.render( this.get('content') );
529 uploading: function( attachment ) {
530 var content = this.frame.content;
532 // If the uploader was selected, navigate to the browser.
533 if ( 'upload' === content.mode() )
534 this.frame.content.mode('browse');
536 // If we're in a workflow that supports multiple attachments,
537 // automatically select any uploading attachments.
538 if ( this.get('multiple') )
539 this.get('selection').add( attachment );
542 saveContentMode: function() {
543 // Only track the browse router on library states.
544 if ( 'browse' !== this.get('router') )
547 var mode = this.frame.content.mode(),
548 view = this.frame.router.get();
550 if ( view && view.get( mode ) )
551 setUserSetting( 'libraryContent', mode );
555 // wp.media.controller.GalleryEdit
556 // -------------------------------
557 media.controller.GalleryEdit = media.controller.Library.extend({
566 toolbar: 'gallery-edit',
568 title: l10n.editGalleryTitle,
572 // Don't sync the selection, as the Edit Gallery library
573 // *is* the selection.
577 initialize: function() {
578 // If we haven't been provided a `library`, create a `Selection`.
579 if ( ! this.get('library') )
580 this.set( 'library', new media.model.Selection() );
582 // The single `Attachment` view to be used in the `Attachments` view.
583 if ( ! this.get('AttachmentView') )
584 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
585 media.controller.Library.prototype.initialize.apply( this, arguments );
588 activate: function() {
589 var library = this.get('library');
591 // Limit the library to images only.
592 library.props.set( 'type', 'image' );
594 // Watch for uploaded attachments.
595 this.get('library').observe( wp.Uploader.queue );
597 this.frame.on( 'content:render:browse', this.gallerySettings, this );
599 media.controller.Library.prototype.activate.apply( this, arguments );
602 deactivate: function() {
603 // Stop watching for uploaded attachments.
604 this.get('library').unobserve( wp.Uploader.queue );
606 this.frame.off( 'content:render:browse', this.gallerySettings, this );
608 media.controller.Library.prototype.deactivate.apply( this, arguments );
611 gallerySettings: function( browser ) {
612 var library = this.get('library');
614 if ( ! library || ! browser )
617 library.gallery = library.gallery || new Backbone.Model();
619 browser.sidebar.set({
620 gallery: new media.view.Settings.Gallery({
622 model: library.gallery,
627 browser.toolbar.set( 'reverse', {
628 text: l10n.reverseOrder,
632 library.reset( library.toArray().reverse() );
638 // wp.media.controller.GalleryAdd
639 // ---------------------------------
640 media.controller.GalleryAdd = media.controller.Library.extend({
641 defaults: _.defaults({
642 id: 'gallery-library',
643 filterable: 'uploaded',
646 toolbar: 'gallery-add',
647 title: l10n.addToGalleryTitle,
650 // Don't sync the selection, as the Edit Gallery library
651 // *is* the selection.
653 }, media.controller.Library.prototype.defaults ),
655 initialize: function() {
656 // If we haven't been provided a `library`, create a `Selection`.
657 if ( ! this.get('library') )
658 this.set( 'library', media.query({ type: 'image' }) );
660 media.controller.Library.prototype.initialize.apply( this, arguments );
663 activate: function() {
664 var library = this.get('library'),
665 edit = this.frame.state('gallery-edit').get('library');
667 if ( this.editLibrary && this.editLibrary !== edit )
668 library.unobserve( this.editLibrary );
670 // Accepts attachments that exist in the original library and
671 // that do not exist in gallery's library.
672 library.validator = function( attachment ) {
673 return !! this.mirroring.getByCid( attachment.cid ) && ! edit.getByCid( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
676 library.observe( edit );
677 this.editLibrary = edit;
679 media.controller.Library.prototype.activate.apply( this, arguments );
683 // wp.media.controller.FeaturedImage
684 // ---------------------------------
685 media.controller.FeaturedImage = media.controller.Library.extend({
686 defaults: _.defaults({
687 id: 'featured-image',
688 filterable: 'uploaded',
690 toolbar: 'featured-image',
691 title: l10n.setFeaturedImageTitle,
695 }, media.controller.Library.prototype.defaults ),
697 initialize: function() {
698 var library, comparator;
700 // If we haven't been provided a `library`, create a `Selection`.
701 if ( ! this.get('library') )
702 this.set( 'library', media.query({ type: 'image' }) );
704 media.controller.Library.prototype.initialize.apply( this, arguments );
706 library = this.get('library');
707 comparator = library.comparator;
709 // Overload the library's comparator to push items that are not in
710 // the mirrored query to the front of the aggregate collection.
711 library.comparator = function( a, b ) {
712 var aInQuery = !! this.mirroring.getByCid( a.cid ),
713 bInQuery = !! this.mirroring.getByCid( b.cid );
715 if ( ! aInQuery && bInQuery )
717 else if ( aInQuery && ! bInQuery )
720 return comparator.apply( this, arguments );
723 // Add all items in the selection to the library, so any featured
724 // images that are not initially loaded still appear.
725 library.observe( this.get('selection') );
728 activate: function() {
729 this.updateSelection();
730 this.frame.on( 'open', this.updateSelection, this );
731 media.controller.Library.prototype.activate.apply( this, arguments );
734 deactivate: function() {
735 this.frame.off( 'open', this.updateSelection, this );
736 media.controller.Library.prototype.deactivate.apply( this, arguments );
739 updateSelection: function() {
740 var selection = this.get('selection'),
741 id = media.view.settings.post.featuredImageId,
744 if ( '' !== id && -1 !== id ) {
745 attachment = Attachment.get( id );
749 selection.reset( attachment ? [ attachment ] : [] );
754 // wp.media.controller.Embed
755 // -------------------------
756 media.controller.Embed = media.controller.State.extend({
762 toolbar: 'main-embed',
765 title: l10n.insertFromUrlTitle,
769 // The amount of time used when debouncing the scan.
772 initialize: function() {
773 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
774 this.props = new Backbone.Model({ url: '' });
775 this.props.on( 'change:url', this.debouncedScan, this );
776 this.props.on( 'change:url', this.refresh, this );
777 this.on( 'scan', this.scanImage, this );
788 // Scan is triggered with the list of `attributes` to set on the
789 // state, useful for the 'type' attribute and 'scanners' attribute,
790 // an array of promise objects for asynchronous scan operations.
791 if ( this.props.get('url') )
792 this.trigger( 'scan', attributes );
794 if ( attributes.scanners.length ) {
795 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
796 scanners.always( function() {
797 if ( embed.get('scanners') === scanners )
798 embed.set( 'loading', false );
801 attributes.scanners = null;
804 attributes.loading = !! attributes.scanners;
805 this.set( attributes );
808 scanImage: function( attributes ) {
809 var frame = this.frame,
811 url = this.props.get('url'),
813 deferred = $.Deferred();
815 attributes.scanners.push( deferred.promise() );
817 // Try to load the image and find its width/height.
818 image.onload = function() {
821 if ( state !== frame.state() || url !== state.props.get('url') )
834 image.onerror = deferred.reject;
838 refresh: function() {
839 this.frame.toolbar.get().refresh();
843 this.props.clear().set({ url: '' });
851 * ========================================================================
853 * ========================================================================
859 // A subview manager.
861 media.Views = function( view, views ) {
863 this._views = _.isArray( views ) ? { '': views } : views || {};
866 media.Views.extend = Backbone.Model.extend;
868 _.extend( media.Views.prototype, {
869 // ### Fetch all of the subviews
871 // Returns an array of all subviews.
873 return _.flatten( this._views );
876 // ### Get a selector's subviews
878 // Fetches all subviews that match a given `selector`.
880 // If no `selector` is provided, it will grab all subviews attached
881 // to the view's root.
882 get: function( selector ) {
883 selector = selector || '';
884 return this._views[ selector ];
887 // ### Get a selector's first subview
889 // Fetches the first subview that matches a given `selector`.
891 // If no `selector` is provided, it will grab the first subview
892 // attached to the view's root.
894 // Useful when a selector only has one subview at a time.
895 first: function( selector ) {
896 var views = this.get( selector );
897 return views && views.length ? views[0] : null;
900 // ### Register subview(s)
902 // Registers any number of `views` to a `selector`.
904 // When no `selector` is provided, the root selector (the empty string)
905 // is used. `views` accepts a `Backbone.View` instance or an array of
906 // `Backbone.View` instances.
910 // Accepts an `options` object, which has a significant effect on the
911 // resulting behavior.
913 // `options.silent` – *boolean, `false`*
914 // > If `options.silent` is true, no DOM modifications will be made.
916 // `options.add` – *boolean, `false`*
917 // > Use `Views.add()` as a shortcut for setting `options.add` to true.
919 // > By default, the provided `views` will replace
920 // any existing views associated with the selector. If `options.add`
921 // is true, the provided `views` will be added to the existing views.
923 // `options.at` – *integer, `undefined`*
924 // > When adding, to insert `views` at a specific index, use
925 // `options.at`. By default, `views` are added to the end of the array.
926 set: function( selector, views, options ) {
929 if ( ! _.isString( selector ) ) {
935 options = options || {};
936 views = _.isArray( views ) ? views : [ views ];
937 existing = this.get( selector );
942 if ( _.isUndefined( options.at ) ) {
943 next = existing.concat( views );
946 next.splice.apply( next, [ options.at, 0 ].concat( views ) );
949 _.each( next, function( view ) {
950 view.__detach = true;
953 _.each( existing, function( view ) {
960 _.each( next, function( view ) {
961 delete view.__detach;
966 this._views[ selector ] = next;
968 _.each( views, function( subview ) {
969 var constructor = subview.Views || media.Views,
970 subviews = subview.views = subview.views || new constructor( subview );
971 subviews.parent = this.view;
972 subviews.selector = selector;
975 if ( ! options.silent )
976 this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
981 // ### Add subview(s) to existing subviews
983 // An alias to `Views.set()`, which defaults `options.add` to true.
985 // Adds any number of `views` to a `selector`.
987 // When no `selector` is provided, the root selector (the empty string)
988 // is used. `views` accepts a `Backbone.View` instance or an array of
989 // `Backbone.View` instances.
991 // Use `Views.set()` when setting `options.add` to `false`.
993 // Accepts an `options` object. By default, provided `views` will be
994 // inserted at the end of the array of existing views. To insert
995 // `views` at a specific index, use `options.at`. If `options.silent`
996 // is true, no DOM modifications will be made.
998 // For more information on the `options` object, see `Views.set()`.
999 add: function( selector, views, options ) {
1000 if ( ! _.isString( selector ) ) {
1006 return this.set( selector, views, _.extend({ add: true }, options ) );
1009 // ### Stop tracking subviews
1011 // Stops tracking `views` registered to a `selector`. If no `views` are
1012 // set, then all of the `selector`'s subviews will be unregistered and
1015 // Accepts an `options` object. If `options.silent` is set, `dispose`
1016 // will *not* be triggered on the unregistered views.
1017 unset: function( selector, views, options ) {
1020 if ( ! _.isString( selector ) ) {
1026 views = views || [];
1028 if ( existing = this.get( selector ) ) {
1029 views = _.isArray( views ) ? views : [ views ];
1030 this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
1033 if ( ! options || ! options.silent )
1034 _.invoke( views, 'dispose' );
1039 // ### Detach all subviews
1041 // Detaches all subviews from the DOM.
1043 // Helps to preserve all subview events when re-rendering the master
1044 // view. Used in conjunction with `Views.render()`.
1045 detach: function() {
1046 $( _.pluck( this.all(), 'el' ) ).detach();
1050 // ### Render all subviews
1052 // Renders all subviews. Used in conjunction with `Views.detach()`.
1053 render: function() {
1055 ready: this._isReady()
1058 _.each( this._views, function( views, selector ) {
1059 this._attach( selector, views, options );
1062 this.rendered = true;
1066 // ### Dispose all subviews
1068 // Triggers the `dispose()` method on all subviews. Detaches the master
1069 // view from its parent. Resets the internals of the views manager.
1071 // Accepts an `options` object. If `options.silent` is set, `unset`
1072 // will *not* be triggered on the master view's parent.
1073 dispose: function( options ) {
1074 if ( ! options || ! options.silent ) {
1075 if ( this.parent && this.parent.views )
1076 this.parent.views.unset( this.selector, this.view, { silent: true });
1078 delete this.selector;
1081 _.invoke( this.all(), 'dispose' );
1086 // ### Replace a selector's subviews
1088 // By default, sets the `$target` selector's html to the subview `els`.
1090 // Can be overridden in subclasses.
1091 replace: function( $target, els ) {
1092 $target.html( els );
1096 // ### Insert subviews into a selector
1098 // By default, appends the subview `els` to the end of the `$target`
1099 // selector. If `options.at` is set, inserts the subview `els` at the
1102 // Can be overridden in subclasses.
1103 insert: function( $target, els, options ) {
1104 var at = options && options.at,
1107 if ( _.isNumber( at ) && ($children = $target.children()).length > at )
1108 $children.eq( at ).before( els );
1110 $target.append( els );
1115 // ### Trigger the ready event
1117 // **Only use this method if you know what you're doing.**
1118 // For performance reasons, this method does not check if the view is
1119 // actually attached to the DOM. It's taking your word for it.
1121 // Fires the ready event on the current view and all attached subviews.
1123 this.view.trigger('ready');
1125 // Find all attached subviews, and call ready on them.
1126 _.chain( this.all() ).map( function( view ) {
1128 }).flatten().where({ attached: true }).invoke('ready');
1131 // #### Internal. Attaches a series of views to a selector.
1133 // Checks to see if a matching selector exists, renders the views,
1134 // performs the proper DOM operation, and then checks if the view is
1135 // attached to the document.
1136 _attach: function( selector, views, options ) {
1137 var $selector = selector ? this.view.$( selector ) : this.view.$el,
1140 // Check if we found a location to attach the views.
1141 if ( ! $selector.length )
1144 managers = _.chain( views ).pluck('views').flatten().value();
1146 // Render the views if necessary.
1147 _.each( managers, function( manager ) {
1148 if ( manager.rendered )
1151 manager.view.render();
1152 manager.rendered = true;
1155 // Insert or replace the views.
1156 this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
1158 // Set attached and trigger ready if the current view is already
1159 // attached to the DOM.
1160 _.each( managers, function( manager ) {
1161 manager.attached = true;
1163 if ( options.ready )
1170 // #### Internal. Checks if the current view is in the DOM.
1171 _isReady: function() {
1172 var node = this.view.el;
1174 if ( node === document.body )
1176 node = node.parentNode;
1186 // The base view class.
1187 media.View = Backbone.View.extend({
1188 // The constructor for the `Views` manager.
1191 constructor: function( options ) {
1192 this.views = new this.Views( this, this.views );
1193 this.on( 'ready', this.ready, this );
1195 if ( options && options.controller )
1196 this.controller = options.controller;
1198 Backbone.View.apply( this, arguments );
1201 dispose: function() {
1202 // Undelegating events, removing events from the model, and
1203 // removing events from the controller mirror the code for
1204 // `Backbone.View.dispose` in Backbone master.
1205 this.undelegateEvents();
1207 if ( this.model && this.model.off )
1208 this.model.off( null, null, this );
1210 if ( this.collection && this.collection.off )
1211 this.collection.off( null, null, this );
1213 // Unbind controller events.
1214 if ( this.controller && this.controller.off )
1215 this.controller.off( null, null, this );
1217 // Recursively dispose child views.
1219 this.views.dispose();
1224 remove: function() {
1226 return Backbone.View.prototype.remove.apply( this, arguments );
1229 render: function() {
1233 options = this.prepare();
1235 this.views.detach();
1237 if ( this.template ) {
1238 options = options || {};
1239 this.trigger( 'prepare', options );
1240 this.$el.html( this.template( options ) );
1243 this.views.render();
1247 prepare: function() {
1248 return this.options;
1251 ready: function() {}
1255 * wp.media.view.Frame
1257 media.view.Frame = media.View.extend({
1258 initialize: function() {
1259 this._createRegions();
1260 this._createStates();
1263 _createRegions: function() {
1264 // Clone the regions array.
1265 this.regions = this.regions ? this.regions.slice() : [];
1267 // Initialize regions.
1268 _.each( this.regions, function( region ) {
1269 this[ region ] = new media.controller.Region({
1272 selector: '.media-frame-' + region
1277 _createStates: function() {
1278 // Create the default `states` collection.
1279 this.states = new Backbone.Collection( null, {
1280 model: media.controller.State
1283 // Ensure states have a reference to the frame.
1284 this.states.on( 'add', function( model ) {
1286 model.trigger('ready');
1289 if ( this.options.states )
1290 this.states.add( this.options.states );
1294 this.states.invoke( 'trigger', 'reset' );
1299 // Make the `Frame` a `StateMachine`.
1300 _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1303 * wp.media.view.MediaFrame
1305 media.view.MediaFrame = media.view.Frame.extend({
1306 className: 'media-frame',
1307 template: media.template('media-frame'),
1308 regions: ['menu','title','content','toolbar','router'],
1310 initialize: function() {
1311 media.view.Frame.prototype.initialize.apply( this, arguments );
1313 _.defaults( this.options, {
1319 // Ensure core UI is enabled.
1320 this.$el.addClass('wp-core-ui');
1322 // Initialize modal container view.
1323 if ( this.options.modal ) {
1324 this.modal = new media.view.Modal({
1326 title: this.options.title
1329 this.modal.content( this );
1332 // Force the uploader off if the upload limit has been exceeded or
1333 // if the browser isn't supported.
1334 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported )
1335 this.options.uploader = false;
1337 // Initialize window-wide uploader.
1338 if ( this.options.uploader ) {
1339 this.uploader = new media.view.UploaderWindow({
1342 dropzone: this.modal ? this.modal.$el : this.$el,
1346 this.views.set( '.media-frame-uploader', this.uploader );
1349 this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1351 // Bind default title creation.
1352 this.on( 'title:create:default', this.createTitle, this );
1353 this.title.mode('default');
1355 // Bind default menu.
1356 this.on( 'menu:create:default', this.createMenu, this );
1359 render: function() {
1360 // Activate the default state if no active state exists.
1361 if ( ! this.state() && this.options.state )
1362 this.setState( this.options.state );
1364 return media.view.Frame.prototype.render.apply( this, arguments );
1367 createTitle: function( title ) {
1368 title.view = new media.View({
1374 createMenu: function( menu ) {
1375 menu.view = new media.view.Menu({
1380 createToolbar: function( toolbar ) {
1381 toolbar.view = new media.view.Toolbar({
1386 createRouter: function( router ) {
1387 router.view = new media.view.Router({
1392 createIframeStates: function( options ) {
1393 var settings = media.view.settings,
1394 tabs = settings.tabs,
1395 tabUrl = settings.tabUrl,
1398 if ( ! tabs || ! tabUrl )
1401 // Add the post ID to the tab URL if it exists.
1402 $postId = $('#post_ID');
1403 if ( $postId.length )
1404 tabUrl += '&post_id=' + $postId.val();
1406 // Generate the tab states.
1407 _.each( tabs, function( title, id ) {
1408 var frame = this.state( 'iframe:' + id ).set( _.defaults({
1410 src: tabUrl + '&tab=' + id,
1417 this.on( 'content:create:iframe', this.iframeContent, this );
1418 this.on( 'menu:render:default', this.iframeMenu, this );
1419 this.on( 'open', this.hijackThickbox, this );
1420 this.on( 'close', this.restoreThickbox, this );
1423 iframeContent: function( content ) {
1424 this.$el.addClass('hide-toolbar');
1425 content.view = new media.view.Iframe({
1430 iframeMenu: function( view ) {
1436 _.each( media.view.settings.tabs, function( title, id ) {
1437 views[ 'iframe:' + id ] = {
1438 text: this.state( 'iframe:' + id ).get('title'),
1446 hijackThickbox: function() {
1449 if ( ! window.tb_remove || this._tb_remove )
1452 this._tb_remove = window.tb_remove;
1453 window.tb_remove = function() {
1456 frame.setState( frame.options.state );
1457 frame._tb_remove.call( window );
1461 restoreThickbox: function() {
1462 if ( ! this._tb_remove )
1465 window.tb_remove = this._tb_remove;
1466 delete this._tb_remove;
1470 // Map some of the modal's methods to the frame.
1471 _.each(['open','close','attach','detach','escape'], function( method ) {
1472 media.view.MediaFrame.prototype[ method ] = function( view ) {
1474 this.modal[ method ].apply( this.modal, arguments );
1480 * wp.media.view.MediaFrame.Select
1482 media.view.MediaFrame.Select = media.view.MediaFrame.extend({
1483 initialize: function() {
1484 media.view.MediaFrame.prototype.initialize.apply( this, arguments );
1486 _.defaults( this.options, {
1493 this.createSelection();
1494 this.createStates();
1495 this.bindHandlers();
1498 createSelection: function() {
1499 var controller = this,
1500 selection = this.options.selection;
1502 if ( ! (selection instanceof media.model.Selection) ) {
1503 this.options.selection = new media.model.Selection( selection, {
1504 multiple: this.options.multiple
1509 attachments: new Attachments(),
1514 createStates: function() {
1515 var options = this.options;
1517 if ( this.options.states )
1520 // Add the default states.
1523 new media.controller.Library({
1524 library: media.query( options.library ),
1525 multiple: options.multiple,
1526 title: options.title,
1532 bindHandlers: function() {
1533 this.on( 'router:create:browse', this.createRouter, this );
1534 this.on( 'router:render:browse', this.browseRouter, this );
1535 this.on( 'content:create:browse', this.browseContent, this );
1536 this.on( 'content:render:upload', this.uploadContent, this );
1537 this.on( 'toolbar:create:select', this.createSelectToolbar, this );
1541 browseRouter: function( view ) {
1544 text: l10n.uploadFilesTitle,
1548 text: l10n.mediaLibraryTitle,
1555 browseContent: function( content ) {
1556 var state = this.state();
1558 this.$el.removeClass('hide-toolbar');
1560 // Browse our library of attachments.
1561 content.view = new media.view.AttachmentsBrowser({
1563 collection: state.get('library'),
1564 selection: state.get('selection'),
1566 sortable: state.get('sortable'),
1567 search: state.get('searchable'),
1568 filters: state.get('filterable'),
1569 display: state.get('displaySettings'),
1570 dragInfo: state.get('dragInfo'),
1572 AttachmentView: state.get('AttachmentView')
1576 uploadContent: function() {
1577 this.$el.removeClass('hide-toolbar');
1578 this.content.set( new media.view.UploaderInline({
1584 createSelectToolbar: function( toolbar, options ) {
1585 options = options || this.options.button || {};
1586 options.controller = this;
1588 toolbar.view = new media.view.Toolbar.Select( options );
1593 * wp.media.view.MediaFrame.Post
1595 media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
1596 initialize: function() {
1597 _.defaults( this.options, {
1603 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
1604 this.createIframeStates();
1607 createStates: function() {
1608 var options = this.options;
1610 // Add the default states.
1613 new media.controller.Library({
1615 title: l10n.insertMediaTitle,
1617 toolbar: 'main-insert',
1619 library: media.query( options.library ),
1620 multiple: options.multiple ? 'reset' : false,
1623 // If the user isn't allowed to edit fields,
1624 // can they still edit it locally?
1625 allowLocalEdits: true,
1627 // Show the attachment display settings.
1628 displaySettings: true,
1629 // Update user settings when users adjust the
1630 // attachment display settings.
1631 displayUserSettings: true
1634 new media.controller.Library({
1636 title: l10n.createGalleryTitle,
1638 toolbar: 'main-gallery',
1639 filterable: 'uploaded',
1643 library: media.query( _.defaults({
1645 }, options.library ) )
1649 new media.controller.Embed(),
1652 new media.controller.GalleryEdit({
1653 library: options.selection,
1654 editing: options.editing,
1658 new media.controller.GalleryAdd()
1662 if ( media.view.settings.post.featuredImageId ) {
1663 this.states.add( new media.controller.FeaturedImage() );
1667 bindHandlers: function() {
1668 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
1669 this.on( 'menu:create:gallery', this.createMenu, this );
1670 this.on( 'toolbar:create:main-insert', this.createToolbar, this );
1671 this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
1672 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
1673 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
1677 'default': 'mainMenu',
1678 'gallery': 'galleryMenu'
1682 'embed': 'embedContent',
1683 'edit-selection': 'editSelectionContent'
1687 'main-insert': 'mainInsertToolbar',
1688 'main-gallery': 'mainGalleryToolbar',
1689 'gallery-edit': 'galleryEditToolbar',
1690 'gallery-add': 'galleryAddToolbar'
1694 _.each( handlers, function( regionHandlers, region ) {
1695 _.each( regionHandlers, function( callback, handler ) {
1696 this.on( region + ':render:' + handler, this[ callback ], this );
1702 mainMenu: function( view ) {
1704 'library-separator': new media.View({
1705 className: 'separator',
1711 galleryMenu: function( view ) {
1712 var lastState = this.lastState(),
1713 previous = lastState && lastState.id,
1718 text: l10n.cancelGalleryTitle,
1722 frame.setState( previous );
1727 separateCancel: new media.View({
1728 className: 'separator',
1735 embedContent: function() {
1736 var view = new media.view.Embed({
1741 this.content.set( view );
1745 editSelectionContent: function() {
1746 var state = this.state(),
1747 selection = state.get('selection'),
1750 view = new media.view.AttachmentsBrowser({
1752 collection: selection,
1753 selection: selection,
1759 AttachmentView: media.view.Attachment.EditSelection
1762 view.toolbar.set( 'backToLibrary', {
1763 text: l10n.returnToLibrary,
1767 this.controller.content.mode('browse');
1771 // Browse our library of attachments.
1772 this.content.set( view );
1776 selectionStatusToolbar: function( view ) {
1777 var editable = this.state().get('editable');
1779 view.set( 'selection', new media.view.Selection({
1781 collection: this.state().get('selection'),
1784 // If the selection is editable, pass the callback to
1785 // switch the content mode.
1786 editable: editable && function() {
1787 this.controller.content.mode('edit-selection');
1792 mainInsertToolbar: function( view ) {
1793 var controller = this;
1795 this.selectionStatusToolbar( view );
1797 view.set( 'insert', {
1800 text: l10n.insertIntoPost,
1801 requires: { selection: true },
1804 var state = controller.state(),
1805 selection = state.get('selection');
1808 state.trigger( 'insert', selection ).reset();
1813 mainGalleryToolbar: function( view ) {
1814 var controller = this;
1816 this.selectionStatusToolbar( view );
1818 view.set( 'gallery', {
1820 text: l10n.createNewGallery,
1822 requires: { selection: true },
1825 var selection = controller.state().get('selection'),
1826 edit = controller.state('gallery-edit'),
1827 models = selection.where({ type: 'image' });
1829 edit.set( 'library', new media.model.Selection( models, {
1830 props: selection.props.toJSON(),
1834 this.controller.setState('gallery-edit');
1839 featuredImageToolbar: function( toolbar ) {
1840 this.createSelectToolbar( toolbar, {
1841 text: l10n.setFeaturedImage,
1842 state: this.options.state || 'upload'
1846 mainEmbedToolbar: function( toolbar ) {
1847 toolbar.view = new media.view.Toolbar.Embed({
1852 galleryEditToolbar: function() {
1853 var editing = this.state().get('editing');
1854 this.toolbar.set( new media.view.Toolbar({
1859 text: editing ? l10n.updateGallery : l10n.insertGallery,
1861 requires: { library: true },
1864 var controller = this.controller,
1865 state = controller.state();
1868 state.trigger( 'update', state.get('library') );
1871 // @todo: Make the state activated dynamic (instead of hardcoded).
1872 controller.setState('upload');
1879 galleryAddToolbar: function() {
1880 this.toolbar.set( new media.view.Toolbar({
1885 text: l10n.addToGallery,
1887 requires: { selection: true },
1890 var controller = this.controller,
1891 state = controller.state(),
1892 edit = controller.state('gallery-edit');
1894 edit.get('library').add( state.get('selection').models );
1895 state.trigger('reset');
1896 controller.setState('gallery-edit');
1905 * wp.media.view.Modal
1907 media.view.Modal = media.View.extend({
1909 template: media.template('media-modal'),
1916 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
1917 'keydown': 'keydown'
1920 initialize: function() {
1921 _.defaults( this.options, {
1922 container: document.body,
1929 prepare: function() {
1931 title: this.options.title
1935 attach: function() {
1936 if ( this.views.attached )
1939 if ( ! this.views.rendered )
1942 this.$el.appendTo( this.options.container );
1944 // Manually mark the view as attached and trigger ready.
1945 this.views.attached = true;
1948 return this.propagate('attach');
1951 detach: function() {
1952 if ( this.$el.is(':visible') )
1956 this.views.attached = false;
1957 return this.propagate('detach');
1962 options = this.options;
1964 if ( $el.is(':visible') )
1967 if ( ! this.views.attached )
1970 // If the `freeze` option is set, record the window's scroll position.
1971 if ( options.freeze ) {
1973 scrollTop: $( window ).scrollTop()
1978 return this.propagate('open');
1981 close: function( options ) {
1982 var freeze = this._freeze;
1984 if ( ! this.views.attached || ! this.$el.is(':visible') )
1988 this.propagate('close');
1990 // If the `freeze` option is set, restore the container's scroll position.
1992 $( window ).scrollTop( freeze.scrollTop );
1995 if ( options && options.escape )
1996 this.propagate('escape');
2001 escape: function() {
2002 return this.close({ escape: true });
2005 escapeHandler: function( event ) {
2006 event.preventDefault();
2010 content: function( content ) {
2011 this.views.set( '.media-modal-content', content );
2015 // Triggers a modal event and if the `propagate` option is set,
2016 // forwards events to the modal's controller.
2017 propagate: function( id ) {
2020 if ( this.options.propagate )
2021 this.controller.trigger( id );
2026 keydown: function( event ) {
2027 // Close the modal when escape is pressed.
2028 if ( 27 === event.which ) {
2029 event.preventDefault();
2036 // wp.media.view.FocusManager
2037 // ----------------------------
2038 media.view.FocusManager = media.View.extend({
2040 keydown: 'recordTab',
2041 focusin: 'updateIndex'
2045 if ( _.isUndefined( this.index ) )
2048 // Update our collection of `$tabbables`.
2049 this.$tabbables = this.$(':tabbable');
2051 // If tab is saved, focus it.
2052 this.$tabbables.eq( this.index ).focus();
2055 recordTab: function( event ) {
2056 // Look for the tab key.
2057 if ( 9 !== event.keyCode )
2060 // First try to update the index.
2061 if ( _.isUndefined( this.index ) )
2062 this.updateIndex( event );
2064 // If we still don't have an index, bail.
2065 if ( _.isUndefined( this.index ) )
2068 var index = this.index + ( event.shiftKey ? -1 : 1 );
2070 if ( index >= 0 && index < this.$tabbables.length )
2076 updateIndex: function( event ) {
2077 this.$tabbables = this.$(':tabbable');
2079 var index = this.$tabbables.index( event.target );
2088 // wp.media.view.UploaderWindow
2089 // ----------------------------
2090 media.view.UploaderWindow = media.View.extend({
2092 className: 'uploader-window',
2093 template: media.template('uploader-window'),
2095 initialize: function() {
2098 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
2100 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
2102 browser: this.$browser,
2106 // Ensure the dropzone is a jQuery collection.
2107 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) )
2108 uploader.dropzone = $( uploader.dropzone );
2110 this.controller.on( 'activate', this.refresh, this );
2113 refresh: function() {
2114 if ( this.uploader )
2115 this.uploader.refresh();
2119 var postId = media.view.settings.post.id,
2122 // If the uploader already exists, bail.
2123 if ( this.uploader )
2127 this.options.uploader.params.post_id = postId;
2129 this.uploader = new wp.Uploader( this.options.uploader );
2131 dropzone = this.uploader.dropzone;
2132 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
2133 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
2137 var $el = this.$el.show();
2139 // Ensure that the animation is triggered by waiting until
2140 // the transparent element is painted into the DOM.
2141 _.defer( function() {
2142 $el.css({ opacity: 1 });
2147 var $el = this.$el.css({ opacity: 0 });
2149 media.transition( $el ).done( function() {
2150 // Transition end events are subject to race conditions.
2151 // Make sure that the value is set as intended.
2152 if ( '0' === $el.css('opacity') )
2158 media.view.UploaderInline = media.View.extend({
2160 className: 'uploader-inline',
2161 template: media.template('uploader-inline'),
2163 initialize: function() {
2164 _.defaults( this.options, {
2169 if ( ! this.options.$browser && this.controller.uploader )
2170 this.options.$browser = this.controller.uploader.$browser;
2172 if ( _.isUndefined( this.options.postId ) )
2173 this.options.postId = media.view.settings.post.id;
2175 if ( this.options.status ) {
2176 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
2177 controller: this.controller
2182 dispose: function() {
2183 if ( this.disposing )
2184 return media.View.prototype.dispose.apply( this, arguments );
2186 // Run remove on `dispose`, so we can be sure to refresh the
2187 // uploader with a view-less DOM. Track whether we're disposing
2188 // so we don't trigger an infinite loop.
2189 this.disposing = true;
2190 return this.remove();
2193 remove: function() {
2194 var result = media.View.prototype.remove.apply( this, arguments );
2196 _.defer( _.bind( this.refresh, this ) );
2200 refresh: function() {
2201 var uploader = this.controller.uploader;
2208 var $browser = this.options.$browser,
2211 if ( this.controller.uploader ) {
2212 $placeholder = this.$('.browser');
2214 // Check if we've already replaced the placeholder.
2215 if ( $placeholder[0] === $browser[0] )
2218 $browser.detach().text( $placeholder.text() );
2219 $browser[0].className = $placeholder[0].className;
2220 $placeholder.replaceWith( $browser.show() );
2229 * wp.media.view.UploaderStatus
2231 media.view.UploaderStatus = media.View.extend({
2232 className: 'media-uploader-status',
2233 template: media.template('uploader-status'),
2236 'click .upload-dismiss-errors': 'dismiss'
2239 initialize: function() {
2240 this.queue = wp.Uploader.queue;
2241 this.queue.on( 'add remove reset', this.visibility, this );
2242 this.queue.on( 'add remove reset change:percent', this.progress, this );
2243 this.queue.on( 'add remove reset change:uploading', this.info, this );
2245 this.errors = wp.Uploader.errors;
2246 this.errors.reset();
2247 this.errors.on( 'add remove reset', this.visibility, this );
2248 this.errors.on( 'add', this.error, this );
2251 dispose: function() {
2252 wp.Uploader.queue.off( null, null, this );
2253 media.View.prototype.dispose.apply( this, arguments );
2257 visibility: function() {
2258 this.$el.toggleClass( 'uploading', !! this.queue.length );
2259 this.$el.toggleClass( 'errors', !! this.errors.length );
2260 this.$el.toggle( !! this.queue.length || !! this.errors.length );
2265 '$bar': '.media-progress-bar div',
2266 '$index': '.upload-index',
2267 '$total': '.upload-total',
2268 '$filename': '.upload-filename'
2269 }, function( selector, key ) {
2270 this[ key ] = this.$( selector );
2278 progress: function() {
2279 var queue = this.queue,
2283 if ( ! $bar || ! queue.length )
2286 $bar.width( ( queue.reduce( function( memo, attachment ) {
2287 if ( ! attachment.get('uploading') )
2290 var percent = attachment.get('percent');
2291 return memo + ( _.isNumber( percent ) ? percent : 100 );
2292 }, 0 ) / queue.length ) + '%' );
2296 var queue = this.queue,
2299 if ( ! queue.length )
2302 active = this.queue.find( function( attachment, i ) {
2304 return attachment.get('uploading');
2307 this.$index.text( index + 1 );
2308 this.$total.text( queue.length );
2309 this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
2312 filename: function( filename ) {
2313 return media.truncate( _.escape( filename ), 24 );
2316 error: function( error ) {
2317 this.views.add( '.upload-errors', new media.view.UploaderStatusError({
2318 filename: this.filename( error.get('file').name ),
2319 message: error.get('message')
2323 dismiss: function( event ) {
2324 var errors = this.views.get('.upload-errors');
2326 event.preventDefault();
2329 _.invoke( errors, 'remove' );
2330 wp.Uploader.errors.reset();
2334 media.view.UploaderStatusError = media.View.extend({
2335 className: 'upload-error',
2336 template: media.template('uploader-status-error')
2340 * wp.media.view.Toolbar
2342 media.view.Toolbar = media.View.extend({
2344 className: 'media-toolbar',
2346 initialize: function() {
2347 var state = this.controller.state(),
2348 selection = this.selection = state.get('selection'),
2349 library = this.library = state.get('library');
2353 // The toolbar is composed of two `PriorityList` views.
2354 this.primary = new media.view.PriorityList();
2355 this.secondary = new media.view.PriorityList();
2356 this.primary.$el.addClass('media-toolbar-primary');
2357 this.secondary.$el.addClass('media-toolbar-secondary');
2359 this.views.set([ this.secondary, this.primary ]);
2361 if ( this.options.items )
2362 this.set( this.options.items, { silent: true });
2364 if ( ! this.options.silent )
2368 selection.on( 'add remove reset', this.refresh, this );
2370 library.on( 'add remove reset', this.refresh, this );
2373 dispose: function() {
2374 if ( this.selection )
2375 this.selection.off( null, null, this );
2377 this.library.off( null, null, this );
2378 return media.View.prototype.dispose.apply( this, arguments );
2385 set: function( id, view, options ) {
2387 options = options || {};
2389 // Accept an object with an `id` : `view` mapping.
2390 if ( _.isObject( id ) ) {
2391 _.each( id, function( view, id ) {
2392 this.set( id, view, { silent: true });
2396 if ( ! ( view instanceof Backbone.View ) ) {
2397 view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
2398 view = new media.view.Button( view ).render();
2401 view.controller = view.controller || this.controller;
2403 this._views[ id ] = view;
2405 list = view.options.priority < 0 ? 'secondary' : 'primary';
2406 this[ list ].set( id, view, options );
2409 if ( ! options.silent )
2415 get: function( id ) {
2416 return this._views[ id ];
2419 unset: function( id, options ) {
2420 delete this._views[ id ];
2421 this.primary.unset( id, options );
2422 this.secondary.unset( id, options );
2424 if ( ! options || ! options.silent )
2429 refresh: function() {
2430 var state = this.controller.state(),
2431 library = state.get('library'),
2432 selection = state.get('selection');
2434 _.each( this._views, function( button ) {
2435 if ( ! button.model || ! button.options || ! button.options.requires )
2438 var requires = button.options.requires,
2441 if ( requires.selection && selection && ! selection.length )
2443 else if ( requires.library && library && ! library.length )
2446 button.model.set( 'disabled', disabled );
2451 // wp.media.view.Toolbar.Select
2452 // ----------------------------
2453 media.view.Toolbar.Select = media.view.Toolbar.extend({
2454 initialize: function() {
2455 var options = this.options,
2456 controller = options.controller,
2457 selection = controller.state().get('selection');
2459 _.bindAll( this, 'clickSelect' );
2461 _.defaults( options, {
2468 // Does the button rely on the selection?
2474 options.items = _.defaults( options.items || {}, {
2479 click: this.clickSelect,
2480 requires: options.requires
2484 media.view.Toolbar.prototype.initialize.apply( this, arguments );
2487 clickSelect: function() {
2488 var options = this.options,
2489 controller = this.controller;
2491 if ( options.close )
2494 if ( options.event )
2495 controller.state().trigger( options.event );
2497 if ( options.reset )
2500 if ( options.state )
2501 controller.setState( options.state );
2505 // wp.media.view.Toolbar.Embed
2506 // ---------------------------
2507 media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
2508 initialize: function() {
2509 _.defaults( this.options, {
2510 text: l10n.insertIntoPost,
2514 media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
2517 refresh: function() {
2518 var url = this.controller.state().props.get('url');
2519 this.get('select').model.set( 'disabled', ! url || url === 'http://' );
2521 media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
2526 * wp.media.view.Button
2528 media.view.Button = media.View.extend({
2530 className: 'media-button',
2531 attributes: { href: '#' },
2544 initialize: function() {
2545 // Create a model with the provided `defaults`.
2546 this.model = new Backbone.Model( this.defaults );
2548 // If any of the `options` have a key from `defaults`, apply its
2549 // value to the `model` and remove it from the `options object.
2550 _.each( this.defaults, function( def, key ) {
2551 var value = this.options[ key ];
2552 if ( _.isUndefined( value ) )
2555 this.model.set( key, value );
2556 delete this.options[ key ];
2559 this.model.on( 'change', this.render, this );
2562 render: function() {
2563 var classes = [ 'button', this.className ],
2564 model = this.model.toJSON();
2567 classes.push( 'button-' + model.style );
2570 classes.push( 'button-' + model.size );
2572 classes = _.uniq( classes.concat( this.options.classes ) );
2573 this.el.className = classes.join(' ');
2575 this.$el.attr( 'disabled', model.disabled );
2576 this.$el.text( this.model.get('text') );
2581 click: function( event ) {
2582 if ( '#' === this.attributes.href )
2583 event.preventDefault();
2585 if ( this.options.click && ! this.model.get('disabled') )
2586 this.options.click.apply( this, arguments );
2591 * wp.media.view.ButtonGroup
2593 media.view.ButtonGroup = media.View.extend({
2595 className: 'button-group button-large media-button-group',
2597 initialize: function() {
2598 this.buttons = _.map( this.options.buttons || [], function( button ) {
2599 if ( button instanceof Backbone.View )
2602 return new media.view.Button( button ).render();
2605 delete this.options.buttons;
2607 if ( this.options.classes )
2608 this.$el.addClass( this.options.classes );
2611 render: function() {
2612 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
2618 * wp.media.view.PriorityList
2621 media.view.PriorityList = media.View.extend({
2624 initialize: function() {
2627 this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
2628 delete this.options.views;
2630 if ( ! this.options.silent )
2634 set: function( id, view, options ) {
2635 var priority, views, index;
2637 options = options || {};
2639 // Accept an object with an `id` : `view` mapping.
2640 if ( _.isObject( id ) ) {
2641 _.each( id, function( view, id ) {
2642 this.set( id, view );
2647 if ( ! (view instanceof Backbone.View) )
2648 view = this.toView( view, id, options );
2650 view.controller = view.controller || this.controller;
2654 priority = view.options.priority || 10;
2655 views = this.views.get() || [];
2657 _.find( views, function( existing, i ) {
2658 if ( existing.options.priority > priority ) {
2664 this._views[ id ] = view;
2665 this.views.add( view, {
2666 at: _.isNumber( index ) ? index : views.length || 0
2672 get: function( id ) {
2673 return this._views[ id ];
2676 unset: function( id ) {
2677 var view = this.get( id );
2682 delete this._views[ id ];
2686 toView: function( options ) {
2687 return new media.View( options );
2692 * wp.media.view.MenuItem
2694 media.view.MenuItem = media.View.extend({
2696 className: 'media-menu-item',
2706 _click: function( event ) {
2707 var clickOverride = this.options.click;
2710 event.preventDefault();
2712 if ( clickOverride )
2713 clickOverride.call( this );
2719 var state = this.options.state;
2721 this.controller.setState( state );
2724 render: function() {
2725 var options = this.options;
2728 this.$el.text( options.text );
2729 else if ( options.html )
2730 this.$el.html( options.html );
2737 * wp.media.view.Menu
2739 media.view.Menu = media.view.PriorityList.extend({
2741 className: 'media-menu',
2743 ItemView: media.view.MenuItem,
2746 toView: function( options, id ) {
2747 options = options || {};
2748 options[ this.property ] = options[ this.property ] || id;
2749 return new this.ItemView( options ).render();
2753 media.view.PriorityList.prototype.ready.apply( this, arguments );
2758 media.view.PriorityList.prototype.set.apply( this, arguments );
2763 media.view.PriorityList.prototype.unset.apply( this, arguments );
2767 visibility: function() {
2768 var region = this.region,
2769 view = this.controller[ region ].get(),
2770 views = this.views.get(),
2771 hide = ! views || views.length < 2;
2773 if ( this === view )
2774 this.controller.$el.toggleClass( 'hide-' + region, hide );
2777 select: function( id ) {
2778 var view = this.get( id );
2784 view.$el.addClass('active');
2787 deselect: function() {
2788 this.$el.children().removeClass('active');
2793 * wp.media.view.RouterItem
2795 media.view.RouterItem = media.view.MenuItem.extend({
2797 var contentMode = this.options.contentMode;
2799 this.controller.content.mode( contentMode );
2804 * wp.media.view.Router
2806 media.view.Router = media.view.Menu.extend({
2808 className: 'media-router',
2809 property: 'contentMode',
2810 ItemView: media.view.RouterItem,
2813 initialize: function() {
2814 this.controller.on( 'content:render', this.update, this );
2815 media.view.Menu.prototype.initialize.apply( this, arguments );
2818 update: function() {
2819 var mode = this.controller.content.mode();
2821 this.select( mode );
2827 * wp.media.view.Sidebar
2829 media.view.Sidebar = media.view.PriorityList.extend({
2830 className: 'media-sidebar'
2834 * wp.media.view.Attachment
2836 media.view.Attachment = media.View.extend({
2838 className: 'attachment',
2839 template: media.template('attachment'),
2842 'click .attachment-preview': 'toggleSelectionHandler',
2843 'change [data-setting]': 'updateSetting',
2844 'change [data-setting] input': 'updateSetting',
2845 'change [data-setting] select': 'updateSetting',
2846 'change [data-setting] textarea': 'updateSetting',
2847 'click .close': 'removeFromLibrary',
2848 'click .check': 'removeFromSelection',
2849 'click a': 'preventDefault'
2854 initialize: function() {
2855 var selection = this.options.selection;
2857 this.model.on( 'change:sizes change:uploading change:caption change:title', this.render, this );
2858 this.model.on( 'change:percent', this.progress, this );
2860 // Update the selection.
2861 this.model.on( 'add', this.select, this );
2862 this.model.on( 'remove', this.deselect, this );
2864 selection.on( 'reset', this.updateSelect, this );
2866 // Update the model's details view.
2867 this.model.on( 'selection:single selection:unsingle', this.details, this );
2868 this.details( this.model, this.controller.state().get('selection') );
2871 dispose: function() {
2872 var selection = this.options.selection;
2874 // Make sure all settings are saved before removing the view.
2878 selection.off( null, null, this );
2880 media.View.prototype.dispose.apply( this, arguments );
2884 render: function() {
2885 var options = _.defaults( this.model.toJSON(), {
2886 orientation: 'landscape',
2902 options.buttons = this.buttons;
2903 options.describe = this.controller.state().get('describe');
2905 if ( 'image' === options.type )
2906 options.size = this.imageSize();
2909 if ( options.nonces ) {
2910 options.can.remove = !! options.nonces['delete'];
2911 options.can.save = !! options.nonces.update;
2914 if ( this.controller.state().get('allowLocalEdits') )
2915 options.allowLocalEdits = true;
2917 this.views.detach();
2918 this.$el.html( this.template( options ) );
2920 this.$el.toggleClass( 'uploading', options.uploading );
2921 if ( options.uploading )
2922 this.$bar = this.$('.media-progress-bar div');
2926 // Check if the model is selected.
2927 this.updateSelect();
2929 // Update the save status.
2932 this.views.render();
2937 progress: function() {
2938 if ( this.$bar && this.$bar.length )
2939 this.$bar.width( this.model.get('percent') + '%' );
2942 toggleSelectionHandler: function( event ) {
2945 if ( event.shiftKey )
2947 else if ( event.ctrlKey || event.metaKey )
2950 this.toggleSelection({
2955 toggleSelection: function( options ) {
2956 var collection = this.collection,
2957 selection = this.options.selection,
2959 method = options && options.method,
2960 single, between, models, singleIndex, modelIndex;
2965 single = selection.single();
2966 method = _.isUndefined( method ) ? selection.multiple : method;
2968 // If the `method` is set to `between`, select all models that
2969 // exist between the current and the selected model.
2970 if ( 'between' === method && single && selection.multiple ) {
2971 // If the models are the same, short-circuit.
2972 if ( single === model )
2975 singleIndex = collection.indexOf( single );
2976 modelIndex = collection.indexOf( this.model );
2978 if ( singleIndex < modelIndex )
2979 models = collection.models.slice( singleIndex, modelIndex + 1 );
2981 models = collection.models.slice( modelIndex, singleIndex + 1 );
2983 selection.add( models ).single( model );
2986 // If the `method` is set to `toggle`, just flip the selection
2987 // status, regardless of whether the model is the single model.
2988 } else if ( 'toggle' === method ) {
2989 selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model );
2993 if ( method !== 'add' )
2996 if ( this.selected() ) {
2997 // If the model is the single model, remove it.
2998 // If it is not the same as the single model,
2999 // it now becomes the single model.
3000 selection[ single === model ? 'remove' : 'single' ]( model );
3002 // If the model is not selected, run the `method` on the
3003 // selection. By default, we `reset` the selection, but the
3004 // `method` can be set to `add` the model to the selection.
3005 selection[ method ]( model ).single( model );
3009 updateSelect: function() {
3010 this[ this.selected() ? 'select' : 'deselect' ]();
3013 selected: function() {
3014 var selection = this.options.selection;
3016 return !! selection.getByCid( this.model.cid );
3019 select: function( model, collection ) {
3020 var selection = this.options.selection;
3022 // Check if a selection exists and if it's the collection provided.
3023 // If they're not the same collection, bail; we're in another
3024 // selection's event loop.
3025 if ( ! selection || ( collection && collection !== selection ) )
3028 this.$el.addClass('selected');
3031 deselect: function( model, collection ) {
3032 var selection = this.options.selection;
3034 // Check if a selection exists and if it's the collection provided.
3035 // If they're not the same collection, bail; we're in another
3036 // selection's event loop.
3037 if ( ! selection || ( collection && collection !== selection ) )
3040 this.$el.removeClass('selected');
3043 details: function( model, collection ) {
3044 var selection = this.options.selection,
3047 if ( selection !== collection )
3050 details = selection.single();
3051 this.$el.toggleClass( 'details', details === this.model );
3054 preventDefault: function( event ) {
3055 event.preventDefault();
3058 imageSize: function( size ) {
3059 var sizes = this.model.get('sizes');
3061 size = size || 'medium';
3063 // Use the provided image size if possible.
3064 if ( sizes && sizes[ size ] ) {
3065 return _.clone( sizes[ size ] );
3068 url: this.model.get('url'),
3069 width: this.model.get('width'),
3070 height: this.model.get('height'),
3071 orientation: this.model.get('orientation')
3076 updateSetting: function( event ) {
3077 var $setting = $( event.target ).closest('[data-setting]'),
3080 if ( ! $setting.length )
3083 setting = $setting.data('setting');
3084 value = event.target.value;
3086 if ( this.model.get( setting ) !== value )
3087 this.save( setting, value );
3090 // Pass all the arguments to the model's save method.
3092 // Records the aggregate status of all save requests and updates the
3093 // view's classes accordingly.
3096 save = this._save = this._save || { status: 'ready' },
3097 request = this.model.save.apply( this.model, arguments ),
3098 requests = save.requests ? $.when( request, save.requests ) : request;
3100 // If we're waiting to remove 'Saved.', stop.
3101 if ( save.savedTimer )
3102 clearTimeout( save.savedTimer );
3104 this.updateSave('waiting');
3105 save.requests = requests;
3106 requests.always( function() {
3107 // If we've performed another request since this one, bail.
3108 if ( save.requests !== requests )
3111 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
3112 save.savedTimer = setTimeout( function() {
3113 view.updateSave('ready');
3114 delete save.savedTimer;
3120 updateSave: function( status ) {
3121 var save = this._save = this._save || { status: 'ready' };
3123 if ( status && status !== save.status ) {
3124 this.$el.removeClass( 'save-' + save.status );
3125 save.status = status;
3128 this.$el.addClass( 'save-' + save.status );
3132 updateAll: function() {
3133 var $settings = this.$('[data-setting]'),
3137 changed = _.chain( $settings ).map( function( el ) {
3138 var $input = $('input, textarea, select, [value]', el ),
3141 if ( ! $input.length )
3144 setting = $(el).data('setting');
3145 value = $input.val();
3147 // Record the value if it changed.
3148 if ( model.get( setting ) !== value )
3149 return [ setting, value ];
3150 }).compact().object().value();
3152 if ( ! _.isEmpty( changed ) )
3153 model.save( changed );
3156 removeFromLibrary: function( event ) {
3157 // Stop propagation so the model isn't selected.
3158 event.stopPropagation();
3160 this.collection.remove( this.model );
3163 removeFromSelection: function( event ) {
3164 var selection = this.options.selection;
3168 // Stop propagation so the model isn't selected.
3169 event.stopPropagation();
3171 selection.remove( this.model );
3176 * wp.media.view.Attachment.Library
3178 media.view.Attachment.Library = media.view.Attachment.extend({
3185 * wp.media.view.Attachment.EditLibrary
3187 media.view.Attachment.EditLibrary = media.view.Attachment.extend({
3194 * wp.media.view.Attachments
3196 media.view.Attachments = media.View.extend({
3198 className: 'attachments',
3200 cssTemplate: media.template('attachments-css'),
3206 initialize: function() {
3207 this.el.id = _.uniqueId('__attachments-view-');
3209 _.defaults( this.options, {
3210 refreshSensitivity: 200,
3211 refreshThreshold: 3,
3212 AttachmentView: media.view.Attachment,
3217 this._viewsByCid = {};
3219 this.collection.on( 'add', function( attachment, attachments, options ) {
3220 this.views.add( this.createAttachmentView( attachment ), {
3225 this.collection.on( 'remove', function( attachment, attachments, options ) {
3226 var view = this._viewsByCid[ attachment.cid ];
3227 delete this._viewsByCid[ attachment.cid ];
3233 this.collection.on( 'reset', this.render, this );
3235 // Throttle the scroll handler.
3236 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
3238 this.initSortable();
3240 _.bindAll( this, 'css' );
3241 this.model.on( 'change:edge change:gutter', this.css, this );
3242 this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
3243 if ( this.options.resize )
3244 $(window).on( 'resize.attachments', this._resizeCss );
3248 dispose: function() {
3249 this.collection.props.off( null, null, this );
3250 $(window).off( 'resize.attachments', this._resizeCss );
3251 media.View.prototype.dispose.apply( this, arguments );
3255 var $css = $( '#' + this.el.id + '-css' );
3260 media.view.Attachments.$head().append( this.cssTemplate({
3263 gutter: this.model.get('gutter')
3268 var edge = this.model.get('edge'),
3269 gutter, width, columns;
3271 if ( ! this.$el.is(':visible') )
3274 gutter = this.model.get('gutter') * 2;
3275 width = this.$el.width() - gutter;
3276 columns = Math.ceil( width / ( edge + gutter ) );
3277 edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
3281 initSortable: function() {
3282 var collection = this.collection;
3284 if ( ! this.options.sortable || ! $.fn.sortable )
3287 this.$el.sortable( _.extend({
3288 // If the `collection` has a `comparator`, disable sorting.
3289 disabled: !! collection.comparator,
3291 // Prevent attachments from being dragged outside the bounding
3293 containment: this.$el,
3295 // Change the position of the attachment as soon as the
3296 // mouse pointer overlaps a thumbnail.
3297 tolerance: 'pointer',
3299 // Record the initial `index` of the dragged model.
3300 start: function( event, ui ) {
3301 ui.item.data('sortableIndexStart', ui.item.index());
3304 // Update the model's index in the collection.
3305 // Do so silently, as the view is already accurate.
3306 update: function( event, ui ) {
3307 var model = collection.at( ui.item.data('sortableIndexStart') ),
3308 comparator = collection.comparator;
3310 // Temporarily disable the comparator to prevent `add`
3312 delete collection.comparator;
3314 // Silently shift the model to its new index.
3315 collection.remove( model, {
3322 // Restore the comparator.
3323 collection.comparator = comparator;
3325 // Fire the `reset` event to ensure other collections sync.
3326 collection.trigger( 'reset', collection );
3328 // If the collection is sorted by menu order,
3329 // update the menu order.
3330 collection.saveMenuOrder();
3332 }, this.options.sortable ) );
3334 // If the `orderby` property is changed on the `collection`,
3335 // check to see if we have a `comparator`. If so, disable sorting.
3336 collection.props.on( 'change:orderby', function() {
3337 this.$el.sortable( 'option', 'disabled', !! collection.comparator );
3340 this.collection.props.on( 'change:orderby', this.refreshSortable, this );
3341 this.refreshSortable();
3344 refreshSortable: function() {
3345 if ( ! this.options.sortable || ! $.fn.sortable )
3348 // If the `collection` has a `comparator`, disable sorting.
3349 var collection = this.collection,
3350 orderby = collection.props.get('orderby'),
3351 enabled = 'menuOrder' === orderby || ! collection.comparator;
3353 this.$el.sortable( 'option', 'disabled', ! enabled );
3356 createAttachmentView: function( attachment ) {
3357 var view = new this.options.AttachmentView({
3358 controller: this.controller,
3360 collection: this.collection,
3361 selection: this.options.selection
3364 return this._viewsByCid[ attachment.cid ] = view;
3367 prepare: function() {
3368 // Create all of the Attachment views, and replace
3369 // the list in a single DOM operation.
3370 if ( this.collection.length ) {
3371 this.views.set( this.collection.map( this.createAttachmentView, this ) );
3373 // If there are no elements, clear the views and load some.
3376 this.collection.more().done( this.scroll );
3381 // Trigger the scroll event to check if we're within the
3382 // threshold to query for additional attachments.
3386 scroll: function( event ) {
3387 // @todo: is this still necessary?
3388 if ( ! this.$el.is(':visible') )
3391 if ( this.collection.hasMore() && this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
3392 this.collection.more().done( this.scroll );
3396 $head: (function() {
3399 return $head = $head || $('head');
3405 * wp.media.view.Search
3407 media.view.Search = media.View.extend({
3409 className: 'search',
3413 placeholder: l10n.search
3423 render: function() {
3424 this.el.value = this.model.escape('search');
3428 search: function( event ) {
3429 if ( event.target.value )
3430 this.model.set( 'search', event.target.value );
3432 this.model.unset('search');
3437 * wp.media.view.AttachmentFilters
3439 media.view.AttachmentFilters = media.View.extend({
3441 className: 'attachment-filters',
3449 initialize: function() {
3450 this.createFilters();
3451 _.extend( this.filters, this.options.filters );
3453 // Build `<option>` elements.
3454 this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
3456 el: this.make( 'option', { value: value }, filter.text ),
3457 priority: filter.priority || 50
3459 }, this ).sortBy('priority').pluck('el').value() );
3461 this.model.on( 'change', this.select, this );
3465 createFilters: function() {
3469 change: function( event ) {
3470 var filter = this.filters[ this.el.value ];
3473 this.model.set( filter.props );
3476 select: function() {
3477 var model = this.model,
3479 props = model.toJSON();
3481 _.find( this.filters, function( filter, id ) {
3482 var equal = _.all( filter.props, function( prop, key ) {
3483 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
3490 this.$el.val( value );
3494 media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
3495 createFilters: function() {
3496 var type = this.model.get('type'),
3497 types = media.view.settings.mimeTypes,
3500 if ( types && type )
3501 text = types[ type ];
3505 text: text || l10n.allMediaItems,
3515 text: l10n.uploadedToThisPost,
3517 uploadedTo: media.view.settings.post.id,
3518 orderby: 'menuOrder',
3527 media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
3528 createFilters: function() {
3531 _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
3544 text: l10n.allMediaItems,
3554 filters.uploaded = {
3555 text: l10n.uploadedToThisPost,
3558 uploadedTo: media.view.settings.post.id,
3559 orderby: 'menuOrder',
3565 this.filters = filters;
3572 * wp.media.view.AttachmentsBrowser
3574 media.view.AttachmentsBrowser = media.View.extend({
3576 className: 'attachments-browser',
3578 initialize: function() {
3579 _.defaults( this.options, {
3584 AttachmentView: media.view.Attachment.Library
3587 this.createToolbar();
3588 this.updateContent();
3589 this.createSidebar();
3591 this.collection.on( 'add remove reset', this.updateContent, this );
3594 dispose: function() {
3595 this.options.selection.off( null, null, this );
3596 media.View.prototype.dispose.apply( this, arguments );
3600 createToolbar: function() {
3601 var filters, FiltersConstructor;
3603 this.toolbar = new media.view.Toolbar({
3604 controller: this.controller
3607 this.views.add( this.toolbar );
3609 filters = this.options.filters;
3610 if ( 'uploaded' === filters )
3611 FiltersConstructor = media.view.AttachmentFilters.Uploaded;
3612 else if ( 'all' === filters )
3613 FiltersConstructor = media.view.AttachmentFilters.All;
3615 if ( FiltersConstructor ) {
3616 this.toolbar.set( 'filters', new FiltersConstructor({
3617 controller: this.controller,
3618 model: this.collection.props,
3623 if ( this.options.search ) {
3624 this.toolbar.set( 'search', new media.view.Search({
3625 controller: this.controller,
3626 model: this.collection.props,
3631 if ( this.options.dragInfo ) {
3632 this.toolbar.set( 'dragInfo', new media.View({
3633 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
3639 updateContent: function() {
3642 if( ! this.attachments )
3643 this.createAttachments();
3645 if ( ! this.collection.length ) {
3646 this.collection.more().done( function() {
3647 if ( ! view.collection.length )
3648 view.createUploader();
3653 removeContent: function() {
3654 _.each(['attachments','uploader'], function( key ) {
3655 if ( this[ key ] ) {
3656 this[ key ].remove();
3662 createUploader: function() {
3663 this.removeContent();
3665 this.uploader = new media.view.UploaderInline({
3666 controller: this.controller,
3668 message: l10n.noItemsFound
3671 this.views.add( this.uploader );
3674 createAttachments: function() {
3675 this.removeContent();
3677 this.attachments = new media.view.Attachments({
3678 controller: this.controller,
3679 collection: this.collection,
3680 selection: this.options.selection,
3682 sortable: this.options.sortable,
3684 // The single `Attachment` view to be used in the `Attachments` view.
3685 AttachmentView: this.options.AttachmentView
3688 this.views.add( this.attachments );
3691 createSidebar: function() {
3692 var options = this.options,
3693 selection = options.selection,
3694 sidebar = this.sidebar = new media.view.Sidebar({
3695 controller: this.controller
3698 this.views.add( sidebar );
3700 if ( this.controller.uploader ) {
3701 sidebar.set( 'uploads', new media.view.UploaderStatus({
3702 controller: this.controller,
3707 selection.on( 'selection:single', this.createSingle, this );
3708 selection.on( 'selection:unsingle', this.disposeSingle, this );
3710 if ( selection.single() )
3711 this.createSingle();
3714 createSingle: function() {
3715 var sidebar = this.sidebar,
3716 single = this.options.selection.single(),
3719 sidebar.set( 'details', new media.view.Attachment.Details({
3720 controller: this.controller,
3725 sidebar.set( 'compat', new media.view.AttachmentCompat({
3726 controller: this.controller,
3731 if ( this.options.display ) {
3732 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
3733 controller: this.controller,
3734 model: this.model.display( single ),
3737 userSettings: this.model.get('displayUserSettings')
3742 disposeSingle: function() {
3743 var sidebar = this.sidebar;
3744 sidebar.unset('details');
3745 sidebar.unset('compat');
3746 sidebar.unset('display');
3751 * wp.media.view.Selection
3753 media.view.Selection = media.View.extend({
3755 className: 'media-selection',
3756 template: media.template('media-selection'),
3759 'click .edit-selection': 'edit',
3760 'click .clear-selection': 'clear'
3763 initialize: function() {
3764 _.defaults( this.options, {
3769 this.attachments = new media.view.Attachments.Selection({
3770 controller: this.controller,
3771 collection: this.collection,
3772 selection: this.collection,
3773 model: new Backbone.Model({
3779 this.views.set( '.selection-view', this.attachments );
3780 this.collection.on( 'add remove reset', this.refresh, this );
3781 this.controller.on( 'content:activate', this.refresh, this );
3788 refresh: function() {
3789 // If the selection hasn't been rendered, bail.
3790 if ( ! this.$el.children().length )
3793 var collection = this.collection,
3794 editing = 'edit-selection' === this.controller.content.mode();
3796 // If nothing is selected, display nothing.
3797 this.$el.toggleClass( 'empty', ! collection.length );
3798 this.$el.toggleClass( 'one', 1 === collection.length );
3799 this.$el.toggleClass( 'editing', editing );
3801 this.$('.count').text( l10n.selected.replace('%d', collection.length) );
3804 edit: function( event ) {
3805 event.preventDefault();
3806 if ( this.options.editable )
3807 this.options.editable.call( this, this.collection );
3810 clear: function( event ) {
3811 event.preventDefault();
3812 this.collection.reset();
3818 * wp.media.view.Attachment.Selection
3820 media.view.Attachment.Selection = media.view.Attachment.extend({
3821 className: 'attachment selection',
3823 // On click, just select the model, instead of removing the model from
3825 toggleSelection: function() {
3826 this.options.selection.single( this.model );
3831 * wp.media.view.Attachments.Selection
3833 media.view.Attachments.Selection = media.view.Attachments.extend({
3835 initialize: function() {
3836 _.defaults( this.options, {
3840 // The single `Attachment` view to be used in the `Attachments` view.
3841 AttachmentView: media.view.Attachment.Selection
3843 return media.view.Attachments.prototype.initialize.apply( this, arguments );
3848 * wp.media.view.Attachments.EditSelection
3850 media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
3858 * wp.media.view.Settings
3860 media.view.Settings = media.View.extend({
3862 'click button': 'updateHandler',
3863 'change input': 'updateHandler',
3864 'change select': 'updateHandler',
3865 'change textarea': 'updateHandler'
3868 initialize: function() {
3869 this.model = this.model || new Backbone.Model();
3870 this.model.on( 'change', this.updateChanges, this );
3873 prepare: function() {
3875 model: this.model.toJSON()
3879 render: function() {
3880 media.View.prototype.render.apply( this, arguments );
3881 // Select the correct values.
3882 _( this.model.attributes ).chain().keys().each( this.update, this );
3886 update: function( key ) {
3887 var value = this.model.get( key ),
3888 $setting = this.$('[data-setting="' + key + '"]'),
3891 // Bail if we didn't find a matching setting.
3892 if ( ! $setting.length )
3895 // Attempt to determine how the setting is rendered and update
3896 // the selected value.
3898 // Handle dropdowns.
3899 if ( $setting.is('select') ) {
3900 $value = $setting.find('[value="' + value + '"]');
3902 if ( $value.length ) {
3903 $setting.find('option').prop( 'selected', false );
3904 $value.prop( 'selected', true );
3906 // If we can't find the desired value, record what *is* selected.
3907 this.model.set( key, $setting.find(':selected').val() );
3911 // Handle button groups.
3912 } else if ( $setting.hasClass('button-group') ) {
3913 $buttons = $setting.find('button').removeClass('active');
3914 $buttons.filter( '[value="' + value + '"]' ).addClass('active');
3916 // Handle text inputs and textareas.
3917 } else if ( $setting.is('input[type="text"], textarea') ) {
3918 if ( ! $setting.is(':focus') )
3919 $setting.val( value );
3921 // Handle checkboxes.
3922 } else if ( $setting.is('input[type="checkbox"]') ) {
3923 $setting.attr( 'checked', !! value );
3927 updateHandler: function( event ) {
3928 var $setting = $( event.target ).closest('[data-setting]'),
3929 value = event.target.value,
3932 event.preventDefault();
3934 if ( ! $setting.length )
3937 // Use the correct value for checkboxes.
3938 if ( $setting.is('input[type="checkbox"]') )
3939 value = $setting[0].checked;
3941 // Update the corresponding setting.
3942 this.model.set( $setting.data('setting'), value );
3944 // If the setting has a corresponding user setting,
3945 // update that as well.
3946 if ( userSetting = $setting.data('userSetting') )
3947 setUserSetting( userSetting, value );
3950 updateChanges: function( model, options ) {
3951 if ( options.changes )
3952 _( options.changes ).chain().keys().each( this.update, this );
3957 * wp.media.view.Settings.AttachmentDisplay
3959 media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
3960 className: 'attachment-display-settings',
3961 template: media.template('attachment-display-settings'),
3963 initialize: function() {
3964 var attachment = this.options.attachment;
3966 _.defaults( this.options, {
3970 media.view.Settings.prototype.initialize.apply( this, arguments );
3971 this.model.on( 'change:link', this.updateLinkTo, this );
3974 attachment.on( 'change:uploading', this.render, this );
3977 dispose: function() {
3978 var attachment = this.options.attachment;
3980 attachment.off( null, null, this );
3982 media.view.Settings.prototype.dispose.apply( this, arguments );
3985 render: function() {
3986 var attachment = this.options.attachment;
3988 _.extend( this.options, {
3989 sizes: attachment.get('sizes'),
3990 type: attachment.get('type')
3994 media.view.Settings.prototype.render.call( this );
3995 this.updateLinkTo();
3999 updateLinkTo: function() {
4000 var linkTo = this.model.get('link'),
4001 $input = this.$('.link-to-custom'),
4002 attachment = this.options.attachment;
4004 if ( 'none' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
4010 if ( 'post' === linkTo ) {
4011 $input.val( attachment.get('link') );
4012 } else if ( 'file' === linkTo ) {
4013 $input.val( attachment.get('url') );
4014 } else if ( ! this.model.get('linkUrl') ) {
4015 $input.val('http://');
4018 $input.prop( 'readonly', 'custom' !== linkTo );
4023 // If the input is visible, focus and select its contents.
4024 if ( $input.is(':visible') )
4025 $input.focus()[0].select();
4030 * wp.media.view.Settings.Gallery
4032 media.view.Settings.Gallery = media.view.Settings.extend({
4033 className: 'gallery-settings',
4034 template: media.template('gallery-settings')
4038 * wp.media.view.Attachment.Details
4040 media.view.Attachment.Details = media.view.Attachment.extend({
4042 className: 'attachment-details',
4043 template: media.template('attachment-details'),
4046 'change [data-setting]': 'updateSetting',
4047 'change [data-setting] input': 'updateSetting',
4048 'change [data-setting] select': 'updateSetting',
4049 'change [data-setting] textarea': 'updateSetting',
4050 'click .delete-attachment': 'deleteAttachment',
4051 'click .edit-attachment': 'editAttachment',
4052 'click .refresh-attachment': 'refreshAttachment'
4055 initialize: function() {
4056 this.focusManager = new media.view.FocusManager({
4060 media.view.Attachment.prototype.initialize.apply( this, arguments );
4063 render: function() {
4064 media.view.Attachment.prototype.render.apply( this, arguments );
4065 this.focusManager.focus();
4069 deleteAttachment: function( event ) {
4070 event.preventDefault();
4072 if ( confirm( l10n.warnDelete ) )
4073 this.model.destroy();
4076 editAttachment: function( event ) {
4077 this.$el.addClass('needs-refresh');
4080 refreshAttachment: function( event ) {
4081 this.$el.removeClass('needs-refresh');
4082 event.preventDefault();
4088 * wp.media.view.AttachmentCompat
4090 media.view.AttachmentCompat = media.View.extend({
4092 className: 'compat-item',
4095 'submit': 'preventDefault',
4096 'change input': 'save',
4097 'change select': 'save',
4098 'change textarea': 'save'
4101 initialize: function() {
4102 this.focusManager = new media.view.FocusManager({
4106 this.model.on( 'change:compat', this.render, this );
4109 dispose: function() {
4110 if ( this.$(':focus').length )
4113 return media.View.prototype.dispose.apply( this, arguments );
4116 render: function() {
4117 var compat = this.model.get('compat');
4118 if ( ! compat || ! compat.item )
4121 this.views.detach();
4122 this.$el.html( compat.item );
4123 this.views.render();
4125 this.focusManager.focus();
4129 preventDefault: function( event ) {
4130 event.preventDefault();
4133 save: function( event ) {
4137 event.preventDefault();
4139 _.each( this.$el.serializeArray(), function( pair ) {
4140 data[ pair.name ] = pair.value;
4143 this.model.saveCompat( data );
4148 * wp.media.view.Iframe
4150 media.view.Iframe = media.View.extend({
4151 className: 'media-iframe',
4153 render: function() {
4154 this.views.detach();
4155 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
4156 this.views.render();
4162 * wp.media.view.Embed
4164 media.view.Embed = media.View.extend({
4165 className: 'media-embed',
4167 initialize: function() {
4168 this.url = new media.view.EmbedUrl({
4169 controller: this.controller,
4170 model: this.model.props
4173 this.views.set([ this.url ]);
4175 this.model.on( 'change:type', this.refresh, this );
4176 this.model.on( 'change:loading', this.loading, this );
4179 settings: function( view ) {
4180 if ( this._settings )
4181 this._settings.remove();
4182 this._settings = view;
4183 this.views.add( view );
4186 refresh: function() {
4187 var type = this.model.get('type'),
4190 if ( 'image' === type )
4191 constructor = media.view.EmbedImage;
4192 else if ( 'link' === type )
4193 constructor = media.view.EmbedLink;
4197 this.settings( new constructor({
4198 controller: this.controller,
4199 model: this.model.props,
4204 loading: function() {
4205 this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
4210 * wp.media.view.EmbedUrl
4212 media.view.EmbedUrl = media.View.extend({
4214 className: 'embed-url',
4222 initialize: function() {
4223 this.input = this.make( 'input', {
4225 value: this.model.get('url') || ''
4228 this.spinner = this.make( 'span', {
4232 this.$input = $( this.input );
4233 this.$el.append([ this.input, this.spinner ]);
4235 this.model.on( 'change:url', this.render, this );
4238 render: function() {
4239 var $input = this.$input;
4241 if ( $input.is(':focus') )
4244 this.input.value = this.model.get('url') || 'http://';
4245 media.View.prototype.render.apply( this, arguments );
4253 url: function( event ) {
4254 this.model.set( 'url', event.target.value );
4258 var $input = this.$input;
4259 // If the input is visible, focus and select its contents.
4260 if ( $input.is(':visible') )
4261 $input.focus()[0].select();
4266 * wp.media.view.EmbedLink
4268 media.view.EmbedLink = media.view.Settings.extend({
4269 className: 'embed-link-settings',
4270 template: media.template('embed-link-settings')
4274 * wp.media.view.EmbedImage
4276 media.view.EmbedImage = media.view.Settings.AttachmentDisplay.extend({
4277 className: 'embed-image-settings',
4278 template: media.template('embed-image-settings'),
4280 initialize: function() {
4281 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
4282 this.model.on( 'change:url', this.updateImage, this );
4285 updateImage: function() {
4286 this.$('img').attr( 'src', this.model.get('url') );