mirror of
https://github.com/WordPress/WordPress.git
synced 2024-09-10 22:58:33 +02:00
Fixes #6820. Built from https://develop.svn.wordpress.org/trunk@31960 git-svn-id: http://core.svn.wordpress.org/trunk@31939 1a063a9b-81f0-0310-95a4-ce76da25c4cd
555 lines
14 KiB
JavaScript
555 lines
14 KiB
JavaScript
/*globals wp, _, jQuery */
|
|
|
|
/**
|
|
* wp.media.view.Attachment
|
|
*
|
|
* @class
|
|
* @augments wp.media.View
|
|
* @augments wp.Backbone.View
|
|
* @augments Backbone.View
|
|
*/
|
|
var View = wp.media.View,
|
|
$ = jQuery,
|
|
Attachment;
|
|
|
|
Attachment = View.extend({
|
|
tagName: 'li',
|
|
className: 'attachment',
|
|
template: wp.template('attachment'),
|
|
|
|
attributes: function() {
|
|
return {
|
|
'tabIndex': 0,
|
|
'role': 'checkbox',
|
|
'aria-label': this.model.get( 'title' ),
|
|
'aria-checked': false,
|
|
'data-id': this.model.get( 'id' )
|
|
};
|
|
},
|
|
|
|
events: {
|
|
'click .js--select-attachment': 'toggleSelectionHandler',
|
|
'change [data-setting]': 'updateSetting',
|
|
'change [data-setting] input': 'updateSetting',
|
|
'change [data-setting] select': 'updateSetting',
|
|
'change [data-setting] textarea': 'updateSetting',
|
|
'click .close': 'removeFromLibrary',
|
|
'click .check': 'checkClickHandler',
|
|
'click a': 'preventDefault',
|
|
'keydown .close': 'removeFromLibrary',
|
|
'keydown': 'toggleSelectionHandler'
|
|
},
|
|
|
|
buttons: {},
|
|
|
|
initialize: function() {
|
|
var selection = this.options.selection,
|
|
options = _.defaults( this.options, {
|
|
rerenderOnModelChange: true
|
|
} );
|
|
|
|
if ( options.rerenderOnModelChange ) {
|
|
this.listenTo( this.model, 'change', this.render );
|
|
} else {
|
|
this.listenTo( this.model, 'change:percent', this.progress );
|
|
}
|
|
this.listenTo( this.model, 'change:title', this._syncTitle );
|
|
this.listenTo( this.model, 'change:caption', this._syncCaption );
|
|
this.listenTo( this.model, 'change:artist', this._syncArtist );
|
|
this.listenTo( this.model, 'change:album', this._syncAlbum );
|
|
|
|
// Update the selection.
|
|
this.listenTo( this.model, 'add', this.select );
|
|
this.listenTo( this.model, 'remove', this.deselect );
|
|
if ( selection ) {
|
|
selection.on( 'reset', this.updateSelect, this );
|
|
// Update the model's details view.
|
|
this.listenTo( this.model, 'selection:single selection:unsingle', this.details );
|
|
this.details( this.model, this.controller.state().get('selection') );
|
|
}
|
|
|
|
this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave );
|
|
},
|
|
/**
|
|
* @returns {wp.media.view.Attachment} Returns itself to allow chaining
|
|
*/
|
|
dispose: function() {
|
|
var selection = this.options.selection;
|
|
|
|
// Make sure all settings are saved before removing the view.
|
|
this.updateAll();
|
|
|
|
if ( selection ) {
|
|
selection.off( null, null, this );
|
|
}
|
|
/**
|
|
* call 'dispose' directly on the parent class
|
|
*/
|
|
View.prototype.dispose.apply( this, arguments );
|
|
return this;
|
|
},
|
|
/**
|
|
* @returns {wp.media.view.Attachment} Returns itself to allow chaining
|
|
*/
|
|
render: function() {
|
|
var options = _.defaults( this.model.toJSON(), {
|
|
orientation: 'landscape',
|
|
uploading: false,
|
|
type: '',
|
|
subtype: '',
|
|
icon: '',
|
|
filename: '',
|
|
caption: '',
|
|
title: '',
|
|
dateFormatted: '',
|
|
width: '',
|
|
height: '',
|
|
compat: false,
|
|
alt: '',
|
|
description: ''
|
|
}, this.options );
|
|
|
|
options.buttons = this.buttons;
|
|
options.describe = this.controller.state().get('describe');
|
|
|
|
if ( 'image' === options.type ) {
|
|
options.size = this.imageSize();
|
|
}
|
|
|
|
options.can = {};
|
|
if ( options.nonces ) {
|
|
options.can.remove = !! options.nonces['delete'];
|
|
options.can.save = !! options.nonces.update;
|
|
}
|
|
|
|
if ( this.controller.state().get('allowLocalEdits') ) {
|
|
options.allowLocalEdits = true;
|
|
}
|
|
|
|
if ( options.uploading && ! options.percent ) {
|
|
options.percent = 0;
|
|
}
|
|
|
|
this.views.detach();
|
|
this.$el.html( this.template( options ) );
|
|
|
|
this.$el.toggleClass( 'uploading', options.uploading );
|
|
|
|
if ( options.uploading ) {
|
|
this.$bar = this.$('.media-progress-bar div');
|
|
} else {
|
|
delete this.$bar;
|
|
}
|
|
|
|
// Check if the model is selected.
|
|
this.updateSelect();
|
|
|
|
// Update the save status.
|
|
this.updateSave();
|
|
|
|
this.views.render();
|
|
|
|
return this;
|
|
},
|
|
|
|
progress: function() {
|
|
if ( this.$bar && this.$bar.length ) {
|
|
this.$bar.width( this.model.get('percent') + '%' );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object} event
|
|
*/
|
|
toggleSelectionHandler: function( event ) {
|
|
var method;
|
|
|
|
// Don't do anything inside inputs.
|
|
if ( 'INPUT' === event.target.nodeName ) {
|
|
return;
|
|
}
|
|
|
|
// Catch arrow events
|
|
if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
|
|
this.controller.trigger( 'attachment:keydown:arrow', event );
|
|
return;
|
|
}
|
|
|
|
// Catch enter and space events
|
|
if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
// In the grid view, bubble up an edit:attachment event to the controller.
|
|
if ( this.controller.isModeActive( 'grid' ) ) {
|
|
if ( this.controller.isModeActive( 'edit' ) ) {
|
|
// Pass the current target to restore focus when closing
|
|
this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
|
|
return;
|
|
}
|
|
|
|
if ( this.controller.isModeActive( 'select' ) ) {
|
|
method = 'toggle';
|
|
}
|
|
}
|
|
|
|
if ( event.shiftKey ) {
|
|
method = 'between';
|
|
} else if ( event.ctrlKey || event.metaKey ) {
|
|
method = 'toggle';
|
|
}
|
|
|
|
this.toggleSelection({
|
|
method: method
|
|
});
|
|
|
|
this.controller.trigger( 'selection:toggle' );
|
|
},
|
|
/**
|
|
* @param {Object} options
|
|
*/
|
|
toggleSelection: function( options ) {
|
|
var collection = this.collection,
|
|
selection = this.options.selection,
|
|
model = this.model,
|
|
method = options && options.method,
|
|
single, models, singleIndex, modelIndex;
|
|
|
|
if ( ! selection ) {
|
|
return;
|
|
}
|
|
|
|
single = selection.single();
|
|
method = _.isUndefined( method ) ? selection.multiple : method;
|
|
|
|
// If the `method` is set to `between`, select all models that
|
|
// exist between the current and the selected model.
|
|
if ( 'between' === method && single && selection.multiple ) {
|
|
// If the models are the same, short-circuit.
|
|
if ( single === model ) {
|
|
return;
|
|
}
|
|
|
|
singleIndex = collection.indexOf( single );
|
|
modelIndex = collection.indexOf( this.model );
|
|
|
|
if ( singleIndex < modelIndex ) {
|
|
models = collection.models.slice( singleIndex, modelIndex + 1 );
|
|
} else {
|
|
models = collection.models.slice( modelIndex, singleIndex + 1 );
|
|
}
|
|
|
|
selection.add( models );
|
|
selection.single( model );
|
|
return;
|
|
|
|
// If the `method` is set to `toggle`, just flip the selection
|
|
// status, regardless of whether the model is the single model.
|
|
} else if ( 'toggle' === method ) {
|
|
selection[ this.selected() ? 'remove' : 'add' ]( model );
|
|
selection.single( model );
|
|
return;
|
|
} else if ( 'add' === method ) {
|
|
selection.add( model );
|
|
selection.single( model );
|
|
return;
|
|
}
|
|
|
|
// Fixes bug that loses focus when selecting a featured image
|
|
if ( ! method ) {
|
|
method = 'add';
|
|
}
|
|
|
|
if ( method !== 'add' ) {
|
|
method = 'reset';
|
|
}
|
|
|
|
if ( this.selected() ) {
|
|
// If the model is the single model, remove it.
|
|
// If it is not the same as the single model,
|
|
// it now becomes the single model.
|
|
selection[ single === model ? 'remove' : 'single' ]( model );
|
|
} else {
|
|
// If the model is not selected, run the `method` on the
|
|
// selection. By default, we `reset` the selection, but the
|
|
// `method` can be set to `add` the model to the selection.
|
|
selection[ method ]( model );
|
|
selection.single( model );
|
|
}
|
|
},
|
|
|
|
updateSelect: function() {
|
|
this[ this.selected() ? 'select' : 'deselect' ]();
|
|
},
|
|
/**
|
|
* @returns {unresolved|Boolean}
|
|
*/
|
|
selected: function() {
|
|
var selection = this.options.selection;
|
|
if ( selection ) {
|
|
return !! selection.get( this.model.cid );
|
|
}
|
|
},
|
|
/**
|
|
* @param {Backbone.Model} model
|
|
* @param {Backbone.Collection} collection
|
|
*/
|
|
select: function( model, collection ) {
|
|
var selection = this.options.selection,
|
|
controller = this.controller;
|
|
|
|
// Check if a selection exists and if it's the collection provided.
|
|
// If they're not the same collection, bail; we're in another
|
|
// selection's event loop.
|
|
if ( ! selection || ( collection && collection !== selection ) ) {
|
|
return;
|
|
}
|
|
|
|
// Bail if the model is already selected.
|
|
if ( this.$el.hasClass( 'selected' ) ) {
|
|
return;
|
|
}
|
|
|
|
// Add 'selected' class to model, set aria-checked to true.
|
|
this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
|
|
// Make the checkbox tabable, except in media grid (bulk select mode).
|
|
if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
|
|
this.$( '.check' ).attr( 'tabindex', '0' );
|
|
}
|
|
},
|
|
/**
|
|
* @param {Backbone.Model} model
|
|
* @param {Backbone.Collection} collection
|
|
*/
|
|
deselect: function( model, collection ) {
|
|
var selection = this.options.selection;
|
|
|
|
// Check if a selection exists and if it's the collection provided.
|
|
// If they're not the same collection, bail; we're in another
|
|
// selection's event loop.
|
|
if ( ! selection || ( collection && collection !== selection ) ) {
|
|
return;
|
|
}
|
|
this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
|
|
.find( '.check' ).attr( 'tabindex', '-1' );
|
|
},
|
|
/**
|
|
* @param {Backbone.Model} model
|
|
* @param {Backbone.Collection} collection
|
|
*/
|
|
details: function( model, collection ) {
|
|
var selection = this.options.selection,
|
|
details;
|
|
|
|
if ( selection !== collection ) {
|
|
return;
|
|
}
|
|
|
|
details = selection.single();
|
|
this.$el.toggleClass( 'details', details === this.model );
|
|
},
|
|
/**
|
|
* @param {Object} event
|
|
*/
|
|
preventDefault: function( event ) {
|
|
event.preventDefault();
|
|
},
|
|
/**
|
|
* @param {string} size
|
|
* @returns {Object}
|
|
*/
|
|
imageSize: function( size ) {
|
|
var sizes = this.model.get('sizes'), matched = false;
|
|
|
|
size = size || 'medium';
|
|
|
|
// Use the provided image size if possible.
|
|
if ( sizes ) {
|
|
if ( sizes[ size ] ) {
|
|
matched = sizes[ size ];
|
|
} else if ( sizes.large ) {
|
|
matched = sizes.large;
|
|
} else if ( sizes.thumbnail ) {
|
|
matched = sizes.thumbnail;
|
|
} else if ( sizes.full ) {
|
|
matched = sizes.full;
|
|
}
|
|
|
|
if ( matched ) {
|
|
return _.clone( matched );
|
|
}
|
|
}
|
|
|
|
return {
|
|
url: this.model.get('url'),
|
|
width: this.model.get('width'),
|
|
height: this.model.get('height'),
|
|
orientation: this.model.get('orientation')
|
|
};
|
|
},
|
|
/**
|
|
* @param {Object} event
|
|
*/
|
|
updateSetting: function( event ) {
|
|
var $setting = $( event.target ).closest('[data-setting]'),
|
|
setting, value;
|
|
|
|
if ( ! $setting.length ) {
|
|
return;
|
|
}
|
|
|
|
setting = $setting.data('setting');
|
|
value = event.target.value;
|
|
|
|
if ( this.model.get( setting ) !== value ) {
|
|
this.save( setting, value );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pass all the arguments to the model's save method.
|
|
*
|
|
* Records the aggregate status of all save requests and updates the
|
|
* view's classes accordingly.
|
|
*/
|
|
save: function() {
|
|
var view = this,
|
|
save = this._save = this._save || { status: 'ready' },
|
|
request = this.model.save.apply( this.model, arguments ),
|
|
requests = save.requests ? $.when( request, save.requests ) : request;
|
|
|
|
// If we're waiting to remove 'Saved.', stop.
|
|
if ( save.savedTimer ) {
|
|
clearTimeout( save.savedTimer );
|
|
}
|
|
|
|
this.updateSave('waiting');
|
|
save.requests = requests;
|
|
requests.always( function() {
|
|
// If we've performed another request since this one, bail.
|
|
if ( save.requests !== requests ) {
|
|
return;
|
|
}
|
|
|
|
view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
|
|
save.savedTimer = setTimeout( function() {
|
|
view.updateSave('ready');
|
|
delete save.savedTimer;
|
|
}, 2000 );
|
|
});
|
|
},
|
|
/**
|
|
* @param {string} status
|
|
* @returns {wp.media.view.Attachment} Returns itself to allow chaining
|
|
*/
|
|
updateSave: function( status ) {
|
|
var save = this._save = this._save || { status: 'ready' };
|
|
|
|
if ( status && status !== save.status ) {
|
|
this.$el.removeClass( 'save-' + save.status );
|
|
save.status = status;
|
|
}
|
|
|
|
this.$el.addClass( 'save-' + save.status );
|
|
return this;
|
|
},
|
|
|
|
updateAll: function() {
|
|
var $settings = this.$('[data-setting]'),
|
|
model = this.model,
|
|
changed;
|
|
|
|
changed = _.chain( $settings ).map( function( el ) {
|
|
var $input = $('input, textarea, select, [value]', el ),
|
|
setting, value;
|
|
|
|
if ( ! $input.length ) {
|
|
return;
|
|
}
|
|
|
|
setting = $(el).data('setting');
|
|
value = $input.val();
|
|
|
|
// Record the value if it changed.
|
|
if ( model.get( setting ) !== value ) {
|
|
return [ setting, value ];
|
|
}
|
|
}).compact().object().value();
|
|
|
|
if ( ! _.isEmpty( changed ) ) {
|
|
model.save( changed );
|
|
}
|
|
},
|
|
/**
|
|
* @param {Object} event
|
|
*/
|
|
removeFromLibrary: function( event ) {
|
|
// Catch enter and space events
|
|
if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
|
|
return;
|
|
}
|
|
|
|
// Stop propagation so the model isn't selected.
|
|
event.stopPropagation();
|
|
|
|
this.collection.remove( this.model );
|
|
},
|
|
|
|
/**
|
|
* Add the model if it isn't in the selection, if it is in the selection,
|
|
* remove it.
|
|
*
|
|
* @param {[type]} event [description]
|
|
* @return {[type]} [description]
|
|
*/
|
|
checkClickHandler: function ( event ) {
|
|
var selection = this.options.selection;
|
|
if ( ! selection ) {
|
|
return;
|
|
}
|
|
event.stopPropagation();
|
|
if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
|
|
selection.remove( this.model );
|
|
// Move focus back to the attachment tile (from the check).
|
|
this.$el.focus();
|
|
} else {
|
|
selection.add( this.model );
|
|
}
|
|
}
|
|
});
|
|
|
|
// Ensure settings remain in sync between attachment views.
|
|
_.each({
|
|
caption: '_syncCaption',
|
|
title: '_syncTitle',
|
|
artist: '_syncArtist',
|
|
album: '_syncAlbum'
|
|
}, function( method, setting ) {
|
|
/**
|
|
* @param {Backbone.Model} model
|
|
* @param {string} value
|
|
* @returns {wp.media.view.Attachment} Returns itself to allow chaining
|
|
*/
|
|
Attachment.prototype[ method ] = function( model, value ) {
|
|
var $setting = this.$('[data-setting="' + setting + '"]');
|
|
|
|
if ( ! $setting.length ) {
|
|
return this;
|
|
}
|
|
|
|
// If the updated value is in sync with the value in the DOM, there
|
|
// is no need to re-render. If we're currently editing the value,
|
|
// it will automatically be in sync, suppressing the re-render for
|
|
// the view we're editing, while updating any others.
|
|
if ( value === $setting.find('input, textarea, select, [value]').val() ) {
|
|
return this;
|
|
}
|
|
|
|
return this.render();
|
|
};
|
|
});
|
|
|
|
module.exports = Attachment;
|