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 if ( this.get('contentUserSetting') ) {
417 this.frame.on( 'content:activate', this.saveContentMode, this );
418 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
422 deactivate: function() {
423 this.recordSelection();
425 this.frame.off( 'content:activate', this.saveContentMode, this );
427 // Unbind all event handlers that use this state as the context
428 // from the selection.
429 this.get('selection').off( null, null, this );
431 wp.Uploader.queue.off( null, null, this );
435 this.get('selection').reset();
436 this.resetDisplays();
437 this.refreshContent();
440 resetDisplays: function() {
441 var defaultProps = media.view.settings.defaultProps;
443 this._defaultDisplaySettings = {
444 align: defaultProps.align || getUserSetting( 'align', 'none' ),
445 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ),
446 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' )
450 display: function( attachment ) {
451 var displays = this._displays;
453 if ( ! displays[ attachment.cid ] )
454 displays[ attachment.cid ] = new Backbone.Model( this._defaultDisplaySettings );
456 return displays[ attachment.cid ];
459 syncSelection: function() {
460 var selection = this.get('selection'),
461 manager = this.frame._selection;
463 if ( ! this.get('syncSelection') || ! manager || ! selection )
466 // If the selection supports multiple items, validate the stored
467 // attachments based on the new selection's conditions. Record
468 // the attachments that are not included; we'll maintain a
469 // reference to those. Other attachments are considered in flux.
470 if ( selection.multiple ) {
471 selection.reset( [], { silent: true });
472 selection.validateAll( manager.attachments );
473 manager.difference = _.difference( manager.attachments.models, selection.models );
476 // Sync the selection's single item with the master.
477 selection.single( manager.single );
480 recordSelection: function() {
481 var selection = this.get('selection'),
482 manager = this.frame._selection,
485 if ( ! this.get('syncSelection') || ! manager || ! selection )
488 // Record the currently active attachments, which is a combination
489 // of the selection's attachments and the set of selected
490 // attachments that this specific selection considered invalid.
491 // Reset the difference and record the single attachment.
492 if ( selection.multiple ) {
493 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
494 manager.difference = [];
496 manager.attachments.add( selection.toArray() );
499 manager.single = selection._single;
502 refreshContent: function() {
503 var selection = this.get('selection'),
505 router = frame.router.get(),
506 mode = frame.content.mode();
508 // If the state is active, no items are selected, and the current
509 // content mode is not an option in the state's router (provided
510 // the state has a router), reset the content mode to the default.
511 if ( this.active && ! selection.length && router && ! router.get( mode ) )
512 this.frame.content.render( this.get('content') );
515 uploading: function( attachment ) {
516 var content = this.frame.content;
518 // If the uploader was selected, navigate to the browser.
519 if ( 'upload' === content.mode() )
520 this.frame.content.mode('browse');
522 // Automatically select any uploading attachments.
524 // Selections that don't support multiple attachments automatically
525 // limit themselves to one attachment (in this case, the last
526 // attachment in the upload queue).
527 this.get('selection').add( attachment );
530 saveContentMode: function() {
531 // Only track the browse router on library states.
532 if ( 'browse' !== this.get('router') )
535 var mode = this.frame.content.mode(),
536 view = this.frame.router.get();
538 if ( view && view.get( mode ) )
539 setUserSetting( 'libraryContent', mode );
543 // wp.media.controller.GalleryEdit
544 // -------------------------------
545 media.controller.GalleryEdit = media.controller.Library.extend({
554 toolbar: 'gallery-edit',
556 title: l10n.editGalleryTitle,
560 // Don't sync the selection, as the Edit Gallery library
561 // *is* the selection.
565 initialize: function() {
566 // If we haven't been provided a `library`, create a `Selection`.
567 if ( ! this.get('library') )
568 this.set( 'library', new media.model.Selection() );
570 // The single `Attachment` view to be used in the `Attachments` view.
571 if ( ! this.get('AttachmentView') )
572 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
573 media.controller.Library.prototype.initialize.apply( this, arguments );
576 activate: function() {
577 var library = this.get('library');
579 // Limit the library to images only.
580 library.props.set( 'type', 'image' );
582 // Watch for uploaded attachments.
583 this.get('library').observe( wp.Uploader.queue );
585 this.frame.on( 'content:render:browse', this.gallerySettings, this );
587 media.controller.Library.prototype.activate.apply( this, arguments );
590 deactivate: function() {
591 // Stop watching for uploaded attachments.
592 this.get('library').unobserve( wp.Uploader.queue );
594 this.frame.off( 'content:render:browse', this.gallerySettings, this );
596 media.controller.Library.prototype.deactivate.apply( this, arguments );
599 gallerySettings: function( browser ) {
600 var library = this.get('library');
602 if ( ! library || ! browser )
605 library.gallery = library.gallery || new Backbone.Model();
607 browser.sidebar.set({
608 gallery: new media.view.Settings.Gallery({
610 model: library.gallery,
615 browser.toolbar.set( 'reverse', {
616 text: l10n.reverseOrder,
620 library.reset( library.toArray().reverse() );
626 // wp.media.controller.GalleryAdd
627 // ---------------------------------
628 media.controller.GalleryAdd = media.controller.Library.extend({
629 defaults: _.defaults({
630 id: 'gallery-library',
631 filterable: 'uploaded',
634 toolbar: 'gallery-add',
635 title: l10n.addToGalleryTitle,
638 // Don't sync the selection, as the Edit Gallery library
639 // *is* the selection.
641 }, media.controller.Library.prototype.defaults ),
643 initialize: function() {
644 // If we haven't been provided a `library`, create a `Selection`.
645 if ( ! this.get('library') )
646 this.set( 'library', media.query({ type: 'image' }) );
648 media.controller.Library.prototype.initialize.apply( this, arguments );
651 activate: function() {
652 var library = this.get('library'),
653 edit = this.frame.state('gallery-edit').get('library');
655 if ( this.editLibrary && this.editLibrary !== edit )
656 library.unobserve( this.editLibrary );
658 // Accepts attachments that exist in the original library and
659 // that do not exist in gallery's library.
660 library.validator = function( attachment ) {
661 return !! this.mirroring.getByCid( attachment.cid ) && ! edit.getByCid( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
664 // Reset the library to ensure that all attachments are re-added
665 // to the collection. Do so silently, as calling `observe` will
666 // trigger the `reset` event.
667 library.reset( library.mirroring.models, { silent: true });
668 library.observe( edit );
669 this.editLibrary = edit;
671 media.controller.Library.prototype.activate.apply( this, arguments );
675 // wp.media.controller.FeaturedImage
676 // ---------------------------------
677 media.controller.FeaturedImage = media.controller.Library.extend({
678 defaults: _.defaults({
679 id: 'featured-image',
680 filterable: 'uploaded',
682 toolbar: 'featured-image',
683 title: l10n.setFeaturedImageTitle,
687 }, media.controller.Library.prototype.defaults ),
689 initialize: function() {
690 var library, comparator;
692 // If we haven't been provided a `library`, create a `Selection`.
693 if ( ! this.get('library') )
694 this.set( 'library', media.query({ type: 'image' }) );
696 media.controller.Library.prototype.initialize.apply( this, arguments );
698 library = this.get('library');
699 comparator = library.comparator;
701 // Overload the library's comparator to push items that are not in
702 // the mirrored query to the front of the aggregate collection.
703 library.comparator = function( a, b ) {
704 var aInQuery = !! this.mirroring.getByCid( a.cid ),
705 bInQuery = !! this.mirroring.getByCid( b.cid );
707 if ( ! aInQuery && bInQuery )
709 else if ( aInQuery && ! bInQuery )
712 return comparator.apply( this, arguments );
715 // Add all items in the selection to the library, so any featured
716 // images that are not initially loaded still appear.
717 library.observe( this.get('selection') );
720 activate: function() {
721 this.updateSelection();
722 this.frame.on( 'open', this.updateSelection, this );
723 media.controller.Library.prototype.activate.apply( this, arguments );
726 deactivate: function() {
727 this.frame.off( 'open', this.updateSelection, this );
728 media.controller.Library.prototype.deactivate.apply( this, arguments );
731 updateSelection: function() {
732 var selection = this.get('selection'),
733 id = media.view.settings.post.featuredImageId,
736 if ( '' !== id && -1 !== id ) {
737 attachment = Attachment.get( id );
741 selection.reset( attachment ? [ attachment ] : [] );
746 // wp.media.controller.Embed
747 // -------------------------
748 media.controller.Embed = media.controller.State.extend({
754 toolbar: 'main-embed',
757 title: l10n.insertFromUrlTitle,
761 // The amount of time used when debouncing the scan.
764 initialize: function() {
765 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
766 this.props = new Backbone.Model({ url: '' });
767 this.props.on( 'change:url', this.debouncedScan, this );
768 this.props.on( 'change:url', this.refresh, this );
769 this.on( 'scan', this.scanImage, this );
780 // Scan is triggered with the list of `attributes` to set on the
781 // state, useful for the 'type' attribute and 'scanners' attribute,
782 // an array of promise objects for asynchronous scan operations.
783 if ( this.props.get('url') )
784 this.trigger( 'scan', attributes );
786 if ( attributes.scanners.length ) {
787 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
788 scanners.always( function() {
789 if ( embed.get('scanners') === scanners )
790 embed.set( 'loading', false );
793 attributes.scanners = null;
796 attributes.loading = !! attributes.scanners;
797 this.set( attributes );
800 scanImage: function( attributes ) {
801 var frame = this.frame,
803 url = this.props.get('url'),
805 deferred = $.Deferred();
807 attributes.scanners.push( deferred.promise() );
809 // Try to load the image and find its width/height.
810 image.onload = function() {
813 if ( state !== frame.state() || url !== state.props.get('url') )
826 image.onerror = deferred.reject;
830 refresh: function() {
831 this.frame.toolbar.get().refresh();
835 this.props.clear().set({ url: '' });
843 * ========================================================================
845 * ========================================================================
851 // A subview manager.
853 media.Views = function( view, views ) {
855 this._views = _.isArray( views ) ? { '': views } : views || {};
858 media.Views.extend = Backbone.Model.extend;
860 _.extend( media.Views.prototype, {
861 // ### Fetch all of the subviews
863 // Returns an array of all subviews.
865 return _.flatten( this._views );
868 // ### Get a selector's subviews
870 // Fetches all subviews that match a given `selector`.
872 // If no `selector` is provided, it will grab all subviews attached
873 // to the view's root.
874 get: function( selector ) {
875 selector = selector || '';
876 return this._views[ selector ];
879 // ### Get a selector's first subview
881 // Fetches the first subview that matches a given `selector`.
883 // If no `selector` is provided, it will grab the first subview
884 // attached to the view's root.
886 // Useful when a selector only has one subview at a time.
887 first: function( selector ) {
888 var views = this.get( selector );
889 return views && views.length ? views[0] : null;
892 // ### Register subview(s)
894 // Registers any number of `views` to a `selector`.
896 // When no `selector` is provided, the root selector (the empty string)
897 // is used. `views` accepts a `Backbone.View` instance or an array of
898 // `Backbone.View` instances.
902 // Accepts an `options` object, which has a significant effect on the
903 // resulting behavior.
905 // `options.silent` – *boolean, `false`*
906 // > If `options.silent` is true, no DOM modifications will be made.
908 // `options.add` – *boolean, `false`*
909 // > Use `Views.add()` as a shortcut for setting `options.add` to true.
911 // > By default, the provided `views` will replace
912 // any existing views associated with the selector. If `options.add`
913 // is true, the provided `views` will be added to the existing views.
915 // `options.at` – *integer, `undefined`*
916 // > When adding, to insert `views` at a specific index, use
917 // `options.at`. By default, `views` are added to the end of the array.
918 set: function( selector, views, options ) {
921 if ( ! _.isString( selector ) ) {
927 options = options || {};
928 views = _.isArray( views ) ? views : [ views ];
929 existing = this.get( selector );
934 if ( _.isUndefined( options.at ) ) {
935 next = existing.concat( views );
938 next.splice.apply( next, [ options.at, 0 ].concat( views ) );
941 _.each( next, function( view ) {
942 view.__detach = true;
945 _.each( existing, function( view ) {
952 _.each( next, function( view ) {
953 delete view.__detach;
958 this._views[ selector ] = next;
960 _.each( views, function( subview ) {
961 var constructor = subview.Views || media.Views,
962 subviews = subview.views = subview.views || new constructor( subview );
963 subviews.parent = this.view;
964 subviews.selector = selector;
967 if ( ! options.silent )
968 this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
973 // ### Add subview(s) to existing subviews
975 // An alias to `Views.set()`, which defaults `options.add` to true.
977 // Adds any number of `views` to a `selector`.
979 // When no `selector` is provided, the root selector (the empty string)
980 // is used. `views` accepts a `Backbone.View` instance or an array of
981 // `Backbone.View` instances.
983 // Use `Views.set()` when setting `options.add` to `false`.
985 // Accepts an `options` object. By default, provided `views` will be
986 // inserted at the end of the array of existing views. To insert
987 // `views` at a specific index, use `options.at`. If `options.silent`
988 // is true, no DOM modifications will be made.
990 // For more information on the `options` object, see `Views.set()`.
991 add: function( selector, views, options ) {
992 if ( ! _.isString( selector ) ) {
998 return this.set( selector, views, _.extend({ add: true }, options ) );
1001 // ### Stop tracking subviews
1003 // Stops tracking `views` registered to a `selector`. If no `views` are
1004 // set, then all of the `selector`'s subviews will be unregistered and
1007 // Accepts an `options` object. If `options.silent` is set, `dispose`
1008 // will *not* be triggered on the unregistered views.
1009 unset: function( selector, views, options ) {
1012 if ( ! _.isString( selector ) ) {
1018 views = views || [];
1020 if ( existing = this.get( selector ) ) {
1021 views = _.isArray( views ) ? views : [ views ];
1022 this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
1025 if ( ! options || ! options.silent )
1026 _.invoke( views, 'dispose' );
1031 // ### Detach all subviews
1033 // Detaches all subviews from the DOM.
1035 // Helps to preserve all subview events when re-rendering the master
1036 // view. Used in conjunction with `Views.render()`.
1037 detach: function() {
1038 $( _.pluck( this.all(), 'el' ) ).detach();
1042 // ### Render all subviews
1044 // Renders all subviews. Used in conjunction with `Views.detach()`.
1045 render: function() {
1047 ready: this._isReady()
1050 _.each( this._views, function( views, selector ) {
1051 this._attach( selector, views, options );
1054 this.rendered = true;
1058 // ### Dispose all subviews
1060 // Triggers the `dispose()` method on all subviews. Detaches the master
1061 // view from its parent. Resets the internals of the views manager.
1063 // Accepts an `options` object. If `options.silent` is set, `unset`
1064 // will *not* be triggered on the master view's parent.
1065 dispose: function( options ) {
1066 if ( ! options || ! options.silent ) {
1067 if ( this.parent && this.parent.views )
1068 this.parent.views.unset( this.selector, this.view, { silent: true });
1070 delete this.selector;
1073 _.invoke( this.all(), 'dispose' );
1078 // ### Replace a selector's subviews
1080 // By default, sets the `$target` selector's html to the subview `els`.
1082 // Can be overridden in subclasses.
1083 replace: function( $target, els ) {
1084 $target.html( els );
1088 // ### Insert subviews into a selector
1090 // By default, appends the subview `els` to the end of the `$target`
1091 // selector. If `options.at` is set, inserts the subview `els` at the
1094 // Can be overridden in subclasses.
1095 insert: function( $target, els, options ) {
1096 var at = options && options.at,
1099 if ( _.isNumber( at ) && ($children = $target.children()).length > at )
1100 $children.eq( at ).before( els );
1102 $target.append( els );
1107 // ### Trigger the ready event
1109 // **Only use this method if you know what you're doing.**
1110 // For performance reasons, this method does not check if the view is
1111 // actually attached to the DOM. It's taking your word for it.
1113 // Fires the ready event on the current view and all attached subviews.
1115 this.view.trigger('ready');
1117 // Find all attached subviews, and call ready on them.
1118 _.chain( this.all() ).map( function( view ) {
1120 }).flatten().where({ attached: true }).invoke('ready');
1123 // #### Internal. Attaches a series of views to a selector.
1125 // Checks to see if a matching selector exists, renders the views,
1126 // performs the proper DOM operation, and then checks if the view is
1127 // attached to the document.
1128 _attach: function( selector, views, options ) {
1129 var $selector = selector ? this.view.$( selector ) : this.view.$el,
1132 // Check if we found a location to attach the views.
1133 if ( ! $selector.length )
1136 managers = _.chain( views ).pluck('views').flatten().value();
1138 // Render the views if necessary.
1139 _.each( managers, function( manager ) {
1140 if ( manager.rendered )
1143 manager.view.render();
1144 manager.rendered = true;
1147 // Insert or replace the views.
1148 this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
1150 // Set attached and trigger ready if the current view is already
1151 // attached to the DOM.
1152 _.each( managers, function( manager ) {
1153 manager.attached = true;
1155 if ( options.ready )
1162 // #### Internal. Checks if the current view is in the DOM.
1163 _isReady: function() {
1164 var node = this.view.el;
1166 if ( node === document.body )
1168 node = node.parentNode;
1178 // The base view class.
1179 media.View = Backbone.View.extend({
1180 // The constructor for the `Views` manager.
1183 constructor: function( options ) {
1184 this.views = new this.Views( this, this.views );
1185 this.on( 'ready', this.ready, this );
1187 if ( options && options.controller )
1188 this.controller = options.controller;
1190 Backbone.View.apply( this, arguments );
1193 dispose: function() {
1194 // Undelegating events, removing events from the model, and
1195 // removing events from the controller mirror the code for
1196 // `Backbone.View.dispose` in Backbone master.
1197 this.undelegateEvents();
1199 if ( this.model && this.model.off )
1200 this.model.off( null, null, this );
1202 if ( this.collection && this.collection.off )
1203 this.collection.off( null, null, this );
1205 // Unbind controller events.
1206 if ( this.controller && this.controller.off )
1207 this.controller.off( null, null, this );
1209 // Recursively dispose child views.
1211 this.views.dispose();
1216 remove: function() {
1218 return Backbone.View.prototype.remove.apply( this, arguments );
1221 render: function() {
1225 options = this.prepare();
1227 this.views.detach();
1229 if ( this.template ) {
1230 options = options || {};
1231 this.trigger( 'prepare', options );
1232 this.$el.html( this.template( options ) );
1235 this.views.render();
1239 prepare: function() {
1240 return this.options;
1243 ready: function() {}
1247 * wp.media.view.Frame
1249 media.view.Frame = media.View.extend({
1250 initialize: function() {
1251 this._createRegions();
1252 this._createStates();
1255 _createRegions: function() {
1256 // Clone the regions array.
1257 this.regions = this.regions ? this.regions.slice() : [];
1259 // Initialize regions.
1260 _.each( this.regions, function( region ) {
1261 this[ region ] = new media.controller.Region({
1264 selector: '.media-frame-' + region
1269 _createStates: function() {
1270 // Create the default `states` collection.
1271 this.states = new Backbone.Collection( null, {
1272 model: media.controller.State
1275 // Ensure states have a reference to the frame.
1276 this.states.on( 'add', function( model ) {
1278 model.trigger('ready');
1281 if ( this.options.states )
1282 this.states.add( this.options.states );
1286 this.states.invoke( 'trigger', 'reset' );
1291 // Make the `Frame` a `StateMachine`.
1292 _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1295 * wp.media.view.MediaFrame
1297 media.view.MediaFrame = media.view.Frame.extend({
1298 className: 'media-frame',
1299 template: media.template('media-frame'),
1300 regions: ['menu','title','content','toolbar','router'],
1302 initialize: function() {
1303 media.view.Frame.prototype.initialize.apply( this, arguments );
1305 _.defaults( this.options, {
1311 // Ensure core UI is enabled.
1312 this.$el.addClass('wp-core-ui');
1314 // Initialize modal container view.
1315 if ( this.options.modal ) {
1316 this.modal = new media.view.Modal({
1318 title: this.options.title
1321 this.modal.content( this );
1324 // Force the uploader off if the upload limit has been exceeded or
1325 // if the browser isn't supported.
1326 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported )
1327 this.options.uploader = false;
1329 // Initialize window-wide uploader.
1330 if ( this.options.uploader ) {
1331 this.uploader = new media.view.UploaderWindow({
1334 dropzone: this.modal ? this.modal.$el : this.$el,
1338 this.views.set( '.media-frame-uploader', this.uploader );
1341 this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1343 // Bind default title creation.
1344 this.on( 'title:create:default', this.createTitle, this );
1345 this.title.mode('default');
1347 // Bind default menu.
1348 this.on( 'menu:create:default', this.createMenu, this );
1351 render: function() {
1352 // Activate the default state if no active state exists.
1353 if ( ! this.state() && this.options.state )
1354 this.setState( this.options.state );
1356 return media.view.Frame.prototype.render.apply( this, arguments );
1359 createTitle: function( title ) {
1360 title.view = new media.View({
1366 createMenu: function( menu ) {
1367 menu.view = new media.view.Menu({
1372 createToolbar: function( toolbar ) {
1373 toolbar.view = new media.view.Toolbar({
1378 createRouter: function( router ) {
1379 router.view = new media.view.Router({
1384 createIframeStates: function( options ) {
1385 var settings = media.view.settings,
1386 tabs = settings.tabs,
1387 tabUrl = settings.tabUrl,
1390 if ( ! tabs || ! tabUrl )
1393 // Add the post ID to the tab URL if it exists.
1394 $postId = $('#post_ID');
1395 if ( $postId.length )
1396 tabUrl += '&post_id=' + $postId.val();
1398 // Generate the tab states.
1399 _.each( tabs, function( title, id ) {
1400 var frame = this.state( 'iframe:' + id ).set( _.defaults({
1402 src: tabUrl + '&tab=' + id,
1409 this.on( 'content:create:iframe', this.iframeContent, this );
1410 this.on( 'menu:render:default', this.iframeMenu, this );
1411 this.on( 'open', this.hijackThickbox, this );
1412 this.on( 'close', this.restoreThickbox, this );
1415 iframeContent: function( content ) {
1416 this.$el.addClass('hide-toolbar');
1417 content.view = new media.view.Iframe({
1422 iframeMenu: function( view ) {
1428 _.each( media.view.settings.tabs, function( title, id ) {
1429 views[ 'iframe:' + id ] = {
1430 text: this.state( 'iframe:' + id ).get('title'),
1438 hijackThickbox: function() {
1441 if ( ! window.tb_remove || this._tb_remove )
1444 this._tb_remove = window.tb_remove;
1445 window.tb_remove = function() {
1448 frame.setState( frame.options.state );
1449 frame._tb_remove.call( window );
1453 restoreThickbox: function() {
1454 if ( ! this._tb_remove )
1457 window.tb_remove = this._tb_remove;
1458 delete this._tb_remove;
1462 // Map some of the modal's methods to the frame.
1463 _.each(['open','close','attach','detach','escape'], function( method ) {
1464 media.view.MediaFrame.prototype[ method ] = function( view ) {
1466 this.modal[ method ].apply( this.modal, arguments );
1472 * wp.media.view.MediaFrame.Select
1474 media.view.MediaFrame.Select = media.view.MediaFrame.extend({
1475 initialize: function() {
1476 media.view.MediaFrame.prototype.initialize.apply( this, arguments );
1478 _.defaults( this.options, {
1485 this.createSelection();
1486 this.createStates();
1487 this.bindHandlers();
1490 createSelection: function() {
1491 var controller = this,
1492 selection = this.options.selection;
1494 if ( ! (selection instanceof media.model.Selection) ) {
1495 this.options.selection = new media.model.Selection( selection, {
1496 multiple: this.options.multiple
1501 attachments: new Attachments(),
1506 createStates: function() {
1507 var options = this.options;
1509 if ( this.options.states )
1512 // Add the default states.
1515 new media.controller.Library({
1516 library: media.query( options.library ),
1517 multiple: options.multiple,
1518 title: options.title,
1524 bindHandlers: function() {
1525 this.on( 'router:create:browse', this.createRouter, this );
1526 this.on( 'router:render:browse', this.browseRouter, this );
1527 this.on( 'content:create:browse', this.browseContent, this );
1528 this.on( 'content:render:upload', this.uploadContent, this );
1529 this.on( 'toolbar:create:select', this.createSelectToolbar, this );
1533 browseRouter: function( view ) {
1536 text: l10n.uploadFilesTitle,
1540 text: l10n.mediaLibraryTitle,
1547 browseContent: function( content ) {
1548 var state = this.state();
1550 this.$el.removeClass('hide-toolbar');
1552 // Browse our library of attachments.
1553 content.view = new media.view.AttachmentsBrowser({
1555 collection: state.get('library'),
1556 selection: state.get('selection'),
1558 sortable: state.get('sortable'),
1559 search: state.get('searchable'),
1560 filters: state.get('filterable'),
1561 display: state.get('displaySettings'),
1562 dragInfo: state.get('dragInfo'),
1564 AttachmentView: state.get('AttachmentView')
1568 uploadContent: function() {
1569 this.$el.removeClass('hide-toolbar');
1570 this.content.set( new media.view.UploaderInline({
1576 createSelectToolbar: function( toolbar, options ) {
1577 options = options || this.options.button || {};
1578 options.controller = this;
1580 toolbar.view = new media.view.Toolbar.Select( options );
1585 * wp.media.view.MediaFrame.Post
1587 media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
1588 initialize: function() {
1589 _.defaults( this.options, {
1595 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
1596 this.createIframeStates();
1599 createStates: function() {
1600 var options = this.options;
1602 // Add the default states.
1605 new media.controller.Library({
1607 title: l10n.insertMediaTitle,
1609 toolbar: 'main-insert',
1611 library: media.query( options.library ),
1612 multiple: options.multiple ? 'reset' : false,
1615 // If the user isn't allowed to edit fields,
1616 // can they still edit it locally?
1617 allowLocalEdits: true,
1619 // Show the attachment display settings.
1620 displaySettings: true,
1621 // Update user settings when users adjust the
1622 // attachment display settings.
1623 displayUserSettings: true
1626 new media.controller.Library({
1628 title: l10n.createGalleryTitle,
1630 toolbar: 'main-gallery',
1631 filterable: 'uploaded',
1635 library: media.query( _.defaults({
1637 }, options.library ) )
1641 new media.controller.Embed(),
1644 new media.controller.GalleryEdit({
1645 library: options.selection,
1646 editing: options.editing,
1650 new media.controller.GalleryAdd()
1654 if ( media.view.settings.post.featuredImageId ) {
1655 this.states.add( new media.controller.FeaturedImage() );
1659 bindHandlers: function() {
1660 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
1661 this.on( 'menu:create:gallery', this.createMenu, this );
1662 this.on( 'toolbar:create:main-insert', this.createToolbar, this );
1663 this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
1664 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
1665 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
1669 'default': 'mainMenu',
1670 'gallery': 'galleryMenu'
1674 'embed': 'embedContent',
1675 'edit-selection': 'editSelectionContent'
1679 'main-insert': 'mainInsertToolbar',
1680 'main-gallery': 'mainGalleryToolbar',
1681 'gallery-edit': 'galleryEditToolbar',
1682 'gallery-add': 'galleryAddToolbar'
1686 _.each( handlers, function( regionHandlers, region ) {
1687 _.each( regionHandlers, function( callback, handler ) {
1688 this.on( region + ':render:' + handler, this[ callback ], this );
1694 mainMenu: function( view ) {
1696 'library-separator': new media.View({
1697 className: 'separator',
1703 galleryMenu: function( view ) {
1704 var lastState = this.lastState(),
1705 previous = lastState && lastState.id,
1710 text: l10n.cancelGalleryTitle,
1714 frame.setState( previous );
1719 separateCancel: new media.View({
1720 className: 'separator',
1727 embedContent: function() {
1728 var view = new media.view.Embed({
1733 this.content.set( view );
1737 editSelectionContent: function() {
1738 var state = this.state(),
1739 selection = state.get('selection'),
1742 view = new media.view.AttachmentsBrowser({
1744 collection: selection,
1745 selection: selection,
1751 AttachmentView: media.view.Attachment.EditSelection
1754 view.toolbar.set( 'backToLibrary', {
1755 text: l10n.returnToLibrary,
1759 this.controller.content.mode('browse');
1763 // Browse our library of attachments.
1764 this.content.set( view );
1768 selectionStatusToolbar: function( view ) {
1769 var editable = this.state().get('editable');
1771 view.set( 'selection', new media.view.Selection({
1773 collection: this.state().get('selection'),
1776 // If the selection is editable, pass the callback to
1777 // switch the content mode.
1778 editable: editable && function() {
1779 this.controller.content.mode('edit-selection');
1784 mainInsertToolbar: function( view ) {
1785 var controller = this;
1787 this.selectionStatusToolbar( view );
1789 view.set( 'insert', {
1792 text: l10n.insertIntoPost,
1793 requires: { selection: true },
1796 var state = controller.state(),
1797 selection = state.get('selection');
1800 state.trigger( 'insert', selection ).reset();
1805 mainGalleryToolbar: function( view ) {
1806 var controller = this;
1808 this.selectionStatusToolbar( view );
1810 view.set( 'gallery', {
1812 text: l10n.createNewGallery,
1814 requires: { selection: true },
1817 var selection = controller.state().get('selection'),
1818 edit = controller.state('gallery-edit'),
1819 models = selection.where({ type: 'image' });
1821 edit.set( 'library', new media.model.Selection( models, {
1822 props: selection.props.toJSON(),
1826 this.controller.setState('gallery-edit');
1831 featuredImageToolbar: function( toolbar ) {
1832 this.createSelectToolbar( toolbar, {
1833 text: l10n.setFeaturedImage,
1834 state: this.options.state || 'upload'
1838 mainEmbedToolbar: function( toolbar ) {
1839 toolbar.view = new media.view.Toolbar.Embed({
1844 galleryEditToolbar: function() {
1845 var editing = this.state().get('editing');
1846 this.toolbar.set( new media.view.Toolbar({
1851 text: editing ? l10n.updateGallery : l10n.insertGallery,
1853 requires: { library: true },
1856 var controller = this.controller,
1857 state = controller.state();
1860 state.trigger( 'update', state.get('library') );
1863 // @todo: Make the state activated dynamic (instead of hardcoded).
1864 controller.setState('upload');
1871 galleryAddToolbar: function() {
1872 this.toolbar.set( new media.view.Toolbar({
1877 text: l10n.addToGallery,
1879 requires: { selection: true },
1882 var controller = this.controller,
1883 state = controller.state(),
1884 edit = controller.state('gallery-edit');
1886 edit.get('library').add( state.get('selection').models );
1887 state.trigger('reset');
1888 controller.setState('gallery-edit');
1897 * wp.media.view.Modal
1899 media.view.Modal = media.View.extend({
1901 template: media.template('media-modal'),
1908 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
1909 'keydown': 'keydown'
1912 initialize: function() {
1913 _.defaults( this.options, {
1914 container: document.body,
1921 prepare: function() {
1923 title: this.options.title
1927 attach: function() {
1928 if ( this.views.attached )
1931 if ( ! this.views.rendered )
1934 this.$el.appendTo( this.options.container );
1936 // Manually mark the view as attached and trigger ready.
1937 this.views.attached = true;
1940 return this.propagate('attach');
1943 detach: function() {
1944 if ( this.$el.is(':visible') )
1948 this.views.attached = false;
1949 return this.propagate('detach');
1954 options = this.options;
1956 if ( $el.is(':visible') )
1959 if ( ! this.views.attached )
1962 // If the `freeze` option is set, record the window's scroll position.
1963 if ( options.freeze ) {
1965 scrollTop: $( window ).scrollTop()
1970 return this.propagate('open');
1973 close: function( options ) {
1974 var freeze = this._freeze;
1976 if ( ! this.views.attached || ! this.$el.is(':visible') )
1980 this.propagate('close');
1982 // If the `freeze` option is set, restore the container's scroll position.
1984 $( window ).scrollTop( freeze.scrollTop );
1987 if ( options && options.escape )
1988 this.propagate('escape');
1993 escape: function() {
1994 return this.close({ escape: true });
1997 escapeHandler: function( event ) {
1998 event.preventDefault();
2002 content: function( content ) {
2003 this.views.set( '.media-modal-content', content );
2007 // Triggers a modal event and if the `propagate` option is set,
2008 // forwards events to the modal's controller.
2009 propagate: function( id ) {
2012 if ( this.options.propagate )
2013 this.controller.trigger( id );
2018 keydown: function( event ) {
2019 // Close the modal when escape is pressed.
2020 if ( 27 === event.which ) {
2021 event.preventDefault();
2028 // wp.media.view.FocusManager
2029 // ----------------------------
2030 media.view.FocusManager = media.View.extend({
2032 keydown: 'recordTab',
2033 focusin: 'updateIndex'
2037 if ( _.isUndefined( this.index ) )
2040 // Update our collection of `$tabbables`.
2041 this.$tabbables = this.$(':tabbable');
2043 // If tab is saved, focus it.
2044 this.$tabbables.eq( this.index ).focus();
2047 recordTab: function( event ) {
2048 // Look for the tab key.
2049 if ( 9 !== event.keyCode )
2052 // First try to update the index.
2053 if ( _.isUndefined( this.index ) )
2054 this.updateIndex( event );
2056 // If we still don't have an index, bail.
2057 if ( _.isUndefined( this.index ) )
2060 var index = this.index + ( event.shiftKey ? -1 : 1 );
2062 if ( index >= 0 && index < this.$tabbables.length )
2068 updateIndex: function( event ) {
2069 this.$tabbables = this.$(':tabbable');
2071 var index = this.$tabbables.index( event.target );
2080 // wp.media.view.UploaderWindow
2081 // ----------------------------
2082 media.view.UploaderWindow = media.View.extend({
2084 className: 'uploader-window',
2085 template: media.template('uploader-window'),
2087 initialize: function() {
2090 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
2092 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
2094 browser: this.$browser,
2098 // Ensure the dropzone is a jQuery collection.
2099 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) )
2100 uploader.dropzone = $( uploader.dropzone );
2102 this.controller.on( 'activate', this.refresh, this );
2105 refresh: function() {
2106 if ( this.uploader )
2107 this.uploader.refresh();
2111 var postId = media.view.settings.post.id,
2114 // If the uploader already exists, bail.
2115 if ( this.uploader )
2119 this.options.uploader.params.post_id = postId;
2121 this.uploader = new wp.Uploader( this.options.uploader );
2123 dropzone = this.uploader.dropzone;
2124 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
2125 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
2129 var $el = this.$el.show();
2131 // Ensure that the animation is triggered by waiting until
2132 // the transparent element is painted into the DOM.
2133 _.defer( function() {
2134 $el.css({ opacity: 1 });
2139 var $el = this.$el.css({ opacity: 0 });
2141 media.transition( $el ).done( function() {
2142 // Transition end events are subject to race conditions.
2143 // Make sure that the value is set as intended.
2144 if ( '0' === $el.css('opacity') )
2150 media.view.UploaderInline = media.View.extend({
2152 className: 'uploader-inline',
2153 template: media.template('uploader-inline'),
2155 initialize: function() {
2156 _.defaults( this.options, {
2161 if ( ! this.options.$browser && this.controller.uploader )
2162 this.options.$browser = this.controller.uploader.$browser;
2164 if ( _.isUndefined( this.options.postId ) )
2165 this.options.postId = media.view.settings.post.id;
2167 if ( this.options.status ) {
2168 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
2169 controller: this.controller
2174 dispose: function() {
2175 if ( this.disposing )
2176 return media.View.prototype.dispose.apply( this, arguments );
2178 // Run remove on `dispose`, so we can be sure to refresh the
2179 // uploader with a view-less DOM. Track whether we're disposing
2180 // so we don't trigger an infinite loop.
2181 this.disposing = true;
2182 return this.remove();
2185 remove: function() {
2186 var result = media.View.prototype.remove.apply( this, arguments );
2188 _.defer( _.bind( this.refresh, this ) );
2192 refresh: function() {
2193 var uploader = this.controller.uploader;
2200 var $browser = this.options.$browser,
2203 if ( this.controller.uploader ) {
2204 $placeholder = this.$('.browser');
2206 // Check if we've already replaced the placeholder.
2207 if ( $placeholder[0] === $browser[0] )
2210 $browser.detach().text( $placeholder.text() );
2211 $browser[0].className = $placeholder[0].className;
2212 $placeholder.replaceWith( $browser.show() );
2221 * wp.media.view.UploaderStatus
2223 media.view.UploaderStatus = media.View.extend({
2224 className: 'media-uploader-status',
2225 template: media.template('uploader-status'),
2228 'click .upload-dismiss-errors': 'dismiss'
2231 initialize: function() {
2232 this.queue = wp.Uploader.queue;
2233 this.queue.on( 'add remove reset', this.visibility, this );
2234 this.queue.on( 'add remove reset change:percent', this.progress, this );
2235 this.queue.on( 'add remove reset change:uploading', this.info, this );
2237 this.errors = wp.Uploader.errors;
2238 this.errors.reset();
2239 this.errors.on( 'add remove reset', this.visibility, this );
2240 this.errors.on( 'add', this.error, this );
2243 dispose: function() {
2244 wp.Uploader.queue.off( null, null, this );
2245 media.View.prototype.dispose.apply( this, arguments );
2249 visibility: function() {
2250 this.$el.toggleClass( 'uploading', !! this.queue.length );
2251 this.$el.toggleClass( 'errors', !! this.errors.length );
2252 this.$el.toggle( !! this.queue.length || !! this.errors.length );
2257 '$bar': '.media-progress-bar div',
2258 '$index': '.upload-index',
2259 '$total': '.upload-total',
2260 '$filename': '.upload-filename'
2261 }, function( selector, key ) {
2262 this[ key ] = this.$( selector );
2270 progress: function() {
2271 var queue = this.queue,
2275 if ( ! $bar || ! queue.length )
2278 $bar.width( ( queue.reduce( function( memo, attachment ) {
2279 if ( ! attachment.get('uploading') )
2282 var percent = attachment.get('percent');
2283 return memo + ( _.isNumber( percent ) ? percent : 100 );
2284 }, 0 ) / queue.length ) + '%' );
2288 var queue = this.queue,
2291 if ( ! queue.length )
2294 active = this.queue.find( function( attachment, i ) {
2296 return attachment.get('uploading');
2299 this.$index.text( index + 1 );
2300 this.$total.text( queue.length );
2301 this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
2304 filename: function( filename ) {
2305 return media.truncate( _.escape( filename ), 24 );
2308 error: function( error ) {
2309 this.views.add( '.upload-errors', new media.view.UploaderStatusError({
2310 filename: this.filename( error.get('file').name ),
2311 message: error.get('message')
2315 dismiss: function( event ) {
2316 var errors = this.views.get('.upload-errors');
2318 event.preventDefault();
2321 _.invoke( errors, 'remove' );
2322 wp.Uploader.errors.reset();
2326 media.view.UploaderStatusError = media.View.extend({
2327 className: 'upload-error',
2328 template: media.template('uploader-status-error')
2332 * wp.media.view.Toolbar
2334 media.view.Toolbar = media.View.extend({
2336 className: 'media-toolbar',
2338 initialize: function() {
2339 var state = this.controller.state(),
2340 selection = this.selection = state.get('selection'),
2341 library = this.library = state.get('library');
2345 // The toolbar is composed of two `PriorityList` views.
2346 this.primary = new media.view.PriorityList();
2347 this.secondary = new media.view.PriorityList();
2348 this.primary.$el.addClass('media-toolbar-primary');
2349 this.secondary.$el.addClass('media-toolbar-secondary');
2351 this.views.set([ this.secondary, this.primary ]);
2353 if ( this.options.items )
2354 this.set( this.options.items, { silent: true });
2356 if ( ! this.options.silent )
2360 selection.on( 'add remove reset', this.refresh, this );
2362 library.on( 'add remove reset', this.refresh, this );
2365 dispose: function() {
2366 if ( this.selection )
2367 this.selection.off( null, null, this );
2369 this.library.off( null, null, this );
2370 return media.View.prototype.dispose.apply( this, arguments );
2377 set: function( id, view, options ) {
2379 options = options || {};
2381 // Accept an object with an `id` : `view` mapping.
2382 if ( _.isObject( id ) ) {
2383 _.each( id, function( view, id ) {
2384 this.set( id, view, { silent: true });
2388 if ( ! ( view instanceof Backbone.View ) ) {
2389 view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
2390 view = new media.view.Button( view ).render();
2393 view.controller = view.controller || this.controller;
2395 this._views[ id ] = view;
2397 list = view.options.priority < 0 ? 'secondary' : 'primary';
2398 this[ list ].set( id, view, options );
2401 if ( ! options.silent )
2407 get: function( id ) {
2408 return this._views[ id ];
2411 unset: function( id, options ) {
2412 delete this._views[ id ];
2413 this.primary.unset( id, options );
2414 this.secondary.unset( id, options );
2416 if ( ! options || ! options.silent )
2421 refresh: function() {
2422 var state = this.controller.state(),
2423 library = state.get('library'),
2424 selection = state.get('selection');
2426 _.each( this._views, function( button ) {
2427 if ( ! button.model || ! button.options || ! button.options.requires )
2430 var requires = button.options.requires,
2433 if ( requires.selection && selection && ! selection.length )
2435 else if ( requires.library && library && ! library.length )
2438 button.model.set( 'disabled', disabled );
2443 // wp.media.view.Toolbar.Select
2444 // ----------------------------
2445 media.view.Toolbar.Select = media.view.Toolbar.extend({
2446 initialize: function() {
2447 var options = this.options,
2448 controller = options.controller,
2449 selection = controller.state().get('selection');
2451 _.bindAll( this, 'clickSelect' );
2453 _.defaults( options, {
2460 // Does the button rely on the selection?
2466 options.items = _.defaults( options.items || {}, {
2471 click: this.clickSelect,
2472 requires: options.requires
2476 media.view.Toolbar.prototype.initialize.apply( this, arguments );
2479 clickSelect: function() {
2480 var options = this.options,
2481 controller = this.controller;
2483 if ( options.close )
2486 if ( options.event )
2487 controller.state().trigger( options.event );
2489 if ( options.reset )
2492 if ( options.state )
2493 controller.setState( options.state );
2497 // wp.media.view.Toolbar.Embed
2498 // ---------------------------
2499 media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
2500 initialize: function() {
2501 _.defaults( this.options, {
2502 text: l10n.insertIntoPost,
2506 media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
2509 refresh: function() {
2510 var url = this.controller.state().props.get('url');
2511 this.get('select').model.set( 'disabled', ! url || url === 'http://' );
2513 media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
2518 * wp.media.view.Button
2520 media.view.Button = media.View.extend({
2522 className: 'media-button',
2523 attributes: { href: '#' },
2536 initialize: function() {
2537 // Create a model with the provided `defaults`.
2538 this.model = new Backbone.Model( this.defaults );
2540 // If any of the `options` have a key from `defaults`, apply its
2541 // value to the `model` and remove it from the `options object.
2542 _.each( this.defaults, function( def, key ) {
2543 var value = this.options[ key ];
2544 if ( _.isUndefined( value ) )
2547 this.model.set( key, value );
2548 delete this.options[ key ];
2551 this.model.on( 'change', this.render, this );
2554 render: function() {
2555 var classes = [ 'button', this.className ],
2556 model = this.model.toJSON();
2559 classes.push( 'button-' + model.style );
2562 classes.push( 'button-' + model.size );
2564 classes = _.uniq( classes.concat( this.options.classes ) );
2565 this.el.className = classes.join(' ');
2567 this.$el.attr( 'disabled', model.disabled );
2568 this.$el.text( this.model.get('text') );
2573 click: function( event ) {
2574 if ( '#' === this.attributes.href )
2575 event.preventDefault();
2577 if ( this.options.click && ! this.model.get('disabled') )
2578 this.options.click.apply( this, arguments );
2583 * wp.media.view.ButtonGroup
2585 media.view.ButtonGroup = media.View.extend({
2587 className: 'button-group button-large media-button-group',
2589 initialize: function() {
2590 this.buttons = _.map( this.options.buttons || [], function( button ) {
2591 if ( button instanceof Backbone.View )
2594 return new media.view.Button( button ).render();
2597 delete this.options.buttons;
2599 if ( this.options.classes )
2600 this.$el.addClass( this.options.classes );
2603 render: function() {
2604 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
2610 * wp.media.view.PriorityList
2613 media.view.PriorityList = media.View.extend({
2616 initialize: function() {
2619 this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
2620 delete this.options.views;
2622 if ( ! this.options.silent )
2626 set: function( id, view, options ) {
2627 var priority, views, index;
2629 options = options || {};
2631 // Accept an object with an `id` : `view` mapping.
2632 if ( _.isObject( id ) ) {
2633 _.each( id, function( view, id ) {
2634 this.set( id, view );
2639 if ( ! (view instanceof Backbone.View) )
2640 view = this.toView( view, id, options );
2642 view.controller = view.controller || this.controller;
2646 priority = view.options.priority || 10;
2647 views = this.views.get() || [];
2649 _.find( views, function( existing, i ) {
2650 if ( existing.options.priority > priority ) {
2656 this._views[ id ] = view;
2657 this.views.add( view, {
2658 at: _.isNumber( index ) ? index : views.length || 0
2664 get: function( id ) {
2665 return this._views[ id ];
2668 unset: function( id ) {
2669 var view = this.get( id );
2674 delete this._views[ id ];
2678 toView: function( options ) {
2679 return new media.View( options );
2684 * wp.media.view.MenuItem
2686 media.view.MenuItem = media.View.extend({
2688 className: 'media-menu-item',
2698 _click: function( event ) {
2699 var clickOverride = this.options.click;
2702 event.preventDefault();
2704 if ( clickOverride )
2705 clickOverride.call( this );
2711 var state = this.options.state;
2713 this.controller.setState( state );
2716 render: function() {
2717 var options = this.options;
2720 this.$el.text( options.text );
2721 else if ( options.html )
2722 this.$el.html( options.html );
2729 * wp.media.view.Menu
2731 media.view.Menu = media.view.PriorityList.extend({
2733 className: 'media-menu',
2735 ItemView: media.view.MenuItem,
2738 toView: function( options, id ) {
2739 options = options || {};
2740 options[ this.property ] = options[ this.property ] || id;
2741 return new this.ItemView( options ).render();
2745 media.view.PriorityList.prototype.ready.apply( this, arguments );
2750 media.view.PriorityList.prototype.set.apply( this, arguments );
2755 media.view.PriorityList.prototype.unset.apply( this, arguments );
2759 visibility: function() {
2760 var region = this.region,
2761 view = this.controller[ region ].get(),
2762 views = this.views.get(),
2763 hide = ! views || views.length < 2;
2765 if ( this === view )
2766 this.controller.$el.toggleClass( 'hide-' + region, hide );
2769 select: function( id ) {
2770 var view = this.get( id );
2776 view.$el.addClass('active');
2779 deselect: function() {
2780 this.$el.children().removeClass('active');
2785 * wp.media.view.RouterItem
2787 media.view.RouterItem = media.view.MenuItem.extend({
2789 var contentMode = this.options.contentMode;
2791 this.controller.content.mode( contentMode );
2796 * wp.media.view.Router
2798 media.view.Router = media.view.Menu.extend({
2800 className: 'media-router',
2801 property: 'contentMode',
2802 ItemView: media.view.RouterItem,
2805 initialize: function() {
2806 this.controller.on( 'content:render', this.update, this );
2807 media.view.Menu.prototype.initialize.apply( this, arguments );
2810 update: function() {
2811 var mode = this.controller.content.mode();
2813 this.select( mode );
2819 * wp.media.view.Sidebar
2821 media.view.Sidebar = media.view.PriorityList.extend({
2822 className: 'media-sidebar'
2826 * wp.media.view.Attachment
2828 media.view.Attachment = media.View.extend({
2830 className: 'attachment',
2831 template: media.template('attachment'),
2834 'click .attachment-preview': 'toggleSelectionHandler',
2835 'change [data-setting]': 'updateSetting',
2836 'change [data-setting] input': 'updateSetting',
2837 'change [data-setting] select': 'updateSetting',
2838 'change [data-setting] textarea': 'updateSetting',
2839 'click .close': 'removeFromLibrary',
2840 'click .check': 'removeFromSelection',
2841 'click a': 'preventDefault'
2846 initialize: function() {
2847 var selection = this.options.selection;
2849 this.model.on( 'change:sizes change:uploading', this.render, this );
2850 this.model.on( 'change:title', this._syncTitle, this );
2851 this.model.on( 'change:caption', this._syncCaption, this );
2852 this.model.on( 'change:percent', this.progress, this );
2854 // Update the selection.
2855 this.model.on( 'add', this.select, this );
2856 this.model.on( 'remove', this.deselect, this );
2858 selection.on( 'reset', this.updateSelect, this );
2860 // Update the model's details view.
2861 this.model.on( 'selection:single selection:unsingle', this.details, this );
2862 this.details( this.model, this.controller.state().get('selection') );
2865 dispose: function() {
2866 var selection = this.options.selection;
2868 // Make sure all settings are saved before removing the view.
2872 selection.off( null, null, this );
2874 media.View.prototype.dispose.apply( this, arguments );
2878 render: function() {
2879 var options = _.defaults( this.model.toJSON(), {
2880 orientation: 'landscape',
2896 options.buttons = this.buttons;
2897 options.describe = this.controller.state().get('describe');
2899 if ( 'image' === options.type )
2900 options.size = this.imageSize();
2903 if ( options.nonces ) {
2904 options.can.remove = !! options.nonces['delete'];
2905 options.can.save = !! options.nonces.update;
2908 if ( this.controller.state().get('allowLocalEdits') )
2909 options.allowLocalEdits = true;
2911 this.views.detach();
2912 this.$el.html( this.template( options ) );
2914 this.$el.toggleClass( 'uploading', options.uploading );
2915 if ( options.uploading )
2916 this.$bar = this.$('.media-progress-bar div');
2920 // Check if the model is selected.
2921 this.updateSelect();
2923 // Update the save status.
2926 this.views.render();
2931 progress: function() {
2932 if ( this.$bar && this.$bar.length )
2933 this.$bar.width( this.model.get('percent') + '%' );
2936 toggleSelectionHandler: function( event ) {
2939 if ( event.shiftKey )
2941 else if ( event.ctrlKey || event.metaKey )
2944 this.toggleSelection({
2949 toggleSelection: function( options ) {
2950 var collection = this.collection,
2951 selection = this.options.selection,
2953 method = options && options.method,
2954 single, between, models, singleIndex, modelIndex;
2959 single = selection.single();
2960 method = _.isUndefined( method ) ? selection.multiple : method;
2962 // If the `method` is set to `between`, select all models that
2963 // exist between the current and the selected model.
2964 if ( 'between' === method && single && selection.multiple ) {
2965 // If the models are the same, short-circuit.
2966 if ( single === model )
2969 singleIndex = collection.indexOf( single );
2970 modelIndex = collection.indexOf( this.model );
2972 if ( singleIndex < modelIndex )
2973 models = collection.models.slice( singleIndex, modelIndex + 1 );
2975 models = collection.models.slice( modelIndex, singleIndex + 1 );
2977 selection.add( models ).single( model );
2980 // If the `method` is set to `toggle`, just flip the selection
2981 // status, regardless of whether the model is the single model.
2982 } else if ( 'toggle' === method ) {
2983 selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model );
2987 if ( method !== 'add' )
2990 if ( this.selected() ) {
2991 // If the model is the single model, remove it.
2992 // If it is not the same as the single model,
2993 // it now becomes the single model.
2994 selection[ single === model ? 'remove' : 'single' ]( model );
2996 // If the model is not selected, run the `method` on the
2997 // selection. By default, we `reset` the selection, but the
2998 // `method` can be set to `add` the model to the selection.
2999 selection[ method ]( model ).single( model );
3003 updateSelect: function() {
3004 this[ this.selected() ? 'select' : 'deselect' ]();
3007 selected: function() {
3008 var selection = this.options.selection;
3010 return !! selection.getByCid( this.model.cid );
3013 select: function( model, collection ) {
3014 var selection = this.options.selection;
3016 // Check if a selection exists and if it's the collection provided.
3017 // If they're not the same collection, bail; we're in another
3018 // selection's event loop.
3019 if ( ! selection || ( collection && collection !== selection ) )
3022 this.$el.addClass('selected');
3025 deselect: function( model, collection ) {
3026 var selection = this.options.selection;
3028 // Check if a selection exists and if it's the collection provided.
3029 // If they're not the same collection, bail; we're in another
3030 // selection's event loop.
3031 if ( ! selection || ( collection && collection !== selection ) )
3034 this.$el.removeClass('selected');
3037 details: function( model, collection ) {
3038 var selection = this.options.selection,
3041 if ( selection !== collection )
3044 details = selection.single();
3045 this.$el.toggleClass( 'details', details === this.model );
3048 preventDefault: function( event ) {
3049 event.preventDefault();
3052 imageSize: function( size ) {
3053 var sizes = this.model.get('sizes');
3055 size = size || 'medium';
3057 // Use the provided image size if possible.
3058 if ( sizes && sizes[ size ] ) {
3059 return _.clone( sizes[ size ] );
3062 url: this.model.get('url'),
3063 width: this.model.get('width'),
3064 height: this.model.get('height'),
3065 orientation: this.model.get('orientation')
3070 updateSetting: function( event ) {
3071 var $setting = $( event.target ).closest('[data-setting]'),
3074 if ( ! $setting.length )
3077 setting = $setting.data('setting');
3078 value = event.target.value;
3080 if ( this.model.get( setting ) !== value )
3081 this.save( setting, value );
3084 // Pass all the arguments to the model's save method.
3086 // Records the aggregate status of all save requests and updates the
3087 // view's classes accordingly.
3090 save = this._save = this._save || { status: 'ready' },
3091 request = this.model.save.apply( this.model, arguments ),
3092 requests = save.requests ? $.when( request, save.requests ) : request;
3094 // If we're waiting to remove 'Saved.', stop.
3095 if ( save.savedTimer )
3096 clearTimeout( save.savedTimer );
3098 this.updateSave('waiting');
3099 save.requests = requests;
3100 requests.always( function() {
3101 // If we've performed another request since this one, bail.
3102 if ( save.requests !== requests )
3105 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
3106 save.savedTimer = setTimeout( function() {
3107 view.updateSave('ready');
3108 delete save.savedTimer;
3114 updateSave: function( status ) {
3115 var save = this._save = this._save || { status: 'ready' };
3117 if ( status && status !== save.status ) {
3118 this.$el.removeClass( 'save-' + save.status );
3119 save.status = status;
3122 this.$el.addClass( 'save-' + save.status );
3126 updateAll: function() {
3127 var $settings = this.$('[data-setting]'),
3131 changed = _.chain( $settings ).map( function( el ) {
3132 var $input = $('input, textarea, select, [value]', el ),
3135 if ( ! $input.length )
3138 setting = $(el).data('setting');
3139 value = $input.val();
3141 // Record the value if it changed.
3142 if ( model.get( setting ) !== value )
3143 return [ setting, value ];
3144 }).compact().object().value();
3146 if ( ! _.isEmpty( changed ) )
3147 model.save( changed );
3150 removeFromLibrary: function( event ) {
3151 // Stop propagation so the model isn't selected.
3152 event.stopPropagation();
3154 this.collection.remove( this.model );
3157 removeFromSelection: function( event ) {
3158 var selection = this.options.selection;
3162 // Stop propagation so the model isn't selected.
3163 event.stopPropagation();
3165 selection.remove( this.model );
3169 // Ensure settings remain in sync between attachment views.
3171 caption: '_syncCaption',
3173 }, function( method, setting ) {
3174 media.view.Attachment.prototype[ method ] = function( model, value ) {
3175 var $setting = this.$('[data-setting="' + setting + '"]');
3177 if ( ! $setting.length )
3180 // If the updated value is in sync with the value in the DOM, there
3181 // is no need to re-render. If we're currently editing the value,
3182 // it will automatically be in sync, suppressing the re-render for
3183 // the view we're editing, while updating any others.
3184 if ( value === $setting.find('input, textarea, select, [value]').val() )
3187 return this.render();
3192 * wp.media.view.Attachment.Library
3194 media.view.Attachment.Library = media.view.Attachment.extend({
3201 * wp.media.view.Attachment.EditLibrary
3203 media.view.Attachment.EditLibrary = media.view.Attachment.extend({
3210 * wp.media.view.Attachments
3212 media.view.Attachments = media.View.extend({
3214 className: 'attachments',
3216 cssTemplate: media.template('attachments-css'),
3222 initialize: function() {
3223 this.el.id = _.uniqueId('__attachments-view-');
3225 _.defaults( this.options, {
3226 refreshSensitivity: 200,
3227 refreshThreshold: 3,
3228 AttachmentView: media.view.Attachment,
3233 this._viewsByCid = {};
3235 this.collection.on( 'add', function( attachment, attachments, options ) {
3236 this.views.add( this.createAttachmentView( attachment ), {
3241 this.collection.on( 'remove', function( attachment, attachments, options ) {
3242 var view = this._viewsByCid[ attachment.cid ];
3243 delete this._viewsByCid[ attachment.cid ];
3249 this.collection.on( 'reset', this.render, this );
3251 // Throttle the scroll handler.
3252 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
3254 this.initSortable();
3256 _.bindAll( this, 'css' );
3257 this.model.on( 'change:edge change:gutter', this.css, this );
3258 this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
3259 if ( this.options.resize )
3260 $(window).on( 'resize.attachments', this._resizeCss );
3264 dispose: function() {
3265 this.collection.props.off( null, null, this );
3266 $(window).off( 'resize.attachments', this._resizeCss );
3267 media.View.prototype.dispose.apply( this, arguments );
3271 var $css = $( '#' + this.el.id + '-css' );
3276 media.view.Attachments.$head().append( this.cssTemplate({
3279 gutter: this.model.get('gutter')
3284 var edge = this.model.get('edge'),
3285 gutter, width, columns;
3287 if ( ! this.$el.is(':visible') )
3290 gutter = this.model.get('gutter') * 2;
3291 width = this.$el.width() - gutter;
3292 columns = Math.ceil( width / ( edge + gutter ) );
3293 edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
3297 initSortable: function() {
3298 var collection = this.collection;
3300 if ( ! this.options.sortable || ! $.fn.sortable )
3303 this.$el.sortable( _.extend({
3304 // If the `collection` has a `comparator`, disable sorting.
3305 disabled: !! collection.comparator,
3307 // Prevent attachments from being dragged outside the bounding
3309 containment: this.$el,
3311 // Change the position of the attachment as soon as the
3312 // mouse pointer overlaps a thumbnail.
3313 tolerance: 'pointer',
3315 // Record the initial `index` of the dragged model.
3316 start: function( event, ui ) {
3317 ui.item.data('sortableIndexStart', ui.item.index());
3320 // Update the model's index in the collection.
3321 // Do so silently, as the view is already accurate.
3322 update: function( event, ui ) {
3323 var model = collection.at( ui.item.data('sortableIndexStart') ),
3324 comparator = collection.comparator;
3326 // Temporarily disable the comparator to prevent `add`
3328 delete collection.comparator;
3330 // Silently shift the model to its new index.
3331 collection.remove( model, {
3338 // Restore the comparator.
3339 collection.comparator = comparator;
3341 // Fire the `reset` event to ensure other collections sync.
3342 collection.trigger( 'reset', collection );
3344 // If the collection is sorted by menu order,
3345 // update the menu order.
3346 collection.saveMenuOrder();
3348 }, this.options.sortable ) );
3350 // If the `orderby` property is changed on the `collection`,
3351 // check to see if we have a `comparator`. If so, disable sorting.
3352 collection.props.on( 'change:orderby', function() {
3353 this.$el.sortable( 'option', 'disabled', !! collection.comparator );
3356 this.collection.props.on( 'change:orderby', this.refreshSortable, this );
3357 this.refreshSortable();
3360 refreshSortable: function() {
3361 if ( ! this.options.sortable || ! $.fn.sortable )
3364 // If the `collection` has a `comparator`, disable sorting.
3365 var collection = this.collection,
3366 orderby = collection.props.get('orderby'),
3367 enabled = 'menuOrder' === orderby || ! collection.comparator;
3369 this.$el.sortable( 'option', 'disabled', ! enabled );
3372 createAttachmentView: function( attachment ) {
3373 var view = new this.options.AttachmentView({
3374 controller: this.controller,
3376 collection: this.collection,
3377 selection: this.options.selection
3380 return this._viewsByCid[ attachment.cid ] = view;
3383 prepare: function() {
3384 // Create all of the Attachment views, and replace
3385 // the list in a single DOM operation.
3386 if ( this.collection.length ) {
3387 this.views.set( this.collection.map( this.createAttachmentView, this ) );
3389 // If there are no elements, clear the views and load some.
3392 this.collection.more().done( this.scroll );
3397 // Trigger the scroll event to check if we're within the
3398 // threshold to query for additional attachments.
3402 scroll: function( event ) {
3403 // @todo: is this still necessary?
3404 if ( ! this.$el.is(':visible') )
3407 if ( this.collection.hasMore() && this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
3408 this.collection.more().done( this.scroll );
3412 $head: (function() {
3415 return $head = $head || $('head');
3421 * wp.media.view.Search
3423 media.view.Search = media.View.extend({
3425 className: 'search',
3429 placeholder: l10n.search
3439 render: function() {
3440 this.el.value = this.model.escape('search');
3444 search: function( event ) {
3445 if ( event.target.value )
3446 this.model.set( 'search', event.target.value );
3448 this.model.unset('search');
3453 * wp.media.view.AttachmentFilters
3455 media.view.AttachmentFilters = media.View.extend({
3457 className: 'attachment-filters',
3465 initialize: function() {
3466 this.createFilters();
3467 _.extend( this.filters, this.options.filters );
3469 // Build `<option>` elements.
3470 this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
3472 el: this.make( 'option', { value: value }, filter.text ),
3473 priority: filter.priority || 50
3475 }, this ).sortBy('priority').pluck('el').value() );
3477 this.model.on( 'change', this.select, this );
3481 createFilters: function() {
3485 change: function( event ) {
3486 var filter = this.filters[ this.el.value ];
3489 this.model.set( filter.props );
3492 select: function() {
3493 var model = this.model,
3495 props = model.toJSON();
3497 _.find( this.filters, function( filter, id ) {
3498 var equal = _.all( filter.props, function( prop, key ) {
3499 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
3506 this.$el.val( value );
3510 media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
3511 createFilters: function() {
3512 var type = this.model.get('type'),
3513 types = media.view.settings.mimeTypes,
3516 if ( types && type )
3517 text = types[ type ];
3521 text: text || l10n.allMediaItems,
3531 text: l10n.uploadedToThisPost,
3533 uploadedTo: media.view.settings.post.id,
3534 orderby: 'menuOrder',
3543 media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
3544 createFilters: function() {
3547 _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
3560 text: l10n.allMediaItems,
3570 filters.uploaded = {
3571 text: l10n.uploadedToThisPost,
3574 uploadedTo: media.view.settings.post.id,
3575 orderby: 'menuOrder',
3581 this.filters = filters;
3588 * wp.media.view.AttachmentsBrowser
3590 media.view.AttachmentsBrowser = media.View.extend({
3592 className: 'attachments-browser',
3594 initialize: function() {
3595 _.defaults( this.options, {
3600 AttachmentView: media.view.Attachment.Library
3603 this.createToolbar();
3604 this.updateContent();
3605 this.createSidebar();
3607 this.collection.on( 'add remove reset', this.updateContent, this );
3610 dispose: function() {
3611 this.options.selection.off( null, null, this );
3612 media.View.prototype.dispose.apply( this, arguments );
3616 createToolbar: function() {
3617 var filters, FiltersConstructor;
3619 this.toolbar = new media.view.Toolbar({
3620 controller: this.controller
3623 this.views.add( this.toolbar );
3625 filters = this.options.filters;
3626 if ( 'uploaded' === filters )
3627 FiltersConstructor = media.view.AttachmentFilters.Uploaded;
3628 else if ( 'all' === filters )
3629 FiltersConstructor = media.view.AttachmentFilters.All;
3631 if ( FiltersConstructor ) {
3632 this.toolbar.set( 'filters', new FiltersConstructor({
3633 controller: this.controller,
3634 model: this.collection.props,
3639 if ( this.options.search ) {
3640 this.toolbar.set( 'search', new media.view.Search({
3641 controller: this.controller,
3642 model: this.collection.props,
3647 if ( this.options.dragInfo ) {
3648 this.toolbar.set( 'dragInfo', new media.View({
3649 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
3655 updateContent: function() {
3658 if( ! this.attachments )
3659 this.createAttachments();
3661 if ( ! this.collection.length ) {
3662 this.collection.more().done( function() {
3663 if ( ! view.collection.length )
3664 view.createUploader();
3669 removeContent: function() {
3670 _.each(['attachments','uploader'], function( key ) {
3671 if ( this[ key ] ) {
3672 this[ key ].remove();
3678 createUploader: function() {
3679 this.removeContent();
3681 this.uploader = new media.view.UploaderInline({
3682 controller: this.controller,
3684 message: l10n.noItemsFound
3687 this.views.add( this.uploader );
3690 createAttachments: function() {
3691 this.removeContent();
3693 this.attachments = new media.view.Attachments({
3694 controller: this.controller,
3695 collection: this.collection,
3696 selection: this.options.selection,
3698 sortable: this.options.sortable,
3700 // The single `Attachment` view to be used in the `Attachments` view.
3701 AttachmentView: this.options.AttachmentView
3704 this.views.add( this.attachments );
3707 createSidebar: function() {
3708 var options = this.options,
3709 selection = options.selection,
3710 sidebar = this.sidebar = new media.view.Sidebar({
3711 controller: this.controller
3714 this.views.add( sidebar );
3716 if ( this.controller.uploader ) {
3717 sidebar.set( 'uploads', new media.view.UploaderStatus({
3718 controller: this.controller,
3723 selection.on( 'selection:single', this.createSingle, this );
3724 selection.on( 'selection:unsingle', this.disposeSingle, this );
3726 if ( selection.single() )
3727 this.createSingle();
3730 createSingle: function() {
3731 var sidebar = this.sidebar,
3732 single = this.options.selection.single(),
3735 sidebar.set( 'details', new media.view.Attachment.Details({
3736 controller: this.controller,
3741 sidebar.set( 'compat', new media.view.AttachmentCompat({
3742 controller: this.controller,
3747 if ( this.options.display ) {
3748 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
3749 controller: this.controller,
3750 model: this.model.display( single ),
3753 userSettings: this.model.get('displayUserSettings')
3758 disposeSingle: function() {
3759 var sidebar = this.sidebar;
3760 sidebar.unset('details');
3761 sidebar.unset('compat');
3762 sidebar.unset('display');
3767 * wp.media.view.Selection
3769 media.view.Selection = media.View.extend({
3771 className: 'media-selection',
3772 template: media.template('media-selection'),
3775 'click .edit-selection': 'edit',
3776 'click .clear-selection': 'clear'
3779 initialize: function() {
3780 _.defaults( this.options, {
3785 this.attachments = new media.view.Attachments.Selection({
3786 controller: this.controller,
3787 collection: this.collection,
3788 selection: this.collection,
3789 model: new Backbone.Model({
3795 this.views.set( '.selection-view', this.attachments );
3796 this.collection.on( 'add remove reset', this.refresh, this );
3797 this.controller.on( 'content:activate', this.refresh, this );
3804 refresh: function() {
3805 // If the selection hasn't been rendered, bail.
3806 if ( ! this.$el.children().length )
3809 var collection = this.collection,
3810 editing = 'edit-selection' === this.controller.content.mode();
3812 // If nothing is selected, display nothing.
3813 this.$el.toggleClass( 'empty', ! collection.length );
3814 this.$el.toggleClass( 'one', 1 === collection.length );
3815 this.$el.toggleClass( 'editing', editing );
3817 this.$('.count').text( l10n.selected.replace('%d', collection.length) );
3820 edit: function( event ) {
3821 event.preventDefault();
3822 if ( this.options.editable )
3823 this.options.editable.call( this, this.collection );
3826 clear: function( event ) {
3827 event.preventDefault();
3828 this.collection.reset();
3834 * wp.media.view.Attachment.Selection
3836 media.view.Attachment.Selection = media.view.Attachment.extend({
3837 className: 'attachment selection',
3839 // On click, just select the model, instead of removing the model from
3841 toggleSelection: function() {
3842 this.options.selection.single( this.model );
3847 * wp.media.view.Attachments.Selection
3849 media.view.Attachments.Selection = media.view.Attachments.extend({
3851 initialize: function() {
3852 _.defaults( this.options, {
3856 // The single `Attachment` view to be used in the `Attachments` view.
3857 AttachmentView: media.view.Attachment.Selection
3859 return media.view.Attachments.prototype.initialize.apply( this, arguments );
3864 * wp.media.view.Attachments.EditSelection
3866 media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
3874 * wp.media.view.Settings
3876 media.view.Settings = media.View.extend({
3878 'click button': 'updateHandler',
3879 'change input': 'updateHandler',
3880 'change select': 'updateHandler',
3881 'change textarea': 'updateHandler'
3884 initialize: function() {
3885 this.model = this.model || new Backbone.Model();
3886 this.model.on( 'change', this.updateChanges, this );
3889 prepare: function() {
3891 model: this.model.toJSON()
3895 render: function() {
3896 media.View.prototype.render.apply( this, arguments );
3897 // Select the correct values.
3898 _( this.model.attributes ).chain().keys().each( this.update, this );
3902 update: function( key ) {
3903 var value = this.model.get( key ),
3904 $setting = this.$('[data-setting="' + key + '"]'),
3907 // Bail if we didn't find a matching setting.
3908 if ( ! $setting.length )
3911 // Attempt to determine how the setting is rendered and update
3912 // the selected value.
3914 // Handle dropdowns.
3915 if ( $setting.is('select') ) {
3916 $value = $setting.find('[value="' + value + '"]');
3918 if ( $value.length ) {
3919 $setting.find('option').prop( 'selected', false );
3920 $value.prop( 'selected', true );
3922 // If we can't find the desired value, record what *is* selected.
3923 this.model.set( key, $setting.find(':selected').val() );
3927 // Handle button groups.
3928 } else if ( $setting.hasClass('button-group') ) {
3929 $buttons = $setting.find('button').removeClass('active');
3930 $buttons.filter( '[value="' + value + '"]' ).addClass('active');
3932 // Handle text inputs and textareas.
3933 } else if ( $setting.is('input[type="text"], textarea') ) {
3934 if ( ! $setting.is(':focus') )
3935 $setting.val( value );
3937 // Handle checkboxes.
3938 } else if ( $setting.is('input[type="checkbox"]') ) {
3939 $setting.attr( 'checked', !! value );
3943 updateHandler: function( event ) {
3944 var $setting = $( event.target ).closest('[data-setting]'),
3945 value = event.target.value,
3948 event.preventDefault();
3950 if ( ! $setting.length )
3953 // Use the correct value for checkboxes.
3954 if ( $setting.is('input[type="checkbox"]') )
3955 value = $setting[0].checked;
3957 // Update the corresponding setting.
3958 this.model.set( $setting.data('setting'), value );
3960 // If the setting has a corresponding user setting,
3961 // update that as well.
3962 if ( userSetting = $setting.data('userSetting') )
3963 setUserSetting( userSetting, value );
3966 updateChanges: function( model, options ) {
3967 if ( options.changes )
3968 _( options.changes ).chain().keys().each( this.update, this );
3973 * wp.media.view.Settings.AttachmentDisplay
3975 media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
3976 className: 'attachment-display-settings',
3977 template: media.template('attachment-display-settings'),
3979 initialize: function() {
3980 var attachment = this.options.attachment;
3982 _.defaults( this.options, {
3986 media.view.Settings.prototype.initialize.apply( this, arguments );
3987 this.model.on( 'change:link', this.updateLinkTo, this );
3990 attachment.on( 'change:uploading', this.render, this );
3993 dispose: function() {
3994 var attachment = this.options.attachment;
3996 attachment.off( null, null, this );
3998 media.view.Settings.prototype.dispose.apply( this, arguments );
4001 render: function() {
4002 var attachment = this.options.attachment;
4004 _.extend( this.options, {
4005 sizes: attachment.get('sizes'),
4006 type: attachment.get('type')
4010 media.view.Settings.prototype.render.call( this );
4011 this.updateLinkTo();
4015 updateLinkTo: function() {
4016 var linkTo = this.model.get('link'),
4017 $input = this.$('.link-to-custom'),
4018 attachment = this.options.attachment;
4020 if ( 'none' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
4026 if ( 'post' === linkTo ) {
4027 $input.val( attachment.get('link') );
4028 } else if ( 'file' === linkTo ) {
4029 $input.val( attachment.get('url') );
4030 } else if ( ! this.model.get('linkUrl') ) {
4031 $input.val('http://');
4034 $input.prop( 'readonly', 'custom' !== linkTo );
4039 // If the input is visible, focus and select its contents.
4040 if ( $input.is(':visible') )
4041 $input.focus()[0].select();
4046 * wp.media.view.Settings.Gallery
4048 media.view.Settings.Gallery = media.view.Settings.extend({
4049 className: 'gallery-settings',
4050 template: media.template('gallery-settings')
4054 * wp.media.view.Attachment.Details
4056 media.view.Attachment.Details = media.view.Attachment.extend({
4058 className: 'attachment-details',
4059 template: media.template('attachment-details'),
4062 'change [data-setting]': 'updateSetting',
4063 'change [data-setting] input': 'updateSetting',
4064 'change [data-setting] select': 'updateSetting',
4065 'change [data-setting] textarea': 'updateSetting',
4066 'click .delete-attachment': 'deleteAttachment',
4067 'click .edit-attachment': 'editAttachment',
4068 'click .refresh-attachment': 'refreshAttachment'
4071 initialize: function() {
4072 this.focusManager = new media.view.FocusManager({
4076 media.view.Attachment.prototype.initialize.apply( this, arguments );
4079 render: function() {
4080 media.view.Attachment.prototype.render.apply( this, arguments );
4081 this.focusManager.focus();
4085 deleteAttachment: function( event ) {
4086 event.preventDefault();
4088 if ( confirm( l10n.warnDelete ) )
4089 this.model.destroy();
4092 editAttachment: function( event ) {
4093 this.$el.addClass('needs-refresh');
4096 refreshAttachment: function( event ) {
4097 this.$el.removeClass('needs-refresh');
4098 event.preventDefault();
4104 * wp.media.view.AttachmentCompat
4106 media.view.AttachmentCompat = media.View.extend({
4108 className: 'compat-item',
4111 'submit': 'preventDefault',
4112 'change input': 'save',
4113 'change select': 'save',
4114 'change textarea': 'save'
4117 initialize: function() {
4118 this.focusManager = new media.view.FocusManager({
4122 this.model.on( 'change:compat', this.render, this );
4125 dispose: function() {
4126 if ( this.$(':focus').length )
4129 return media.View.prototype.dispose.apply( this, arguments );
4132 render: function() {
4133 var compat = this.model.get('compat');
4134 if ( ! compat || ! compat.item )
4137 this.views.detach();
4138 this.$el.html( compat.item );
4139 this.views.render();
4141 this.focusManager.focus();
4145 preventDefault: function( event ) {
4146 event.preventDefault();
4149 save: function( event ) {
4153 event.preventDefault();
4155 _.each( this.$el.serializeArray(), function( pair ) {
4156 data[ pair.name ] = pair.value;
4159 this.model.saveCompat( data );
4164 * wp.media.view.Iframe
4166 media.view.Iframe = media.View.extend({
4167 className: 'media-iframe',
4169 render: function() {
4170 this.views.detach();
4171 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
4172 this.views.render();
4178 * wp.media.view.Embed
4180 media.view.Embed = media.View.extend({
4181 className: 'media-embed',
4183 initialize: function() {
4184 this.url = new media.view.EmbedUrl({
4185 controller: this.controller,
4186 model: this.model.props
4189 this.views.set([ this.url ]);
4191 this.model.on( 'change:type', this.refresh, this );
4192 this.model.on( 'change:loading', this.loading, this );
4195 settings: function( view ) {
4196 if ( this._settings )
4197 this._settings.remove();
4198 this._settings = view;
4199 this.views.add( view );
4202 refresh: function() {
4203 var type = this.model.get('type'),
4206 if ( 'image' === type )
4207 constructor = media.view.EmbedImage;
4208 else if ( 'link' === type )
4209 constructor = media.view.EmbedLink;
4213 this.settings( new constructor({
4214 controller: this.controller,
4215 model: this.model.props,
4220 loading: function() {
4221 this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
4226 * wp.media.view.EmbedUrl
4228 media.view.EmbedUrl = media.View.extend({
4230 className: 'embed-url',
4238 initialize: function() {
4239 this.input = this.make( 'input', {
4241 value: this.model.get('url') || ''
4244 this.spinner = this.make( 'span', {
4248 this.$input = $( this.input );
4249 this.$el.append([ this.input, this.spinner ]);
4251 this.model.on( 'change:url', this.render, this );
4254 render: function() {
4255 var $input = this.$input;
4257 if ( $input.is(':focus') )
4260 this.input.value = this.model.get('url') || 'http://';
4261 media.View.prototype.render.apply( this, arguments );
4269 url: function( event ) {
4270 this.model.set( 'url', event.target.value );
4274 var $input = this.$input;
4275 // If the input is visible, focus and select its contents.
4276 if ( $input.is(':visible') )
4277 $input.focus()[0].select();
4282 * wp.media.view.EmbedLink
4284 media.view.EmbedLink = media.view.Settings.extend({
4285 className: 'embed-link-settings',
4286 template: media.template('embed-link-settings')
4290 * wp.media.view.EmbedImage
4292 media.view.EmbedImage = media.view.Settings.AttachmentDisplay.extend({
4293 className: 'embed-image-settings',
4294 template: media.template('embed-image-settings'),
4296 initialize: function() {
4297 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
4298 this.model.on( 'change:url', this.updateImage, this );
4301 updateImage: function() {
4302 this.$('img').attr( 'src', this.model.get('url') );