diff --git a/wp-admin/edit-form-advanced.php b/wp-admin/edit-form-advanced.php index 5a21755051..d2ba3e241c 100644 --- a/wp-admin/edit-form-advanced.php +++ b/wp-admin/edit-form-advanced.php @@ -18,6 +18,10 @@ if ( wp_is_mobile() ) if ( post_type_supports($post_type, 'editor') || post_type_supports($post_type, 'thumbnail') ) { add_thickbox(); wp_enqueue_script('media-upload'); + wp_enqueue_script( 'media-views' ); + wp_enqueue_style( 'media-views' ); + wp_plupload_default_settings(); + add_action( 'admin_footer', 'wp_print_media_templates' ); } /** diff --git a/wp-includes/css/media-views.css b/wp-includes/css/media-views.css new file mode 100644 index 0000000000..de1b4ed34e --- /dev/null +++ b/wp-includes/css/media-views.css @@ -0,0 +1,278 @@ +/** + * Modal + */ +.media-modal { + position: fixed; + top: 60px; + left: 60px; + right: 60px; + bottom: 60px; + background: #fff; + z-index: 125000; +} + +.media-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #000; + opacity: 0.8; + z-index: 120000; +} + +.media-modal-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 28px; + background: #f1f1f1; +} + +.media-modal-header h3 { + float: left; + padding: 0 0 0 10px; + margin: 0; + line-height: 28px; +} + +.media-modal-close { + float: right; + padding-right: 10px; + line-height: 28px; +} + +.media-modal-content { + position: absolute; + top: 28px; + left: 0; + right: 0; + bottom: 0; + overflow: auto; +} + +/** + * Workspace + */ +.media-workspace { + position: relative; + width: 100%; + height: 100%; +} + +.upload-attachments { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 180px; + margin: 10px; + text-align: center; + border: 3px dashed #dfdfdf; + background: #fff; + z-index: 100; +} + +.upload-attachments h3 { + font-size: 18px; + font-weight: 200; + color: #777; + padding: 40px 0 0; + margin: 0; +} + +.upload-attachments span { + display: block; + color: #777; + margin: 10px 0; +} + +.upload-attachments a { + display: inline-block; + margin: 0 auto; +} + +.drag-over .upload-attachments { + width: auto; + right: 0; + border-color: #83B4D8; +} + +.existing-attachments { + position: absolute; + top: 0; + left: 200px; + right: 0; + bottom: 0; +} + +/** + * Attachments + */ +.attachments { + width: 100%; + height: 100%; +} + +.attachments-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50px; + background: #fff; +} + +.attachments-header h3 { + float: left; + margin: 0; + padding: 0 0 0 10px; + line-height: 50px; + font-size: 18px; + font-weight: 200; +} +.attachments-header input { + float: right; + margin-top: 10px; + margin-right: 10px; +} + +.attachments ul { + position: absolute; + top: 50px; + left: 0; + right: 0; + bottom: 0; + overflow: auto; + margin: 0 10px 20px; +} + +/** + * Attachment + */ +.attachment { + position: relative; + float: left; + width: 200px; + height: 200px; + + padding: 0; + margin: 0 10px 20px; + border: 1px solid #dfdfdf; +} + +.attachment:hover { + border-color: #d54e21; +} + +.attachment.selected { + border-color: #21759b; +} + +.attachment-thumbnail { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +.attachment img { + display: block; + max-height: 200px; + max-width: 200px; + + /* Vertically center the thumbnails. */ + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate( -50%, -50% ); + -moz-transform: translate( -50%, -50% ); + -ms-transform: translate( -50%, -50% ); + -o-transform: translate( -50%, -50% ); + transform: translate( -50%, -50% ); + + margin: 0 auto; + box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.4 ); +} + + +/* Square crop with overflow visible on hover. */ +/* +.attachment .portrait img { + width: 200px; +} +.attachment .landscape img { + height: 200px; +} +.attachment .attachment-thumbnail:hover { + overflow: visible; + z-index: 1000; +} +.attachment .attachment-thumbnail:hover img { + border: 10px solid #fff; + box-shadow: 0 0 10px rgba( 0, 0, 0, 0.4 ); +}*/ + + +/* Square crop with resized image on hover. */ +/* +.attachment .portrait img { + width: 200px; +} +.attachment .landscape img { + height: 200px; +} +.attachment .attachment-thumbnail:hover img { + height: auto; + width: auto; + max-height: 200px; + max-width: 200px; +}*/ + + +/** + * Progress Bar + */ +.media-progress-bar { + position: relative; + height: 8px; + width: 70%; + margin: 10px auto; + padding: 2px; + border: 2px solid #dfdfdf; + border-radius: 8px; +} + +.media-progress-bar div { + height: 8px; + min-width: 8px; + width: 0; + background: #dfdfdf; + border-radius: 10px; + -webkit-transition: width 200ms; + -moz-transition: width 200ms; + -ms-transition: width 200ms; + -o-transition: width 200ms; + transition: width 200ms; +} + +.attachment-thumbnail .media-progress-bar { + position: absolute; + top: 50%; + left: 15%; + width: 70%; + margin: -8px 0 0 -4px; +} + +.upload-attachments .media-progress-bar { + margin-top: 80px; + display: none; +} + +.uploading .upload-attachments .media-progress-bar { + display: block; +} \ No newline at end of file diff --git a/wp-includes/css/media-views.min.css b/wp-includes/css/media-views.min.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js new file mode 100644 index 0000000000..8f10513c28 --- /dev/null +++ b/wp-includes/js/media-models.js @@ -0,0 +1,410 @@ +if ( typeof wp === 'undefined' ) + var wp = {}; + +(function($){ + var media = wp.media = { model: {}, view: {}, controller: {} }, + Attachment, Attachments, Query; + + /** + * ======================================================================== + * UTILITIES + * ======================================================================== + */ + + _.extend( media, { + /** + * media.template( id ) + * + * Fetches a template by id. + * + * @param {string} id A string that corresponds to a DOM element with an id prefixed with "tmpl-". + * For example, "attachment" maps to "tmpl-attachment". + * @return {function} A function that lazily-compiles the template requested. + */ + template: _.memoize( function( id ) { + var compiled; + return function( data ) { + compiled = compiled || _.template( $( '#tmpl-' + id ).html() ); + return compiled( data ); + }; + }), + + /** + * media.post( [action], [data] ) + * + * Sends a POST request to WordPress. + * + * @param {string} action The slug of the action to fire in WordPress. + * @param {object} data The data to populate $_POST with. + * @return {$.promise} A jQuery promise that represents the request. + */ + post: function( action, data ) { + return media.ajax({ + data: _.isObject( action ) ? action : _.extend( data || {}, { action: action }) + }); + }, + + /** + * media.ajax( [action], [options] ) + * + * Sends a POST request to WordPress. + * + * @param {string} action The slug of the action to fire in WordPress. + * @param {object} options The options passed to jQuery.ajax. + * @return {$.promise} A jQuery promise that represents the request. + */ + ajax: function( action, options ) { + if ( _.isObject( action ) ) { + options = action; + } else { + options = options || {}; + options.data = _.extend( options.data || {}, { action: action }); + } + + options = _.defaults( options || {}, { + type: 'POST', + url: ajaxurl, + context: this + }); + + return $.Deferred( function( deferred ) { + // Transfer success/error callbacks. + if ( options.success ) + deferred.done( options.success ); + if ( options.error ) + deferred.fail( options.error ); + + delete options.success; + delete options.error; + + // Use with PHP's wp_send_json_success() and wp_send_json_error() + $.ajax( options ).done( function( response ) { + if ( _.isObject( response ) && ! _.isUndefined( response.success ) ) + deferred[ response.success ? 'resolveWith' : 'rejectWith' ]( this, [response.data] ); + else + deferred.rejectWith( this, [response] ); + }).fail( function() { + deferred.rejectWith( this, arguments ); + }); + }).promise(); + } + }); + + + /** + * ======================================================================== + * MODELS + * ======================================================================== + */ + + /** + * wp.media.model.Attachment + */ + Attachment = media.model.Attachment = Backbone.Model.extend({ + sync: function( method, model, options ) { + // Overload the read method so Attachment.fetch() functions correctly. + if ( 'read' === method ) { + options = options || {}; + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'get-attachment', + id: this.id + }); + return media.ajax( options ); + + // Otherwise, fall back to Backbone.sync() + } else { + return Backbone.sync.apply( this, arguments ); + } + }, + + parse: function( resp, xhr ) { + // Convert date strings into Date objects. + resp.date = new Date( resp.date ); + resp.modified = new Date( resp.modified ); + return resp; + } + }, { + create: function( attrs ) { + return Attachments.all.push( attrs ); + }, + + get: _.memoize( function( id, attachment ) { + return Attachments.all.push( attachment || { id: id } ); + }) + }); + + /** + * wp.media.model.Attachments + */ + Attachments = media.model.Attachments = Backbone.Collection.extend({ + model: Attachment, + + initialize: function( models, options ) { + options = options || {}; + + this.filters = options.filters || {}; + + if ( options.watch ) + this.watch( options.watch ); + + if ( options.mirror ) + this.mirror( options.mirror ); + }, + + validate: function( attachment ) { + return _.all( this.filters, function( filter ) { + return !! filter.call( this, attachment ); + }, this ); + }, + + changed: function( attachment, options ) { + + if ( this.validate( attachment ) ) + this.add( attachment ); + else + this.remove( attachment ); + return this; + }, + + watch: function( attachments ) { + attachments.on( 'add change', this.changed, this ); + }, + + unwatch: function( attachments ) { + attachments.off( 'add change', this.changed, this ); + }, + + mirror: function( attachments ) { + if ( this.mirroring && this.mirroring === attachments ) + return; + + this.unmirror(); + this.mirroring = attachments; + this.reset( attachments.models ); + attachments.on( 'add', this._mirrorAdd, this ); + attachments.on( 'remove', this._mirrorRemove, this ); + attachments.on( 'reset', this._mirrorReset, this ); + }, + + unmirror: function() { + if ( ! this.mirroring ) + return; + + this.mirroring.off( 'add', this._mirrorAdd, this ); + this.mirroring.off( 'remove', this._mirrorRemove, this ); + this.mirroring.off( 'reset', this._mirrorReset, this ); + delete this.mirroring; + }, + + _mirrorAdd: function( attachment, attachments, options ) { + this.add( attachment, { at: options.index }); + }, + + _mirrorRemove: function( attachment ) { + this.remove( attachment ); + }, + + _mirrorReset: function( attachments ) { + this.reset( attachments.models ); + }, + + more: function( options ) { + if ( this.mirroring && this.mirroring.more ) + return this.mirroring.more( options ); + }, + + parse: function( resp, xhr ) { + return _.map( resp, function( attrs ) { + var attachment = Attachment.get( attrs.id ); + return attachment.set( attachment.parse( attrs, xhr ) ); + }); + } + }); + + Attachments.all = new Attachments(); + + /** + * wp.media.query + */ + media.query = (function(){ + var queries = []; + + return function( args, options ) { + args = _.defaults( args || {}, Query.defaultArgs ); + + var query = _.find( queries, function( query ) { + return _.isEqual( query.args, args ); + }); + + if ( ! query ) { + query = new Query( [], _.extend( options || {}, { args: args } ) ); + queries.push( query ); + } + + return query; + }; + }()); + + /** + * wp.media.model.Query + * + * A set of attachments that corresponds to a set of consecutively paged + * queries on the server. + * + * Note: Do NOT change this.args after the query has been initialized. + * Things will break. + */ + Query = media.model.Query = Attachments.extend({ + initialize: function( models, options ) { + var orderby, + defaultArgs = Query.defaultArgs; + + options = options || {}; + Attachments.prototype.initialize.apply( this, arguments ); + + // Generate this.args. Don't mess with them. + this.args = _.defaults( options.args || {}, defaultArgs ); + + // Normalize the order. + this.args.order = this.args.order.toUpperCase(); + if ( 'DESC' !== this.args.order && 'ASC' !== this.args.order ) + this.args.order = defaultArgs.order.toUpperCase(); + + // Set allowed orderby values. + // These map directly to attachment keys in most scenarios. + // Exceptions are specified in orderby.keymap. + orderby = { + allowed: [ 'name', 'author', 'date', 'title', 'modified', 'parent', 'ID' ], + keymap: { + 'ID': 'id', + 'parent': 'uploadedTo' + } + }; + + if ( ! _.contains( orderby.allowed, this.args.orderby ) ) + this.args.orderby = defaultArgs.orderby; + this.orderkey = orderby.keymap[ this.args.orderby ] || this.args.orderby; + + this.hasMore = true; + this.created = new Date(); + + this.filters.order = function( attachment ) { + // We want any items that can be placed before the last + // item in the set. If we add any items after the last + // item, then we can't guarantee the set is complete. + if ( this.length ) { + return 1 !== this.comparator( attachment, this.last() ); + + // Handle the case where there are no items yet and + // we're sorting for recent items. In that case, we want + // changes that occurred after we created the query. + } else if ( 'DESC' === this.args.order && ( 'date' === this.orderkey || 'modified' === this.orderkey ) ) { + return attachment.get( this.orderkey ) >= this.created; + } + + // Otherwise, we don't want any items yet. + return false; + }; + + if ( this.args.s ) { + // Note that this client-side searching is *not* equivalent + // to our server-side searching. + this.filters.search = function( attachment ) { + return _.any(['title','filename','description','caption','name'], function( key ) { + var value = attachment.get( key ); + return value && -1 !== value.search( this.args.s ); + }, this ); + }; + } + + this.watch( Attachments.all ); + }, + + more: function( options ) { + var query = this; + + if ( ! this.hasMore ) + return; + + options = options || {}; + options.add = true; + + return this.fetch( options ).done( function( resp ) { + if ( _.isEmpty( resp ) || resp.length < this.args.posts_per_page ) + query.hasMore = false; + }); + }, + + sync: function( method, model, options ) { + var fallback; + + // Overload the read method so Attachment.fetch() functions correctly. + if ( 'read' === method ) { + options = options || {}; + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'query-attachments' + }); + + // Clone the args so manipulation is non-destructive. + args = _.clone( this.args ); + + // Determine which page to query. + args.paged = Math.floor( this.length / args.posts_per_page ) + 1; + + options.data.query = args; + return media.ajax( options ); + + // Otherwise, fall back to Backbone.sync() + } else { + fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; + return fallback.sync.apply( this, arguments ); + } + }, + + comparator: (function(){ + /** + * A basic comparator. + * + * @param {mixed} a The primary parameter to compare. + * @param {mixed} b The primary parameter to compare. + * @param {string} ac The fallback parameter to compare, a's cid. + * @param {string} bc The fallback parameter to compare, b's cid. + * @return {number} -1: a should come before b. + * 0: a and b are of the same rank. + * 1: b should come before a. + */ + var compare = function( a, b, ac, bc ) { + if ( _.isEqual( a, b ) ) + return ac === bc ? 0 : (ac > bc ? -1 : 1); + else + return a > b ? -1 : 1; + }; + + return function( a, b ) { + var key = this.orderkey, + order = this.args.order, + ac = a.cid, + bc = b.cid; + + a = a.get( key ); + b = b.get( key ); + + if ( 'date' === key || 'modified' === key ) { + a = a || new Date(); + b = b || new Date(); + } + + return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac ); + }; + }()) + }, { + defaultArgs: { + posts_per_page: 40, + orderby: 'date', + order: 'DESC' + } + }); + +}(jQuery)); \ No newline at end of file diff --git a/wp-includes/js/media-models.min.js b/wp-includes/js/media-models.min.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wp-includes/js/media-views.js b/wp-includes/js/media-views.js new file mode 100644 index 0000000000..d1c9b0650c --- /dev/null +++ b/wp-includes/js/media-views.js @@ -0,0 +1,429 @@ +(function($){ + var media = wp.media, + Attachment = media.model.Attachment, + Attachments = media.model.Attachments, + Query = media.model.Query; + + + /** + * ======================================================================== + * CONTROLLERS + * ======================================================================== + */ + + /** + * wp.media.controller.Workflow + */ + media.controller.Workflow = Backbone.Model.extend({ + defaults: { + multiple: false + }, + + initialize: function() { + this.createSelection(); + + // Initialize views. + this.modal = new media.view.Modal({ controller: this }); + this.workspace = new media.view.Workspace({ controller: this }); + }, + + createSelection: function() { + var controller = this; + + // Initialize workflow-specific models. + this.selection = new Attachments(); + + _.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 ) ); + } + }); + }, + + render: function() { + this.workspace.render(); + this.modal.content( this.workspace ).attach(); + 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; + + _.defaults( this.options, { + title: '', + container: document.body + }); + }, + + render: function() { + this.$el.html( this.template( this.options ) ); + 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 ) { + this.options.$content = ( $content instanceof Backbone.View ) ? $content.$el : $content; + return this.render(); + }, + + title: function( title ) { + this.options.title = title; + return this.render(); + } + }); + + /** + * wp.media.view.Workspace + */ + media.view.Workspace = Backbone.View.extend({ + tagName: 'div', + className: 'media-workspace', + template: media.template('media-workspace'), + + events: { + 'dragenter': 'maybeInitUploader', + 'mouseenter': 'maybeInitUploader' + }, + + initialize: function() { + this.controller = this.options.controller; + + _.defaults( this.options, { + selectOne: false, + uploader: {} + }); + + this.attachmentsView = new media.view.Attachments({ + controller: this.controller, + directions: 'Select stuff.', + collection: new Attachments( null, { + mirror: media.query() + }) + }); + + this.$content = $('
'); + this.$content.append( this.attachmentsView.$el ); + + // Track uploading attachments. + this.pending = new Attachments( [], { query: false }); + this.pending.on( 'add remove reset change:percent', function() { + this.$el.toggleClass( 'uploading', !! this.pending.length ); + + if ( ! this.$bar || ! this.pending.length ) + return; + + this.$bar.width( ( this.pending.reduce( function( memo, attachment ) { + if ( attachment.get('uploading') ) + return memo + ( attachment.get('percent') || 0 ); + else + return memo + 100; + }, 0 ) / this.pending.length ) + '%' ); + }, this ); + }, + + render: function() { + this.attachmentsView.render(); + this.$el.html( this.template( this.options ) ).append( this.$content ); + this.$bar = this.$('.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'), + + added: function( file ) { + file.attachment = Attachment.create( _.extend({ + file: file, + uploading: true, + date: new Date() + }, _.pick( file, 'loaded', 'size', 'percent' ) ) ); + + workspace.pending.add( file.attachment ); + }, + + progress: function( file ) { + file.attachment.set( _.pick( file, 'loaded', 'percent' ) ); + }, + + success: function( resp, file ) { + var complete; + + _.each(['file','loaded','size','uploading','percent'], function( key ) { + file.attachment.unset( key ); + }); + + file.attachment.set( 'id', resp.id ); + Attachment.get( resp.id, file.attachment ).fetch(); + + complete = workspace.pending.all( function( attachment ) { + return ! attachment.get('uploading'); + }); + + if ( complete ) + workspace.pending.reset(); + }, + + error: function( message, error, file ) { + file.attachment.destroy(); + } + }, this.options.uploader ) ); + } + }); + + + /** + * 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 + }); + + _.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 = $('