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