WordPress/wp-includes/js/media-views.js
Daryl Koopersmith e14d191fb6 Media JS: Use a custom Attachment views for both the media library and gallery screens.
To do so, the Attachment view code had to be shifted above the Workspace view code to ensure the subview is defined (preventing errors).

Also corrects an error in `wp.media.view.Attachments.add` where it was still using the default Attachment view.

see #21390, #21809.


git-svn-id: http://core.svn.wordpress.org/trunk@22046 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2012-09-27 06:53:54 +00:00

830 lines
21 KiB
JavaScript

(function($){
var media = wp.media,
Attachment = media.model.Attachment,
Attachments = media.model.Attachments,
Query = media.model.Query,
l10n;
// Link any localized strings.
l10n = media.view.l10n = _.isUndefined( _wpMediaViewsL10n ) ? {} : _wpMediaViewsL10n;
/**
* ========================================================================
* CONTROLLERS
* ========================================================================
*/
/**
* wp.media.controller.Workflow
*/
media.controller.Workflow = Backbone.Model.extend({
defaults: {
title: '',
multiple: false,
view: 'library',
library: {},
selection: []
},
initialize: function() {
this.createSelection();
// Initialize view storage.
this._views = {};
this._pendingViews = {};
// Initialize modal container view.
this.modal = new media.view.Modal({ controller: this });
// Add default views.
//
// Use the `library` property to initialize the corresponding view,
// then unset the property.
this.add( 'library', media.view.Workspace.Library, {
collection: media.query( this.get('library') )
} );
this.unset('library');
// Add the gallery view.
this.add( 'gallery', media.view.Workspace.Gallery, { collection: this.selection } );
},
// Registers a view.
//
// `id` is a unique ID for the view relative to the workflow instance.
// `constructor` is a `Backbone.View` constructor. `options` are the
// options to be passed when the view is initialized.
//
// Triggers the `add` and `add:VIEW_ID` events.
add: function( id, constructor, options ) {
this.remove( id );
this._pendingViews[ id ] = {
view: constructor,
options: options
};
this.trigger( 'add add:' + id, constructor, options );
return this;
},
// Returns a registered view instance. If an `id` is not provided,
// it will return the active view.
//
// Lazily instantiates a registered view.
//
// Triggers the `init` and `init:VIEW_ID` events.
view: function( id ) {
var pending;
id = id || this.get('view');
pending = this._pendingViews[ id ];
if ( ! this._views[ id ] && pending ) {
this._views[ id ] = new pending.view( _.extend({ controller: this }, pending.options || {} ) );
delete this._pendingViews[ id ];
this.trigger( 'init init:' + id, this._views[ id ] );
}
return this._views[ id ];
},
// Unregisters a view from the workflow.
//
// Triggers the `remove` and `remove:VIEW_ID` events.
remove: function( id ) {
delete this._views[ id ];
delete this._pendingViews[ id ];
this.trigger( 'remove remove:' + id );
return this;
},
// Renders a view and places it within the modal window.
// Automatically adds a view if `constructor` is provided.
render: function( id, constructor, options ) {
var view;
id = id || this.get('view');
if ( constructor )
this.add( id, constructor, options );
view = this.view( id );
if ( ! view )
return this;
view.render();
this.modal.content( view );
return this;
},
update: function() {
this.close();
this.trigger( 'update', this.selection );
this.selection.clear();
},
createSelection: function() {
var controller = this;
// Initialize workflow-specific models.
// Use the `selection` property to initialize the Attachments
// collection, then unset the property.
this.selection = new Attachments( this.get('selection') );
this.unset('selection');
_.extend( this.selection, {
// Override the selection's add method.
// If the workflow does not support multiple
// selected attachments, reset the selection.
add: function( models, options ) {
if ( ! controller.get('multiple') ) {
models = _.isArray( models ) ? _.first( models ) : models;
this.clear( options );
}
return Attachments.prototype.add.call( this, models, options );
},
// Removes all models from the selection.
clear: function( options ) {
return this.remove( this.models, options );
},
// Override the selection's reset method.
// Always direct items through add and remove,
// as we need them to fire.
reset: function( models, options ) {
return this.clear( options ).add( models, options );
},
// Create selection.has, which determines if a model
// exists in the collection based on cid and id,
// instead of direct comparison.
has: function( attachment ) {
return !! ( this.getByCid( attachment.cid ) || this.get( attachment.id ) );
}
});
}
});
// Map modal methods to the workflow.
_.each(['attach','detach','open','close'], function( method ) {
media.controller.Workflow.prototype[ method ] = function() {
this.modal[ method ].apply( this.modal, arguments );
return this;
};
});
/**
* ========================================================================
* VIEWS
* ========================================================================
*/
/**
* wp.media.view.Modal
*/
media.view.Modal = Backbone.View.extend({
tagName: 'div',
template: media.template('media-modal'),
events: {
'click .media-modal-backdrop, .media-modal-close' : 'closeHandler'
},
initialize: function() {
this.controller = this.options.controller;
this.controller.on( 'change:title', this.render, this );
_.defaults( this.options, {
container: document.body
});
},
render: function() {
// Ensure content div exists.
this.options.$content = this.options.$content || $('<div />');
// Detach the content element from the DOM to prevent
// `this.$el.html()` from garbage collecting its events.
this.options.$content.detach();
this.$el.html( this.template( this.controller.toJSON() ) );
this.$('.media-modal-content').append( this.options.$content );
return this;
},
attach: function() {
this.$el.appendTo( this.options.container );
},
detach: function() {
this.$el.detach();
},
open: function() {
this.$el.show();
},
close: function() {
this.$el.hide();
},
closeHandler: function( event ) {
event.preventDefault();
this.close();
},
content: function( $content ) {
// Detach any existing content to prevent events from being lost.
if ( this.options.$content )
this.options.$content.detach();
// Set and render the content.
this.options.$content = ( $content instanceof Backbone.View ) ? $content.$el : $content;
return this.render();
}
});
/**
* wp.media.view.Toolbar
*/
media.view.Toolbar = Backbone.View.extend({
tagName: 'div',
className: 'media-toolbar',
initialize: function() {
this._views = {};
this.$primary = $('<div class="media-toolbar-primary" />').prependTo( this.$el );
this.$secondary = $('<div class="media-toolbar-secondary" />').prependTo( this.$el );
if ( this.options.items ) {
_.each( this.options.items, function( view, id ) {
this.add( id, view, { silent: true } );
}, this );
this.render();
}
},
render: function() {
var views = _.chain( this._views ).sortBy( function( view ) {
return view.options.priority || 10;
}).groupBy( function( view ) {
return ( view.options.priority || 10 ) > 0 ? 'primary' : 'secondary';
}).value();
// Make sure to detach the elements we want to reuse.
// Otherwise, `jQuery.html()` will unbind their events.
$( _.pluck( this._views, 'el' ) ).detach();
this.$primary.html( _.pluck( views.primary, 'el' ) );
this.$secondary.html( _.pluck( views.secondary, 'el' ) );
return this;
},
add: function( id, view, options ) {
if ( ! ( view instanceof Backbone.View ) ) {
view.classes = [ id ].concat( view.classes || [] );
view = new media.view.Button( view ).render();
}
this._views[ id ] = view;
if ( ! options || ! options.silent )
this.render();
return this;
},
get: function( id ) {
return this._views[ id ];
},
remove: function( id, options ) {
delete this._views[ id ];
if ( ! options || ! options.silent )
this.render();
return this;
}
});
/**
* wp.media.view.Button
*/
media.view.Button = Backbone.View.extend({
tagName: 'a',
className: 'media-button',
attributes: { href: '#' },
events: {
'click': 'click'
},
defaults: {
text: '',
style: '',
size: 'large'
},
initialize: function() {
// Create a model with the provided `defaults`.
this.model = new Backbone.Model( this.defaults );
// If any of the `options` have a key from `defaults`, apply its
// value to the `model` and remove it from the `options object.
_.each( this.defaults, function( def, key ) {
var value = this.options[ key ];
if ( _.isUndefined( value ) )
return;
this.model.set( key, value );
delete this.options[ key ];
}, this );
this.model.on( 'change', this.render, this );
},
render: function() {
var classes = [ 'button', this.className ];
if ( this.model.get('style') )
classes.push( 'button-' + this.model.get('style') );
if ( this.model.get('size') )
classes.push( 'button-' + this.model.get('size') );
classes = classes.concat( this.options.classes );
this.el.className = classes.join(' ');
this.$el.text( this.model.get('text') );
return this;
},
click: function( event ) {
event.preventDefault();
if ( this.options.click )
this.options.click.apply( this, arguments );
}
});
/**
* wp.media.view.Attachment
*/
media.view.Attachment = Backbone.View.extend({
tagName: 'li',
className: 'attachment',
template: media.template('attachment'),
events: {
'click': 'toggleSelection'
},
initialize: function() {
this.controller = this.options.controller;
this.model.on( 'change:sizes change:uploading', this.render, this );
this.model.on( 'change:percent', this.progress, this );
this.model.on( 'add', this.select, this );
this.model.on( 'remove', this.deselect, this );
},
render: function() {
var attachment = this.model.toJSON(),
options = {
thumbnail: 'image' === attachment.type ? attachment.url : attachment.icon,
uploading: attachment.uploading,
orientation: attachment.orientation || 'landscape',
type: attachment.type,
subtype: attachment.subtype
};
// Use the medium image size if possible. If the medium size
// doesn't exist, then the attachment is too small.
// In that case, use the attachment itself.
if ( attachment.sizes && attachment.sizes.medium ) {
options.orientation = attachment.sizes.medium.orientation;
options.thumbnail = attachment.sizes.medium.url;
}
this.$el.html( this.template( options ) );
if ( attachment.uploading )
this.$bar = this.$('.media-progress-bar div');
else
delete this.$bar;
// Check if the model is selected.
if ( this.controller.selection.has( this.model ) )
this.select();
return this;
},
progress: function() {
if ( this.$bar && this.$bar.length )
this.$bar.width( this.model.get('percent') + '%' );
},
toggleSelection: function( event ) {
var selection = this.controller.selection;
selection[ selection.has( this.model ) ? 'remove' : 'add' ]( this.model );
},
select: function( model, collection ) {
// If a collection is provided, check if it's the selection.
// If it's not, bail; we're in another selection's event loop.
if ( collection && collection !== this.controller.selection )
return;
this.$el.addClass('selected');
},
deselect: function( model, collection ) {
// If a collection is provided, check if it's the selection.
// If it's not, bail; we're in another selection's event loop.
if ( collection && collection !== this.controller.selection )
return;
this.$el.removeClass('selected');
}
});
/**
* wp.media.view.Attachment.Library
*/
media.view.Attachment.Library = media.view.Attachment.extend({
className: 'attachment library'
});
/**
* wp.media.view.Attachment.Gallery
*/
media.view.Attachment.Gallery = media.view.Attachment.extend({
events: {}
});
/**
* wp.media.view.Workspace
*/
media.view.Workspace = Backbone.View.extend({
tagName: 'div',
className: 'media-workspace',
template: media.template('media-workspace'),
// The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment,
events: {
'dragenter': 'maybeInitUploader',
'mouseenter': 'maybeInitUploader'
},
initialize: function() {
this.controller = this.options.controller;
_.defaults( this.options, {
selectOne: false,
uploader: {}
});
this.$content = $('<div class="existing-attachments" />');
this.attachmentsView = new media.view.Attachments({
controller: this.controller,
directions: this.controller.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular,
collection: this.collection,
AttachmentView: this.AttachmentView
});
this.$content.append( this.attachmentsView.$el );
// Track uploading attachments.
wp.Uploader.queue.on( 'add remove reset change:percent', this.renderUploadProgress, this );
wp.Uploader.queue.on( 'add', this.selectUpload, this );
},
render: function() {
this.$content.detach();
this.attachmentsView.render();
this.renderUploadProgress();
this.$el.html( this.template( this.options ) ).append( this.$content );
this.$bar = this.$('.upload-attachments .media-progress-bar div');
return this;
},
maybeInitUploader: function() {
var workspace = this;
// If the uploader already exists or the body isn't in the DOM, bail.
if ( this.uploader || ! this.$el.closest('body').length )
return;
this.uploader = new wp.Uploader( _.extend({
container: this.$el,
dropzone: this.$el,
browser: this.$('.upload-attachments a')
}, this.options.uploader ) );
},
selectUpload: function( attachment ) {
this.controller.selection.add( attachment );
},
renderUploadProgress: function() {
var queue = wp.Uploader.queue;
this.$el.toggleClass( 'uploading', !! queue.length );
if ( ! this.$bar || ! queue.length )
return;
this.$bar.width( ( queue.reduce( function( memo, attachment ) {
if ( attachment.get('uploading') )
return memo + ( attachment.get('percent') || 0 );
else
return memo + 100;
}, 0 ) / queue.length ) + '%' );
}
});
/**
* wp.media.view.Workspace.Library
*/
media.view.Workspace.Library = media.view.Workspace.extend({
// The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment.Library,
initialize: function() {
media.view.Workspace.prototype.initialize.apply( this, arguments );
// If this supports multiple attachments, initialize the sample toolbar view.
if ( this.controller.get('multiple') )
this.initToolbarView();
},
// Initializes the toolbar view. Currently uses defaults set for
// inserting media into a post. This should be pulled out into the
// appropriate workflow when the time comes, but is currently here
// to test multiple selections.
initToolbarView: function() {
var controller = this.controller;
this.toolbarView = new media.view.Toolbar({
items: {
'selection-preview': new media.view.SelectionPreview({
controller: this.controller,
collection: this.controller.selection,
priority: -40
}),
'create-new-gallery': {
style: 'primary',
text: l10n.createNewGallery,
priority: 40,
click: function() {
controller.render('gallery');
}
},
'insert-into-post': {
text: l10n.insertIntoPost,
priority: 30,
click: _.bind( controller.update, controller )
},
'add-to-gallery': {
text: l10n.addToGallery,
priority: 20
}
}
});
this.controller.selection.on( 'add remove', function() {
var count = this.controller.selection.length,
showGallery;
this.$el.toggleClass( 'with-toolbar', !! count );
// Check if every attachment in the selection is an image.
showGallery = count > 1 && this.controller.selection.all( function( attachment ) {
return 'image' === attachment.get('type');
});
this.toolbarView.get('create-new-gallery').$el.toggle( showGallery );
insert = this.toolbarView.get('insert-into-post');
insert.model.set( 'style', showGallery ? '' : 'primary' );
}, this );
this.$content.append( this.toolbarView.$el );
}
});
/**
* wp.media.view.Workspace.Gallery
*/
media.view.Workspace.Gallery = media.view.Workspace.extend({
// The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment.Gallery,
initialize: function() {
media.view.Workspace.prototype.initialize.apply( this, arguments );
this.initToolbarView();
},
// Initializes the toolbar view. Currently uses defaults set for
// inserting media into a post. This should be pulled out into the
// appropriate workflow when the time comes, but is currently here
// to test multiple selections.
initToolbarView: function() {
var controller = this.controller;
this.toolbarView = new media.view.Toolbar({
items: {
'return-to-library': {
text: l10n.returnToLibrary,
priority: -40,
click: function() {
controller.render('library');
}
},
'insert-gallery-into-post': {
style: 'primary',
text: l10n.insertGalleryIntoPost,
priority: 40,
click: _.bind( controller.update, controller )
},
'add-images-from-library': {
text: l10n.addImagesFromLibrary,
priority: 30
}
}
});
this.$el.addClass('with-toolbar');
this.$content.append( this.toolbarView.$el );
}
});
/**
* wp.media.view.Attachments
*/
media.view.Attachments = Backbone.View.extend({
tagName: 'div',
className: 'attachments',
template: media.template('attachments'),
events: {
'keyup input': 'search'
},
initialize: function() {
this.controller = this.options.controller;
_.defaults( this.options, {
refreshSensitivity: 200,
refreshThreshold: 3,
AttachmentView: media.view.Attachment
});
_.each(['add','remove'], function( method ) {
this.collection.on( method, function( attachment, attachments, options ) {
this[ method ]( attachment, options.index );
}, this );
}, this );
this.collection.on( 'reset', this.refresh, this );
this.$list = $('<ul />');
this.list = this.$list[0];
this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
this.$list.on( 'scroll.attachments', this.scroll );
},
render: function() {
// Detach the list from the DOM to prevent event removal.
this.$list.detach();
this.$el.html( this.template( this.options ) ).append( this.$list );
this.refresh();
return this;
},
refresh: function() {
// If there are no elements, load some.
if ( ! this.collection.length ) {
this.collection.more();
this.$list.empty();
return this;
}
// Otherwise, create all of the Attachment views, and replace
// the list in a single DOM operation.
this.$list.html( this.collection.map( function( attachment ) {
return new this.options.AttachmentView({
controller: this.controller,
model: attachment
}).render().$el;
}, this ) );
// Then, trigger the scroll event to check if we're within the
// threshold to query for additional attachments.
this.scroll();
return this;
},
add: function( attachment, index ) {
var view, children;
view = new this.options.AttachmentView({
controller: this.controller,
model: attachment
}).render();
children = this.$list.children();
if ( children.length > index )
children.eq( index ).before( view.$el );
else
this.$list.append( view.$el );
},
remove: function( attachment, index ) {
var children = this.$list.children();
if ( children.length )
children.eq( index ).detach();
},
scroll: function( event ) {
// @todo: is this still necessary?
if ( ! this.$list.is(':visible') )
return;
if ( this.list.scrollHeight < this.list.scrollTop + ( this.list.clientHeight * this.options.refreshThreshold ) ) {
this.collection.more();
}
},
search: function( event ) {
var props = this.collection.props;
if ( event.target.value )
props.set( 'search', event.target.value );
else
props.unset('search');
}
});
/**
* wp.media.view.SelectionPreview
*/
media.view.SelectionPreview = Backbone.View.extend({
tagName: 'div',
className: 'selection-preview',
template: media.template('media-selection-preview'),
events: {
'click .clear-selection': 'clear'
},
initialize: function() {
this.controller = this.options.controller;
this.collection.on( 'add change:url remove', this.render, this );
this.render();
},
render: function() {
var options = {},
first, sizes, amount;
// If nothing is selected, display nothing.
if ( ! this.collection.length ) {
this.$el.empty();
return this;
}
options.count = this.collection.length;
first = this.collection.first();
sizes = first.get('sizes');
if ( 'image' === first.get('type') )
options.thumbnail = ( sizes && sizes.thumbnail ) ? sizes.thumbnail.url : first.get('url');
else
options.thumbnail = first.get('icon');
this.$el.html( this.template( options ) );
return this;
},
clear: function( event ) {
event.preventDefault();
this.collection.clear();
}
});
}(jQuery));