From d5686799463fb406e40d10b31dfd12e5923846c0 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Mon, 9 Feb 2015 00:43:50 +0000 Subject: [PATCH] Split the media JS files into modules: * Add a new folder in `wp-includes/js`, `media` * Create manifest files for `views`, `models`, `grid`, and `audio-video` * Make `browserify` an `npm` dependency * Add Grunt tasks for `browserify` and `uglify:media` on `build` and `watch` * Update the paths loaded for media files in `script-loader` * All new files were created using `svn cp` from their original location Please run `npm install`. While developing media JS, you must run `grunt watch`. See #28510. Built from https://develop.svn.wordpress.org/trunk@31373 git-svn-id: http://core.svn.wordpress.org/trunk@31354 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/js/media/audio-video.js | 7204 ++++++++++++ wp-includes/js/media/audio-video.manifest.js | 224 + wp-includes/js/media/audio-video.min.js | 3 + .../js/media/controllers/audio-details.js | 31 + .../js/media/controllers/collection-add.js | 101 + .../js/media/controllers/collection-edit.js | 163 + wp-includes/js/media/controllers/cropper.js | 117 + .../controllers/edit-attachment-metadata.js | 29 + .../js/media/controllers/edit-image.js | 78 + wp-includes/js/media/controllers/embed.js | 137 + .../js/media/controllers/featured-image.js | 122 + .../js/media/controllers/gallery-add.js | 87 + .../js/media/controllers/gallery-edit.js | 140 + .../js/media/controllers/image-details.js | 62 + wp-includes/js/media/controllers/library.js | 273 + .../js/media/controllers/media-library.js | 50 + wp-includes/js/media/controllers/region.js | 179 + .../js/media/controllers/replace-image.js | 108 + .../js/media/controllers/state-machine.js | 124 + wp-includes/js/media/controllers/state.js | 241 + .../js/media/controllers/video-details.js | 31 + wp-includes/js/media/grid.js | 7085 ++++++++++++ wp-includes/js/media/grid.manifest.js | 15 + wp-includes/js/media/grid.min.js | 3 + wp-includes/js/media/models.js | 1503 +++ wp-includes/js/media/models.manifest.js | 232 + wp-includes/js/media/models.min.js | 1 + wp-includes/js/media/models/attachment.js | 168 + wp-includes/js/media/models/attachments.js | 535 + wp-includes/js/media/models/post-image.js | 156 + wp-includes/js/media/models/post-media.js | 42 + wp-includes/js/media/models/query.js | 308 + wp-includes/js/media/models/selection.js | 97 + wp-includes/js/media/routers/manage.js | 46 + wp-includes/js/media/utils/selection-sync.js | 66 + wp-includes/js/media/views.js | 9735 +++++++++++++++++ wp-includes/js/media/views.manifest.js | 148 + wp-includes/js/media/views.min.js | 4 + .../js/media/views/attachment-compat.js | 85 + .../js/media/views/attachment-filters.js | 78 + .../js/media/views/attachment-filters/all.js | 91 + .../js/media/views/attachment-filters/date.js | 42 + .../views/attachment-filters/uploaded.js | 60 + wp-includes/js/media/views/attachment.js | 554 + .../views/attachment/details-two-column.js | 42 + .../js/media/views/attachment/details.js | 140 + .../js/media/views/attachment/edit-library.js | 19 + .../media/views/attachment/edit-selection.js | 20 + .../js/media/views/attachment/library.js | 19 + .../js/media/views/attachment/selection.js | 23 + wp-includes/js/media/views/attachments.js | 300 + .../js/media/views/attachments/browser.js | 459 + .../js/media/views/attachments/selection.js | 31 + wp-includes/js/media/views/audio-details.js | 38 + wp-includes/js/media/views/button-group.js | 48 + wp-includes/js/media/views/button.js | 89 + .../button/delete-selected-permanently.js | 43 + .../js/media/views/button/delete-selected.js | 51 + .../media/views/button/select-mode-toggle.js | 55 + wp-includes/js/media/views/cropper.js | 68 + .../js/media/views/edit-image-details.js | 26 + wp-includes/js/media/views/edit-image.js | 54 + wp-includes/js/media/views/embed.js | 68 + wp-includes/js/media/views/embed/image.js | 33 + wp-includes/js/media/views/embed/link.js | 67 + wp-includes/js/media/views/embed/url.js | 81 + wp-includes/js/media/views/focus-manager.js | 47 + wp-includes/js/media/views/frame.js | 172 + .../js/media/views/frame/audio-details.js | 77 + .../js/media/views/frame/edit-attachments.js | 248 + .../js/media/views/frame/image-details.js | 185 + wp-includes/js/media/views/frame/manage.js | 247 + .../js/media/views/frame/media-details.js | 134 + wp-includes/js/media/views/frame/post.js | 752 ++ wp-includes/js/media/views/frame/select.js | 176 + .../js/media/views/frame/video-details.js | 137 + wp-includes/js/media/views/iframe.js | 25 + wp-includes/js/media/views/image-details.js | 167 + wp-includes/js/media/views/label.js | 25 + wp-includes/js/media/views/media-details.js | 151 + wp-includes/js/media/views/media-frame.js | 254 + wp-includes/js/media/views/menu-item.js | 73 + wp-includes/js/media/views/menu.js | 115 + wp-includes/js/media/views/modal.js | 215 + wp-includes/js/media/views/priority-list.js | 100 + wp-includes/js/media/views/router-item.js | 25 + wp-includes/js/media/views/router.js | 36 + wp-includes/js/media/views/search.js | 49 + wp-includes/js/media/views/selection.js | 85 + wp-includes/js/media/views/settings.js | 121 + .../views/settings/attachment-display.js | 94 + .../js/media/views/settings/gallery.js | 20 + .../js/media/views/settings/playlist.js | 20 + wp-includes/js/media/views/sidebar.js | 17 + wp-includes/js/media/views/spinner.js | 38 + wp-includes/js/media/views/toolbar.js | 162 + wp-includes/js/media/views/toolbar/embed.js | 37 + wp-includes/js/media/views/toolbar/select.js | 70 + wp-includes/js/media/views/uploader/editor.js | 221 + wp-includes/js/media/views/uploader/inline.js | 132 + .../js/media/views/uploader/status-error.js | 19 + wp-includes/js/media/views/uploader/status.js | 139 + wp-includes/js/media/views/uploader/window.js | 112 + wp-includes/js/media/views/video-details.js | 43 + wp-includes/js/media/views/view.js | 66 + wp-includes/script-loader.php | 8 +- wp-includes/version.php | 2 +- 107 files changed, 37143 insertions(+), 5 deletions(-) create mode 100644 wp-includes/js/media/audio-video.js create mode 100644 wp-includes/js/media/audio-video.manifest.js create mode 100644 wp-includes/js/media/audio-video.min.js create mode 100644 wp-includes/js/media/controllers/audio-details.js create mode 100644 wp-includes/js/media/controllers/collection-add.js create mode 100644 wp-includes/js/media/controllers/collection-edit.js create mode 100644 wp-includes/js/media/controllers/cropper.js create mode 100644 wp-includes/js/media/controllers/edit-attachment-metadata.js create mode 100644 wp-includes/js/media/controllers/edit-image.js create mode 100644 wp-includes/js/media/controllers/embed.js create mode 100644 wp-includes/js/media/controllers/featured-image.js create mode 100644 wp-includes/js/media/controllers/gallery-add.js create mode 100644 wp-includes/js/media/controllers/gallery-edit.js create mode 100644 wp-includes/js/media/controllers/image-details.js create mode 100644 wp-includes/js/media/controllers/library.js create mode 100644 wp-includes/js/media/controllers/media-library.js create mode 100644 wp-includes/js/media/controllers/region.js create mode 100644 wp-includes/js/media/controllers/replace-image.js create mode 100644 wp-includes/js/media/controllers/state-machine.js create mode 100644 wp-includes/js/media/controllers/state.js create mode 100644 wp-includes/js/media/controllers/video-details.js create mode 100644 wp-includes/js/media/grid.js create mode 100644 wp-includes/js/media/grid.manifest.js create mode 100644 wp-includes/js/media/grid.min.js create mode 100644 wp-includes/js/media/models.js create mode 100644 wp-includes/js/media/models.manifest.js create mode 100644 wp-includes/js/media/models.min.js create mode 100644 wp-includes/js/media/models/attachment.js create mode 100644 wp-includes/js/media/models/attachments.js create mode 100644 wp-includes/js/media/models/post-image.js create mode 100644 wp-includes/js/media/models/post-media.js create mode 100644 wp-includes/js/media/models/query.js create mode 100644 wp-includes/js/media/models/selection.js create mode 100644 wp-includes/js/media/routers/manage.js create mode 100644 wp-includes/js/media/utils/selection-sync.js create mode 100644 wp-includes/js/media/views.js create mode 100644 wp-includes/js/media/views.manifest.js create mode 100644 wp-includes/js/media/views.min.js create mode 100644 wp-includes/js/media/views/attachment-compat.js create mode 100644 wp-includes/js/media/views/attachment-filters.js create mode 100644 wp-includes/js/media/views/attachment-filters/all.js create mode 100644 wp-includes/js/media/views/attachment-filters/date.js create mode 100644 wp-includes/js/media/views/attachment-filters/uploaded.js create mode 100644 wp-includes/js/media/views/attachment.js create mode 100644 wp-includes/js/media/views/attachment/details-two-column.js create mode 100644 wp-includes/js/media/views/attachment/details.js create mode 100644 wp-includes/js/media/views/attachment/edit-library.js create mode 100644 wp-includes/js/media/views/attachment/edit-selection.js create mode 100644 wp-includes/js/media/views/attachment/library.js create mode 100644 wp-includes/js/media/views/attachment/selection.js create mode 100644 wp-includes/js/media/views/attachments.js create mode 100644 wp-includes/js/media/views/attachments/browser.js create mode 100644 wp-includes/js/media/views/attachments/selection.js create mode 100644 wp-includes/js/media/views/audio-details.js create mode 100644 wp-includes/js/media/views/button-group.js create mode 100644 wp-includes/js/media/views/button.js create mode 100644 wp-includes/js/media/views/button/delete-selected-permanently.js create mode 100644 wp-includes/js/media/views/button/delete-selected.js create mode 100644 wp-includes/js/media/views/button/select-mode-toggle.js create mode 100644 wp-includes/js/media/views/cropper.js create mode 100644 wp-includes/js/media/views/edit-image-details.js create mode 100644 wp-includes/js/media/views/edit-image.js create mode 100644 wp-includes/js/media/views/embed.js create mode 100644 wp-includes/js/media/views/embed/image.js create mode 100644 wp-includes/js/media/views/embed/link.js create mode 100644 wp-includes/js/media/views/embed/url.js create mode 100644 wp-includes/js/media/views/focus-manager.js create mode 100644 wp-includes/js/media/views/frame.js create mode 100644 wp-includes/js/media/views/frame/audio-details.js create mode 100644 wp-includes/js/media/views/frame/edit-attachments.js create mode 100644 wp-includes/js/media/views/frame/image-details.js create mode 100644 wp-includes/js/media/views/frame/manage.js create mode 100644 wp-includes/js/media/views/frame/media-details.js create mode 100644 wp-includes/js/media/views/frame/post.js create mode 100644 wp-includes/js/media/views/frame/select.js create mode 100644 wp-includes/js/media/views/frame/video-details.js create mode 100644 wp-includes/js/media/views/iframe.js create mode 100644 wp-includes/js/media/views/image-details.js create mode 100644 wp-includes/js/media/views/label.js create mode 100644 wp-includes/js/media/views/media-details.js create mode 100644 wp-includes/js/media/views/media-frame.js create mode 100644 wp-includes/js/media/views/menu-item.js create mode 100644 wp-includes/js/media/views/menu.js create mode 100644 wp-includes/js/media/views/modal.js create mode 100644 wp-includes/js/media/views/priority-list.js create mode 100644 wp-includes/js/media/views/router-item.js create mode 100644 wp-includes/js/media/views/router.js create mode 100644 wp-includes/js/media/views/search.js create mode 100644 wp-includes/js/media/views/selection.js create mode 100644 wp-includes/js/media/views/settings.js create mode 100644 wp-includes/js/media/views/settings/attachment-display.js create mode 100644 wp-includes/js/media/views/settings/gallery.js create mode 100644 wp-includes/js/media/views/settings/playlist.js create mode 100644 wp-includes/js/media/views/sidebar.js create mode 100644 wp-includes/js/media/views/spinner.js create mode 100644 wp-includes/js/media/views/toolbar.js create mode 100644 wp-includes/js/media/views/toolbar/embed.js create mode 100644 wp-includes/js/media/views/toolbar/select.js create mode 100644 wp-includes/js/media/views/uploader/editor.js create mode 100644 wp-includes/js/media/views/uploader/inline.js create mode 100644 wp-includes/js/media/views/uploader/status-error.js create mode 100644 wp-includes/js/media/views/uploader/status.js create mode 100644 wp-includes/js/media/views/uploader/window.js create mode 100644 wp-includes/js/media/views/video-details.js create mode 100644 wp-includes/js/media/views/view.js diff --git a/wp-includes/js/media/audio-video.js b/wp-includes/js/media/audio-video.js new file mode 100644 index 0000000000..5d888e83db --- /dev/null +++ b/wp-includes/js/media/audio-video.js @@ -0,0 +1,7204 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= this.created; + + // If we're sorting by menu order and we have no items, + // accept any items that have the default menu order (0). + } else if ( 'ASC' === order && 'menuOrder' === orderby ) { + return attachment.get( orderby ) === 0; + } + + // Otherwise, we don't want any items yet. + return false; + }; + + // Observe the central `wp.Uploader.queue` collection to watch for + // new matches for the query. + // + // Only observe when a limited number of query args are set. There + // are no filters for other properties, so observing will result in + // false positives in those queries. + allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type', 'post_parent' ]; + if ( wp.Uploader && _( this.args ).chain().keys().difference( allowed ).isEmpty().value() ) { + this.observe( wp.Uploader.queue ); + } + }, + /** + * Whether there are more attachments that haven't been sync'd from the server + * that match the collection's query. + * + * @returns {boolean} + */ + hasMore: function() { + return this._hasMore; + }, + /** + * Fetch more attachments from the server for the collection. + * + * @param {object} [options={}] + * @returns {Promise} + */ + more: function( options ) { + var query = this; + + // If there is already a request pending, return early with the Deferred object. + if ( this._more && 'pending' === this._more.state() ) { + return this._more; + } + + if ( ! this.hasMore() ) { + return jQuery.Deferred().resolveWith( this ).promise(); + } + + options = options || {}; + options.remove = false; + + return this._more = this.fetch( options ).done( function( resp ) { + if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) { + query._hasMore = false; + } + }); + }, + /** + * Overrides Backbone.Collection.sync + * Overrides wp.media.model.Attachments.sync + * + * @param {String} method + * @param {Backbone.Model} model + * @param {Object} [options={}] + * @returns {Promise} + */ + sync: function( method, model, options ) { + var args, 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', + post_id: wp.media.model.settings.post.id + }); + + // Clone the args so manipulation is non-destructive. + args = _.clone( this.args ); + + // Determine which page to query. + if ( -1 !== args.posts_per_page ) { + args.paged = Math.floor( this.length / args.posts_per_page ) + 1; + } + + options.data.query = args; + return wp.media.ajax( options ); + + // Otherwise, fall back to Backbone.sync() + } else { + /** + * Call wp.media.model.Attachments.sync or Backbone.sync + */ + fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone; + return fallback.sync.apply( this, arguments ); + } + } +}, { + /** + * @readonly + */ + defaultProps: { + orderby: 'date', + order: 'DESC' + }, + /** + * @readonly + */ + defaultArgs: { + posts_per_page: 40 + }, + /** + * @readonly + */ + orderby: { + allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in', 'menuOrder' ], + /** + * A map of JavaScript orderby values to their WP_Query equivalents. + * @type {Object} + */ + valuemap: { + 'id': 'ID', + 'uploadedTo': 'parent', + 'menuOrder': 'menu_order ID' + } + }, + /** + * A map of JavaScript query properties to their WP_Query equivalents. + * + * @readonly + */ + propmap: { + 'search': 's', + 'type': 'post_mime_type', + 'perPage': 'posts_per_page', + 'menuOrder': 'menu_order', + 'uploadedTo': 'post_parent', + 'status': 'post_status', + 'include': 'post__in', + 'exclude': 'post__not_in' + }, + /** + * Creates and returns an Attachments Query collection given the properties. + * + * Caches query objects and reuses where possible. + * + * @static + * @method + * + * @param {object} [props] + * @param {Object} [props.cache=true] Whether to use the query cache or not. + * @param {Object} [props.order] + * @param {Object} [props.orderby] + * @param {Object} [props.include] + * @param {Object} [props.exclude] + * @param {Object} [props.s] + * @param {Object} [props.post_mime_type] + * @param {Object} [props.posts_per_page] + * @param {Object} [props.menu_order] + * @param {Object} [props.post_parent] + * @param {Object} [props.post_status] + * @param {Object} [options] + * + * @returns {wp.media.model.Query} A new Attachments Query collection. + */ + get: (function(){ + /** + * @static + * @type Array + */ + var queries = []; + + /** + * @returns {Query} + */ + return function( props, options ) { + var args = {}, + orderby = Query.orderby, + defaults = Query.defaultProps, + query, + cache = !! props.cache || _.isUndefined( props.cache ); + + // Remove the `query` property. This isn't linked to a query, + // this *is* the query. + delete props.query; + delete props.cache; + + // Fill default args. + _.defaults( props, defaults ); + + // Normalize the order. + props.order = props.order.toUpperCase(); + if ( 'DESC' !== props.order && 'ASC' !== props.order ) { + props.order = defaults.order.toUpperCase(); + } + + // Ensure we have a valid orderby value. + if ( ! _.contains( orderby.allowed, props.orderby ) ) { + props.orderby = defaults.orderby; + } + + _.each( [ 'include', 'exclude' ], function( prop ) { + if ( props[ prop ] && ! _.isArray( props[ prop ] ) ) { + props[ prop ] = [ props[ prop ] ]; + } + } ); + + // Generate the query `args` object. + // Correct any differing property names. + _.each( props, function( value, prop ) { + if ( _.isNull( value ) ) { + return; + } + + args[ Query.propmap[ prop ] || prop ] = value; + }); + + // Fill any other default query args. + _.defaults( args, Query.defaultArgs ); + + // `props.orderby` does not always map directly to `args.orderby`. + // Substitute exceptions specified in orderby.keymap. + args.orderby = orderby.valuemap[ props.orderby ] || props.orderby; + + // Search the query cache for a matching query. + if ( cache ) { + query = _.find( queries, function( query ) { + return _.isEqual( query.args, args ); + }); + } else { + queries = []; + } + + // Otherwise, create a new query and add it to the cache. + if ( ! query ) { + query = new Query( [], _.extend( options || {}, { + props: props, + args: args + } ) ); + queries.push( query ); + } + + return query; + }; + }()) +}); + +module.exports = Query; +},{"./attachments.js":10}],13:[function(require,module,exports){ +/*globals _ */ + +/** + * wp.media.model.Selection + * + * A selection of attachments. + * + * @class + * @augments wp.media.model.Attachments + * @augments Backbone.Collection + */ +var Attachments = require( './attachments.js' ), + Selection; + +Selection = Attachments.extend({ + /** + * Refresh the `single` model whenever the selection changes. + * Binds `single` instead of using the context argument to ensure + * it receives no parameters. + * + * @param {Array} [models=[]] Array of models used to populate the collection. + * @param {Object} [options={}] + */ + initialize: function( models, options ) { + /** + * call 'initialize' directly on the parent class + */ + Attachments.prototype.initialize.apply( this, arguments ); + this.multiple = options && options.multiple; + + this.on( 'add remove reset', _.bind( this.single, this, false ) ); + }, + + /** + * If the workflow does not support multi-select, clear out the selection + * before adding a new attachment to it. + * + * @param {Array} models + * @param {Object} options + * @returns {wp.media.model.Attachment[]} + */ + add: function( models, options ) { + if ( ! this.multiple ) { + this.remove( this.models ); + } + /** + * call 'add' directly on the parent class + */ + return Attachments.prototype.add.call( this, models, options ); + }, + + /** + * Fired when toggling (clicking on) an attachment in the modal. + * + * @param {undefined|boolean|wp.media.model.Attachment} model + * + * @fires wp.media.model.Selection#selection:single + * @fires wp.media.model.Selection#selection:unsingle + * + * @returns {Backbone.Model} + */ + single: function( model ) { + var previous = this._single; + + // If a `model` is provided, use it as the single model. + if ( model ) { + this._single = model; + } + // If the single model isn't in the selection, remove it. + if ( this._single && ! this.get( this._single.cid ) ) { + delete this._single; + } + + this._single = this._single || this.last(); + + // If single has changed, fire an event. + if ( this._single !== previous ) { + if ( previous ) { + previous.trigger( 'selection:unsingle', previous, this ); + + // If the model was already removed, trigger the collection + // event manually. + if ( ! this.get( previous.cid ) ) { + this.trigger( 'selection:unsingle', previous, this ); + } + } + if ( this._single ) { + this._single.trigger( 'selection:single', this._single, this ); + } + } + + // Return the single model, or the last model as a fallback. + return this._single; + } +}); + +module.exports = Selection; +},{"./attachments.js":10}],14:[function(require,module,exports){ +/*globals _ */ + +/** + * wp.media.selectionSync + * + * Sync an attachments selection in a state with another state. + * + * Allows for selecting multiple images in the Insert Media workflow, and then + * switching to the Insert Gallery workflow while preserving the attachments selection. + * + * @mixin + */ +var selectionSync = { + /** + * @since 3.5.0 + */ + syncSelection: function() { + var selection = this.get('selection'), + manager = this.frame._selection; + + if ( ! this.get('syncSelection') || ! manager || ! selection ) { + return; + } + + // If the selection supports multiple items, validate the stored + // attachments based on the new selection's conditions. Record + // the attachments that are not included; we'll maintain a + // reference to those. Other attachments are considered in flux. + if ( selection.multiple ) { + selection.reset( [], { silent: true }); + selection.validateAll( manager.attachments ); + manager.difference = _.difference( manager.attachments.models, selection.models ); + } + + // Sync the selection's single item with the master. + selection.single( manager.single ); + }, + + /** + * Record the currently active attachments, which is a combination + * of the selection's attachments and the set of selected + * attachments that this specific selection considered invalid. + * Reset the difference and record the single attachment. + * + * @since 3.5.0 + */ + recordSelection: function() { + var selection = this.get('selection'), + manager = this.frame._selection; + + if ( ! this.get('syncSelection') || ! manager || ! selection ) { + return; + } + + if ( selection.multiple ) { + manager.attachments.reset( selection.toArray().concat( manager.difference ) ); + manager.difference = []; + } else { + manager.attachments.add( selection.toArray() ); + } + + manager.single = selection._single; + } +}; + +module.exports = selectionSync; +},{}],15:[function(require,module,exports){ +/*globals _ */ + +/** + * wp.media.view.AttachmentCompat + * + * A view to display fields added via the `attachment_fields_to_edit` filter. + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + AttachmentCompat; + +AttachmentCompat = View.extend({ + tagName: 'form', + className: 'compat-item', + + events: { + 'submit': 'preventDefault', + 'change input': 'save', + 'change select': 'save', + 'change textarea': 'save' + }, + + initialize: function() { + this.listenTo( this.model, 'change:compat', this.render ); + }, + /** + * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining + */ + dispose: function() { + if ( this.$(':focus').length ) { + this.save(); + } + /** + * call 'dispose' directly on the parent class + */ + return View.prototype.dispose.apply( this, arguments ); + }, + /** + * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining + */ + render: function() { + var compat = this.model.get('compat'); + if ( ! compat || ! compat.item ) { + return; + } + + this.views.detach(); + this.$el.html( compat.item ); + this.views.render(); + return this; + }, + /** + * @param {Object} event + */ + preventDefault: function( event ) { + event.preventDefault(); + }, + /** + * @param {Object} event + */ + save: function( event ) { + var data = {}; + + if ( event ) { + event.preventDefault(); + } + + _.each( this.$el.serializeArray(), function( pair ) { + data[ pair.name ] = pair.value; + }); + + this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); + this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); + }, + + postSave: function() { + this.controller.trigger( 'attachment:compat:ready', ['ready'] ); + } +}); + +module.exports = AttachmentCompat; +},{"./view.js":55}],16:[function(require,module,exports){ +/*globals _, jQuery */ + +/** + * wp.media.view.AttachmentFilters + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + $ = jQuery, + AttachmentFilters; + +AttachmentFilters = View.extend({ + tagName: 'select', + className: 'attachment-filters', + id: 'media-attachment-filters', + + events: { + change: 'change' + }, + + keys: [], + + initialize: function() { + this.createFilters(); + _.extend( this.filters, this.options.filters ); + + // Build `' ).val( value ).html( filter.text )[0], + priority: filter.priority || 50 + }; + }, this ).sortBy('priority').pluck('el').value() ); + + this.listenTo( this.model, 'change', this.select ); + this.select(); + }, + + /** + * @abstract + */ + createFilters: function() { + this.filters = {}; + }, + + /** + * When the selected filter changes, update the Attachment Query properties to match. + */ + change: function() { + var filter = this.filters[ this.el.value ]; + if ( filter ) { + this.model.set( filter.props ); + } + }, + + select: function() { + var model = this.model, + value = 'all', + props = model.toJSON(); + + _.find( this.filters, function( filter, id ) { + var equal = _.all( filter.props, function( prop, key ) { + return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); + }); + + if ( equal ) { + return value = id; + } + }); + + this.$el.val( value ); + } +}); + +module.exports = AttachmentFilters; +},{"./view.js":55}],17:[function(require,module,exports){ +/*globals _, wp */ + +/** + * wp.media.view.AttachmentFilters.All + * + * @class + * @augments wp.media.view.AttachmentFilters + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var AttachmentFilters = require( '../attachment-filters.js' ), + l10n = wp.media.view.l10n, + All; + +All = AttachmentFilters.extend({ + createFilters: function() { + var filters = {}; + + _.each( wp.media.view.settings.mimeTypes || {}, function( text, key ) { + filters[ key ] = { + text: text, + props: { + status: null, + type: key, + uploadedTo: null, + orderby: 'date', + order: 'DESC' + } + }; + }); + + filters.all = { + text: l10n.allMediaItems, + props: { + status: null, + type: null, + uploadedTo: null, + orderby: 'date', + order: 'DESC' + }, + priority: 10 + }; + + if ( wp.media.view.settings.post.id ) { + filters.uploaded = { + text: l10n.uploadedToThisPost, + props: { + status: null, + type: null, + uploadedTo: wp.media.view.settings.post.id, + orderby: 'menuOrder', + order: 'ASC' + }, + priority: 20 + }; + } + + filters.unattached = { + text: l10n.unattached, + props: { + status: null, + uploadedTo: 0, + type: null, + orderby: 'menuOrder', + order: 'ASC' + }, + priority: 50 + }; + + if ( wp.media.view.settings.mediaTrash && + this.controller.isModeActive( 'grid' ) ) { + + filters.trash = { + text: l10n.trash, + props: { + uploadedTo: null, + status: 'trash', + type: null, + orderby: 'date', + order: 'DESC' + }, + priority: 50 + }; + } + + this.filters = filters; + } +}); + +module.exports = All; +},{"../attachment-filters.js":16}],18:[function(require,module,exports){ +/*globals _, wp */ + +/** + * A filter dropdown for month/dates. + * + * @class + * @augments wp.media.view.AttachmentFilters + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var AttachmentFilters = require( '../attachment-filters.js' ), + l10n = wp.media.view.l10n, + DateFilter; + +DateFilter = AttachmentFilters.extend({ + id: 'media-attachment-date-filters', + + createFilters: function() { + var filters = {}; + _.each( wp.media.view.settings.months || {}, function( value, index ) { + filters[ index ] = { + text: value.text, + props: { + year: value.year, + monthnum: value.month + } + }; + }); + filters.all = { + text: l10n.allDates, + props: { + monthnum: false, + year: false + }, + priority: 10 + }; + this.filters = filters; + } +}); + +module.exports = DateFilter; +},{"../attachment-filters.js":16}],19:[function(require,module,exports){ +/*globals wp */ + +/** + * wp.media.view.AttachmentFilters.Uploaded + * + * @class + * @augments wp.media.view.AttachmentFilters + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var AttachmentFilters = require( '../attachment-filters.js' ), + l10n = wp.media.view.l10n, + Uploaded; + +Uploaded = AttachmentFilters.extend({ + createFilters: function() { + var type = this.model.get('type'), + types = wp.media.view.settings.mimeTypes, + text; + + if ( types && type ) { + text = types[ type ]; + } + + this.filters = { + all: { + text: text || l10n.allMediaItems, + props: { + uploadedTo: null, + orderby: 'date', + order: 'DESC' + }, + priority: 10 + }, + + uploaded: { + text: l10n.uploadedToThisPost, + props: { + uploadedTo: wp.media.view.settings.post.id, + orderby: 'menuOrder', + order: 'ASC' + }, + priority: 20 + }, + + unattached: { + text: l10n.unattached, + props: { + uploadedTo: 0, + orderby: 'menuOrder', + order: 'ASC' + }, + priority: 50 + } + }; + } +}); + +module.exports = Uploaded; +},{"../attachment-filters.js":16}],20:[function(require,module,exports){ +/*globals _, wp, jQuery */ + +/** + * wp.media.view.Attachment + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + $ = 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; +},{"./view.js":55}],21:[function(require,module,exports){ +/*globals _, wp */ + +/** + * wp.media.view.Attachment.Details + * + * @class + * @augments wp.media.view.Attachment + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var Attachment = require( '../attachment.js' ), + l10n = wp.media.view.l10n, + Details; + +Details = Attachment.extend({ + tagName: 'div', + className: 'attachment-details', + template: wp.template('attachment-details'), + + attributes: function() { + return { + 'tabIndex': 0, + 'data-id': this.model.get( 'id' ) + }; + }, + + events: { + 'change [data-setting]': 'updateSetting', + 'change [data-setting] input': 'updateSetting', + 'change [data-setting] select': 'updateSetting', + 'change [data-setting] textarea': 'updateSetting', + 'click .delete-attachment': 'deleteAttachment', + 'click .trash-attachment': 'trashAttachment', + 'click .untrash-attachment': 'untrashAttachment', + 'click .edit-attachment': 'editAttachment', + 'click .refresh-attachment': 'refreshAttachment', + 'keydown': 'toggleSelectionHandler' + }, + + initialize: function() { + this.options = _.defaults( this.options, { + rerenderOnModelChange: false + }); + + this.on( 'ready', this.initialFocus ); + // Call 'initialize' directly on the parent class. + Attachment.prototype.initialize.apply( this, arguments ); + }, + + initialFocus: function() { + if ( ! wp.media.isTouchDevice ) { + this.$( ':input' ).eq( 0 ).focus(); + } + }, + /** + * @param {Object} event + */ + deleteAttachment: function( event ) { + event.preventDefault(); + + if ( confirm( l10n.warnDelete ) ) { + this.model.destroy(); + // Keep focus inside media modal + // after image is deleted + this.controller.modal.focusManager.focus(); + } + }, + /** + * @param {Object} event + */ + trashAttachment: function( event ) { + var library = this.controller.library; + event.preventDefault(); + + if ( wp.media.view.settings.mediaTrash && + 'edit-metadata' === this.controller.content.mode() ) { + + this.model.set( 'status', 'trash' ); + this.model.save().done( function() { + library._requery( true ); + } ); + } else { + this.model.destroy(); + } + }, + /** + * @param {Object} event + */ + untrashAttachment: function( event ) { + var library = this.controller.library; + event.preventDefault(); + + this.model.set( 'status', 'inherit' ); + this.model.save().done( function() { + library._requery( true ); + } ); + }, + /** + * @param {Object} event + */ + editAttachment: function( event ) { + var editState = this.controller.states.get( 'edit-image' ); + if ( window.imageEdit && editState ) { + event.preventDefault(); + + editState.set( 'image', this.model ); + this.controller.setState( 'edit-image' ); + } else { + this.$el.addClass('needs-refresh'); + } + }, + /** + * @param {Object} event + */ + refreshAttachment: function( event ) { + this.$el.removeClass('needs-refresh'); + event.preventDefault(); + this.model.fetch(); + }, + /** + * When reverse tabbing(shift+tab) out of the right details panel, deliver + * the focus to the item in the list that was being edited. + * + * @param {Object} event + */ + toggleSelectionHandler: function( event ) { + if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { + this.controller.trigger( 'attachment:details:shift-tab', event ); + return false; + } + + if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { + this.controller.trigger( 'attachment:keydown:arrow', event ); + return; + } + } +}); + +module.exports = Details; +},{"../attachment.js":20}],22:[function(require,module,exports){ +/** + * wp.media.view.Attachment.Library + * + * @class + * @augments wp.media.view.Attachment + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var Attachment = require( '../attachment.js' ), + Library; + +Library = Attachment.extend({ + buttons: { + check: true + } +}); + +module.exports = Library; +},{"../attachment.js":20}],23:[function(require,module,exports){ +/*globals _, wp, jQuery */ + +/** + * wp.media.view.Attachments + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + Attachment = require( './attachment.js' ), + $ = jQuery, + Attachments; + +Attachments = View.extend({ + tagName: 'ul', + className: 'attachments', + + attributes: { + tabIndex: -1 + }, + + initialize: function() { + this.el.id = _.uniqueId('__attachments-view-'); + + _.defaults( this.options, { + refreshSensitivity: wp.media.isTouchDevice ? 300 : 200, + refreshThreshold: 3, + AttachmentView: Attachment, + sortable: false, + resize: true, + idealColumnWidth: $( window ).width() < 640 ? 135 : 150 + }); + + this._viewsByCid = {}; + this.$window = $( window ); + this.resizeEvent = 'resize.media-modal-columns'; + + this.collection.on( 'add', function( attachment ) { + this.views.add( this.createAttachmentView( attachment ), { + at: this.collection.indexOf( attachment ) + }); + }, this ); + + this.collection.on( 'remove', function( attachment ) { + var view = this._viewsByCid[ attachment.cid ]; + delete this._viewsByCid[ attachment.cid ]; + + if ( view ) { + view.remove(); + } + }, this ); + + this.collection.on( 'reset', this.render, this ); + + this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); + + // Throttle the scroll handler and bind this. + this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); + + this.options.scrollElement = this.options.scrollElement || this.el; + $( this.options.scrollElement ).on( 'scroll', this.scroll ); + + this.initSortable(); + + _.bindAll( this, 'setColumns' ); + + if ( this.options.resize ) { + this.on( 'ready', this.bindEvents ); + this.controller.on( 'open', this.setColumns ); + + // Call this.setColumns() after this view has been rendered in the DOM so + // attachments get proper width applied. + _.defer( this.setColumns, this ); + } + }, + + bindEvents: function() { + this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); + }, + + attachmentFocus: function() { + this.$( 'li:first' ).focus(); + }, + + restoreFocus: function() { + this.$( 'li.selected:first' ).focus(); + }, + + arrowEvent: function( event ) { + var attachments = this.$el.children( 'li' ), + perRow = this.columns, + index = attachments.filter( ':focus' ).index(), + row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); + + if ( index === -1 ) { + return; + } + + // Left arrow + if ( 37 === event.keyCode ) { + if ( 0 === index ) { + return; + } + attachments.eq( index - 1 ).focus(); + } + + // Up arrow + if ( 38 === event.keyCode ) { + if ( 1 === row ) { + return; + } + attachments.eq( index - perRow ).focus(); + } + + // Right arrow + if ( 39 === event.keyCode ) { + if ( attachments.length === index ) { + return; + } + attachments.eq( index + 1 ).focus(); + } + + // Down arrow + if ( 40 === event.keyCode ) { + if ( Math.ceil( attachments.length / perRow ) === row ) { + return; + } + attachments.eq( index + perRow ).focus(); + } + }, + + dispose: function() { + this.collection.props.off( null, null, this ); + if ( this.options.resize ) { + this.$window.off( this.resizeEvent ); + } + + /** + * call 'dispose' directly on the parent class + */ + View.prototype.dispose.apply( this, arguments ); + }, + + setColumns: function() { + var prev = this.columns, + width = this.$el.width(); + + if ( width ) { + this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; + + if ( ! prev || prev !== this.columns ) { + this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); + } + } + }, + + initSortable: function() { + var collection = this.collection; + + if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { + return; + } + + this.$el.sortable( _.extend({ + // If the `collection` has a `comparator`, disable sorting. + disabled: !! collection.comparator, + + // Change the position of the attachment as soon as the + // mouse pointer overlaps a thumbnail. + tolerance: 'pointer', + + // Record the initial `index` of the dragged model. + start: function( event, ui ) { + ui.item.data('sortableIndexStart', ui.item.index()); + }, + + // Update the model's index in the collection. + // Do so silently, as the view is already accurate. + update: function( event, ui ) { + var model = collection.at( ui.item.data('sortableIndexStart') ), + comparator = collection.comparator; + + // Temporarily disable the comparator to prevent `add` + // from re-sorting. + delete collection.comparator; + + // Silently shift the model to its new index. + collection.remove( model, { + silent: true + }); + collection.add( model, { + silent: true, + at: ui.item.index() + }); + + // Restore the comparator. + collection.comparator = comparator; + + // Fire the `reset` event to ensure other collections sync. + collection.trigger( 'reset', collection ); + + // If the collection is sorted by menu order, + // update the menu order. + collection.saveMenuOrder(); + } + }, this.options.sortable ) ); + + // If the `orderby` property is changed on the `collection`, + // check to see if we have a `comparator`. If so, disable sorting. + collection.props.on( 'change:orderby', function() { + this.$el.sortable( 'option', 'disabled', !! collection.comparator ); + }, this ); + + this.collection.props.on( 'change:orderby', this.refreshSortable, this ); + this.refreshSortable(); + }, + + refreshSortable: function() { + if ( wp.media.isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { + return; + } + + // If the `collection` has a `comparator`, disable sorting. + var collection = this.collection, + orderby = collection.props.get('orderby'), + enabled = 'menuOrder' === orderby || ! collection.comparator; + + this.$el.sortable( 'option', 'disabled', ! enabled ); + }, + + /** + * @param {wp.media.model.Attachment} attachment + * @returns {wp.media.View} + */ + createAttachmentView: function( attachment ) { + var view = new this.options.AttachmentView({ + controller: this.controller, + model: attachment, + collection: this.collection, + selection: this.options.selection + }); + + return this._viewsByCid[ attachment.cid ] = view; + }, + + prepare: function() { + // Create all of the Attachment views, and replace + // the list in a single DOM operation. + if ( this.collection.length ) { + this.views.set( this.collection.map( this.createAttachmentView, this ) ); + + // If there are no elements, clear the views and load some. + } else { + this.views.unset(); + this.collection.more().done( this.scroll ); + } + }, + + ready: function() { + // Trigger the scroll event to check if we're within the + // threshold to query for additional attachments. + this.scroll(); + }, + + scroll: function() { + var view = this, + el = this.options.scrollElement, + scrollTop = el.scrollTop, + toolbar; + + // The scroll event occurs on the document, but the element + // that should be checked is the document body. + if ( el == document ) { + el = document.body; + scrollTop = $(document).scrollTop(); + } + + if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { + return; + } + + toolbar = this.views.parent.toolbar; + + // Show the spinner only if we are close to the bottom. + if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { + toolbar.get('spinner').show(); + } + + if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { + this.collection.more().done(function() { + view.scroll(); + toolbar.get('spinner').hide(); + }); + } + } +}); + +module.exports = Attachments; +},{"./attachment.js":20,"./view.js":55}],24:[function(require,module,exports){ +/*globals _, wp, jQuery */ + +/** + * wp.media.view.AttachmentsBrowser + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + * + * @param {object} options + * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. + * Accepts 'uploaded' and 'all'. + * @param {object} [options.search=true] Whether to show the search interface in the + * browser's toolbar. + * @param {object} [options.date=true] Whether to show the date filter in the + * browser's toolbar. + * @param {object} [options.display=false] Whether to show the attachments display settings + * view in the sidebar. + * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. + * Accepts true, false, and 'errors'. + */ +var View = require( '../view.js' ), + Library = require( '../attachment/library.js' ), + Toolbar = require( '../toolbar.js' ), + Spinner = require( '../spinner.js' ), + Search = require( '../search.js' ), + Label = require( '../label.js' ), + Uploaded = require( '../attachment-filters/uploaded.js' ), + All = require( '../attachment-filters/all.js' ), + DateFilter = require( '../attachment-filters/date.js' ), + UploaderInline = require( '../uploader/inline.js' ), + Attachments = require( '../attachments.js' ), + Sidebar = require( '../sidebar.js' ), + UploaderStatus = require( '../uploader/status.js' ), + Details = require( '../attachment/details.js' ), + AttachmentCompat = require( '../attachment-compat.js' ), + AttachmentDisplay = require( '../settings/attachment-display.js' ), + mediaTrash = wp.media.view.settings.mediaTrash, + l10n = wp.media.view.l10n, + $ = jQuery, + AttachmentsBrowser; + +AttachmentsBrowser = View.extend({ + tagName: 'div', + className: 'attachments-browser', + + initialize: function() { + _.defaults( this.options, { + filters: false, + search: true, + date: true, + display: false, + sidebar: true, + AttachmentView: Library + }); + + this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); + this.controller.on( 'edit:selection', this.editSelection ); + this.createToolbar(); + if ( this.options.sidebar ) { + this.createSidebar(); + } + this.createUploader(); + this.createAttachments(); + this.updateContent(); + + if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { + this.$el.addClass( 'hide-sidebar' ); + + if ( 'errors' === this.options.sidebar ) { + this.$el.addClass( 'sidebar-for-errors' ); + } + } + + this.collection.on( 'add remove reset', this.updateContent, this ); + }, + + editSelection: function( modal ) { + modal.$( '.media-button-backToLibrary' ).focus(); + }, + + /** + * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining + */ + dispose: function() { + this.options.selection.off( null, null, this ); + View.prototype.dispose.apply( this, arguments ); + return this; + }, + + createToolbar: function() { + var LibraryViewSwitcher, Filters, toolbarOptions; + + toolbarOptions = { + controller: this.controller + }; + + if ( this.controller.isModeActive( 'grid' ) ) { + toolbarOptions.className = 'media-toolbar wp-filter'; + } + + /** + * @member {wp.media.view.Toolbar} + */ + this.toolbar = new Toolbar( toolbarOptions ); + + this.views.add( this.toolbar ); + + this.toolbar.set( 'spinner', new Spinner({ + priority: -60 + }) ); + + if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { + // "Filters" will return a , screen reader text needs to be rendered before + this.toolbar.set( 'dateFilterLabel', new Label({ + value: l10n.filterByDate, + attributes: { + 'for': 'media-attachment-date-filters' + }, + priority: -75 + }).render() ); + this.toolbar.set( 'dateFilter', new DateFilter({ + controller: this.controller, + model: this.collection.props, + priority: -75 + }).render() ); + + // BulkSelection is a
with subviews, including screen reader text + this.toolbar.set( 'selectModeToggleButton', new wp.media.view.SelectModeToggleButton({ + text: l10n.bulkSelect, + controller: this.controller, + priority: -70 + }).render() ); + + this.toolbar.set( 'deleteSelectedButton', new wp.media.view.DeleteSelectedButton({ + filters: Filters, + style: 'primary', + disabled: true, + text: mediaTrash ? l10n.trashSelected : l10n.deleteSelected, + controller: this.controller, + priority: -60, + click: function() { + var changed = [], removed = [], self = this, + selection = this.controller.state().get( 'selection' ), + library = this.controller.state().get( 'library' ); + + if ( ! selection.length ) { + return; + } + + if ( ! mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { + return; + } + + if ( mediaTrash && + 'trash' !== selection.at( 0 ).get( 'status' ) && + ! confirm( l10n.warnBulkTrash ) ) { + + return; + } + + selection.each( function( model ) { + if ( ! model.get( 'nonces' )['delete'] ) { + removed.push( model ); + return; + } + + if ( mediaTrash && 'trash' === model.get( 'status' ) ) { + model.set( 'status', 'inherit' ); + changed.push( model.save() ); + removed.push( model ); + } else if ( mediaTrash ) { + model.set( 'status', 'trash' ); + changed.push( model.save() ); + removed.push( model ); + } else { + model.destroy({wait: true}); + } + } ); + + if ( changed.length ) { + selection.remove( removed ); + + $.when.apply( null, changed ).then( function() { + library._requery( true ); + self.controller.trigger( 'selection:action:done' ); + } ); + } else { + this.controller.trigger( 'selection:action:done' ); + } + } + }).render() ); + + if ( mediaTrash ) { + this.toolbar.set( 'deleteSelectedPermanentlyButton', new wp.media.view.DeleteSelectedPermanentlyButton({ + filters: Filters, + style: 'primary', + disabled: true, + text: l10n.deleteSelected, + controller: this.controller, + priority: -55, + click: function() { + var removed = [], selection = this.controller.state().get( 'selection' ); + + if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { + return; + } + + selection.each( function( model ) { + if ( ! model.get( 'nonces' )['delete'] ) { + removed.push( model ); + return; + } + + model.destroy(); + } ); + + selection.remove( removed ); + this.controller.trigger( 'selection:action:done' ); + } + }).render() ); + } + + } else if ( this.options.date ) { + // DateFilter is a , need to render + // screen reader text before + this.toolbar.set( 'filtersLabel', new Label({ + value: l10n.filterByType, + attributes: { + 'for': 'media-attachment-filters' + }, + priority: -80 + }).render() ); + + if ( 'uploaded' === this.options.filters ) { + this.toolbar.set( 'filters', new Uploaded({ + controller: this.controller, + model: this.collection.props, + priority: -80 + }).render() ); + } else { + Filters = new All({ + controller: this.controller, + model: this.collection.props, + priority: -80 + }); + + this.toolbar.set( 'filters', Filters.render() ); + } + } + + // Feels odd to bring the global media library switcher into the Attachment + // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); + // which the controller can tap into and add this view? + if ( this.controller.isModeActive( 'grid' ) ) { + LibraryViewSwitcher = View.extend({ + className: 'view-switch media-grid-view-switch', + template: wp.template( 'media-library-view-switcher') + }); + + this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ + controller: this.controller, + priority: -90 + }).render() ); + + // DateFilter is a , screen reader text needs to be rendered before + this.toolbar.set( 'dateFilterLabel', new Label({ + value: l10n.filterByDate, + attributes: { + 'for': 'media-attachment-date-filters' + }, + priority: -75 + }).render() ); + this.toolbar.set( 'dateFilter', new DateFilter({ + controller: this.controller, + model: this.collection.props, + priority: -75 + }).render() ); + } + + if ( this.options.search ) { + // Search is an input, screen reader text needs to be rendered before + this.toolbar.set( 'searchLabel', new Label({ + value: l10n.searchMediaLabel, + attributes: { + 'for': 'media-search-input' + }, + priority: 60 + }).render() ); + this.toolbar.set( 'search', new Search({ + controller: this.controller, + model: this.collection.props, + priority: 60 + }).render() ); + } + + if ( this.options.dragInfo ) { + this.toolbar.set( 'dragInfo', new View({ + el: $( '
' + l10n.dragInfo + '
' )[0], + priority: -40 + }) ); + } + + if ( this.options.suggestedWidth && this.options.suggestedHeight ) { + this.toolbar.set( 'suggestedDimensions', new View({ + el: $( '
' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '
' )[0], + priority: -40 + }) ); + } + }, + + updateContent: function() { + var view = this, + noItemsView; + + if ( this.controller.isModeActive( 'grid' ) ) { + noItemsView = view.attachmentsNoResults; + } else { + noItemsView = view.uploader; + } + + if ( ! this.collection.length ) { + this.toolbar.get( 'spinner' ).show(); + this.dfd = this.collection.more().done( function() { + if ( ! view.collection.length ) { + noItemsView.$el.removeClass( 'hidden' ); + } else { + noItemsView.$el.addClass( 'hidden' ); + } + view.toolbar.get( 'spinner' ).hide(); + } ); + } else { + noItemsView.$el.addClass( 'hidden' ); + view.toolbar.get( 'spinner' ).hide(); + } + }, + + createUploader: function() { + this.uploader = new UploaderInline({ + controller: this.controller, + status: false, + message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, + canClose: this.controller.isModeActive( 'grid' ) + }); + + this.uploader.hide(); + this.views.add( this.uploader ); + }, + + toggleUploader: function() { + if ( this.uploader.$el.hasClass( 'hidden' ) ) { + this.uploader.show(); + } else { + this.uploader.hide(); + } + }, + + createAttachments: function() { + this.attachments = new Attachments({ + controller: this.controller, + collection: this.collection, + selection: this.options.selection, + model: this.model, + sortable: this.options.sortable, + scrollElement: this.options.scrollElement, + idealColumnWidth: this.options.idealColumnWidth, + + // The single `Attachment` view to be used in the `Attachments` view. + AttachmentView: this.options.AttachmentView + }); + + // Add keydown listener to the instance of the Attachments view + this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); + this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); + + this.views.add( this.attachments ); + + + if ( this.controller.isModeActive( 'grid' ) ) { + this.attachmentsNoResults = new View({ + controller: this.controller, + tagName: 'p' + }); + + this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); + this.attachmentsNoResults.$el.html( l10n.noMedia ); + + this.views.add( this.attachmentsNoResults ); + } + }, + + createSidebar: function() { + var options = this.options, + selection = options.selection, + sidebar = this.sidebar = new Sidebar({ + controller: this.controller + }); + + this.views.add( sidebar ); + + if ( this.controller.uploader ) { + sidebar.set( 'uploads', new UploaderStatus({ + controller: this.controller, + priority: 40 + }) ); + } + + selection.on( 'selection:single', this.createSingle, this ); + selection.on( 'selection:unsingle', this.disposeSingle, this ); + + if ( selection.single() ) { + this.createSingle(); + } + }, + + createSingle: function() { + var sidebar = this.sidebar, + single = this.options.selection.single(); + + sidebar.set( 'details', new Details({ + controller: this.controller, + model: single, + priority: 80 + }) ); + + sidebar.set( 'compat', new AttachmentCompat({ + controller: this.controller, + model: single, + priority: 120 + }) ); + + if ( this.options.display ) { + sidebar.set( 'display', new AttachmentDisplay({ + controller: this.controller, + model: this.model.display( single ), + attachment: single, + priority: 160, + userSettings: this.model.get('displayUserSettings') + }) ); + } + + // Show the sidebar on mobile + if ( this.model.id === 'insert' ) { + sidebar.$el.addClass( 'visible' ); + } + }, + + disposeSingle: function() { + var sidebar = this.sidebar; + sidebar.unset('details'); + sidebar.unset('compat'); + sidebar.unset('display'); + // Hide the sidebar on mobile + sidebar.$el.removeClass( 'visible' ); + } +}); + +module.exports = AttachmentsBrowser; +},{"../attachment-compat.js":14,"../attachment-filters/all.js":16,"../attachment-filters/date.js":17,"../attachment-filters/uploaded.js":18,"../attachment/details.js":21,"../attachment/library.js":22,"../attachments.js":23,"../label.js":36,"../search.js":45,"../settings/attachment-display.js":47,"../sidebar.js":48,"../spinner.js":49,"../toolbar.js":50,"../uploader/inline.js":51,"../uploader/status.js":53,"../view.js":55}],25:[function(require,module,exports){ +/*globals _, Backbone */ + +/** + * wp.media.view.Button + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + Button; + +Button = View.extend({ + tagName: 'a', + className: 'media-button', + attributes: { href: '#' }, + + events: { + 'click': 'click' + }, + + defaults: { + text: '', + style: '', + size: 'large', + disabled: false + }, + + initialize: function() { + /** + * Create a model with the provided `defaults`. + * + * @member {Backbone.Model} + */ + 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.listenTo( this.model, 'change', this.render ); + }, + /** + * @returns {wp.media.view.Button} Returns itself to allow chaining + */ + render: function() { + var classes = [ 'button', this.className ], + model = this.model.toJSON(); + + if ( model.style ) { + classes.push( 'button-' + model.style ); + } + + if ( model.size ) { + classes.push( 'button-' + model.size ); + } + + classes = _.uniq( classes.concat( this.options.classes ) ); + this.el.className = classes.join(' '); + + this.$el.attr( 'disabled', model.disabled ); + this.$el.text( this.model.get('text') ); + + return this; + }, + /** + * @param {Object} event + */ + click: function( event ) { + if ( '#' === this.attributes.href ) { + event.preventDefault(); + } + + if ( this.options.click && ! this.model.get('disabled') ) { + this.options.click.apply( this, arguments ); + } + } +}); + +module.exports = Button; +},{"./view.js":55}],26:[function(require,module,exports){ +/** + * When MEDIA_TRASH is true, a button that handles bulk Delete Permanently logic + * + * @constructor + * @augments wp.media.view.DeleteSelectedButton + * @augments wp.media.view.Button + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var Button = require( '../button.js' ), + DeleteSelected = require( './delete-selected.js' ), + DeleteSelectedPermanently; + +DeleteSelectedPermanently = DeleteSelected.extend({ + initialize: function() { + DeleteSelected.prototype.initialize.apply( this, arguments ); + this.listenTo( this.controller, 'select:activate', this.selectActivate ); + this.listenTo( this.controller, 'select:deactivate', this.selectDeactivate ); + }, + + filterChange: function( model ) { + this.canShow = ( 'trash' === model.get( 'status' ) ); + }, + + selectActivate: function() { + this.toggleDisabled(); + this.$el.toggleClass( 'hidden', ! this.canShow ); + }, + + selectDeactivate: function() { + this.toggleDisabled(); + this.$el.addClass( 'hidden' ); + }, + + render: function() { + Button.prototype.render.apply( this, arguments ); + this.selectActivate(); + return this; + } +}); + +module.exports = DeleteSelectedPermanently; +},{"../button.js":25,"./delete-selected.js":27}],27:[function(require,module,exports){ +/*globals wp */ + +/** + * A button that handles bulk Delete/Trash logic + * + * @constructor + * @augments wp.media.view.Button + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var Button = require( '../button.js' ), + l10n = wp.media.view.l10n, + DeleteSelected; + +DeleteSelected = Button.extend({ + initialize: function() { + Button.prototype.initialize.apply( this, arguments ); + if ( this.options.filters ) { + this.listenTo( this.options.filters.model, 'change', this.filterChange ); + } + this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled ); + }, + + filterChange: function( model ) { + if ( 'trash' === model.get( 'status' ) ) { + this.model.set( 'text', l10n.untrashSelected ); + } else if ( wp.media.view.settings.mediaTrash ) { + this.model.set( 'text', l10n.trashSelected ); + } else { + this.model.set( 'text', l10n.deleteSelected ); + } + }, + + toggleDisabled: function() { + this.model.set( 'disabled', ! this.controller.state().get( 'selection' ).length ); + }, + + render: function() { + Button.prototype.render.apply( this, arguments ); + if ( this.controller.isModeActive( 'select' ) ) { + this.$el.addClass( 'delete-selected-button' ); + } else { + this.$el.addClass( 'delete-selected-button hidden' ); + } + this.toggleDisabled(); + return this; + } +}); + +module.exports = DeleteSelected; +},{"../button.js":25}],28:[function(require,module,exports){ +/*globals wp */ + +var Button = require( '../button.js' ), + l10n = wp.media.view.l10n, + SelectModeToggle; + +SelectModeToggle = Button.extend({ + initialize: function() { + Button.prototype.initialize.apply( this, arguments ); + this.listenTo( this.controller, 'select:activate select:deactivate', this.toggleBulkEditHandler ); + this.listenTo( this.controller, 'selection:action:done', this.back ); + }, + + back: function () { + this.controller.deactivateMode( 'select' ).activateMode( 'edit' ); + }, + + click: function() { + Button.prototype.click.apply( this, arguments ); + if ( this.controller.isModeActive( 'select' ) ) { + this.back(); + } else { + this.controller.deactivateMode( 'edit' ).activateMode( 'select' ); + } + }, + + render: function() { + Button.prototype.render.apply( this, arguments ); + this.$el.addClass( 'select-mode-toggle-button' ); + return this; + }, + + toggleBulkEditHandler: function() { + var toolbar = this.controller.content.get().toolbar, children; + + children = toolbar.$( '.media-toolbar-secondary > *, .media-toolbar-primary > *' ); + + // TODO: the Frame should be doing all of this. + if ( this.controller.isModeActive( 'select' ) ) { + this.model.set( 'text', l10n.cancelSelection ); + children.not( '.media-button' ).hide(); + this.$el.show(); + toolbar.$( '.delete-selected-button' ).removeClass( 'hidden' ); + } else { + this.model.set( 'text', l10n.bulkSelect ); + this.controller.content.get().$el.removeClass( 'fixed' ); + toolbar.$el.css( 'width', '' ); + toolbar.$( '.delete-selected-button' ).addClass( 'hidden' ); + children.not( '.spinner, .media-button' ).show(); + this.controller.state().get( 'selection' ).reset(); + } + } +}); + +module.exports = SelectModeToggle; +},{"../button.js":25}],29:[function(require,module,exports){ +var View = require( './view.js' ), + EditImage = require( './edit-image.js' ), + Details; + +Details = EditImage.extend({ + initialize: function( options ) { + this.editor = window.imageEdit; + this.frame = options.frame; + this.controller = options.controller; + View.prototype.initialize.apply( this, arguments ); + }, + + back: function() { + this.frame.content.mode( 'edit-metadata' ); + }, + + save: function() { + var self = this; + + this.model.fetch().done( function() { + self.frame.content.mode( 'edit-metadata' ); + }); + } +}); + +module.exports = Details; +},{"./edit-image.js":30,"./view.js":55}],30:[function(require,module,exports){ +/*globals _, wp */ + +var View = require( './view.js' ), + EditImage; + +EditImage = View.extend({ + className: 'image-editor', + template: wp.template('image-editor'), + + initialize: function( options ) { + this.editor = window.imageEdit; + this.controller = options.controller; + View.prototype.initialize.apply( this, arguments ); + }, + + prepare: function() { + return this.model.toJSON(); + }, + + render: function() { + View.prototype.render.apply( this, arguments ); + return this; + }, + + loadEditor: function() { + var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); + dfd.done( _.bind( this.focus, this ) ); + }, + + focus: function() { + this.$( '.imgedit-submit .button' ).eq( 0 ).focus(); + }, + + back: function() { + var lastState = this.controller.lastState(); + this.controller.setState( lastState ); + }, + + refresh: function() { + this.model.fetch(); + }, + + save: function() { + var self = this, + lastState = this.controller.lastState(); + + this.model.fetch().done( function() { + self.controller.setState( lastState ); + }); + } + +}); + +module.exports = EditImage; +},{"./view.js":55}],31:[function(require,module,exports){ +/** + * wp.media.view.FocusManager + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + FocusManager; + +FocusManager = View.extend({ + + events: { + 'keydown': 'constrainTabbing' + }, + + focus: function() { // Reset focus on first left menu item + this.$('.media-menu-item').first().focus(); + }, + /** + * @param {Object} event + */ + constrainTabbing: function( event ) { + var tabbables; + + // Look for the tab key. + if ( 9 !== event.keyCode ) { + return; + } + + // Skip the file input added by Plupload. + tabbables = this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' ); + + // Keep tab focus within media modal while it's open + if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { + tabbables.first().focus(); + return false; + } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { + tabbables.last().focus(); + return false; + } + } + +}); + +module.exports = FocusManager; +},{"./view.js":55}],32:[function(require,module,exports){ +/*globals _, Backbone */ + +/** + * wp.media.view.Frame + * + * A frame is a composite view consisting of one or more regions and one or more + * states. + * + * @see wp.media.controller.State + * @see wp.media.controller.Region + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + * @mixes wp.media.controller.StateMachine + */ +var StateMachine = require( '../controllers/state-machine.js' ), + State = require( '../controllers/state.js' ), + Region = require( '../controllers/region.js' ), + View = require( './view.js' ), + Frame; + +Frame = View.extend({ + initialize: function() { + _.defaults( this.options, { + mode: [ 'select' ] + }); + this._createRegions(); + this._createStates(); + this._createModes(); + }, + + _createRegions: function() { + // Clone the regions array. + this.regions = this.regions ? this.regions.slice() : []; + + // Initialize regions. + _.each( this.regions, function( region ) { + this[ region ] = new Region({ + view: this, + id: region, + selector: '.media-frame-' + region + }); + }, this ); + }, + /** + * Create the frame's states. + * + * @see wp.media.controller.State + * @see wp.media.controller.StateMachine + * + * @fires wp.media.controller.State#ready + */ + _createStates: function() { + // Create the default `states` collection. + this.states = new Backbone.Collection( null, { + model: State + }); + + // Ensure states have a reference to the frame. + this.states.on( 'add', function( model ) { + model.frame = this; + model.trigger('ready'); + }, this ); + + if ( this.options.states ) { + this.states.add( this.options.states ); + } + }, + + /** + * A frame can be in a mode or multiple modes at one time. + * + * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. + */ + _createModes: function() { + // Store active "modes" that the frame is in. Unrelated to region modes. + this.activeModes = new Backbone.Collection(); + this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); + + _.each( this.options.mode, function( mode ) { + this.activateMode( mode ); + }, this ); + }, + /** + * Reset all states on the frame to their defaults. + * + * @returns {wp.media.view.Frame} Returns itself to allow chaining + */ + reset: function() { + this.states.invoke( 'trigger', 'reset' ); + return this; + }, + /** + * Map activeMode collection events to the frame. + */ + triggerModeEvents: function( model, collection, options ) { + var collectionEvent, + modeEventMap = { + add: 'activate', + remove: 'deactivate' + }, + eventToTrigger; + // Probably a better way to do this. + _.each( options, function( value, key ) { + if ( value ) { + collectionEvent = key; + } + } ); + + if ( ! _.has( modeEventMap, collectionEvent ) ) { + return; + } + + eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; + this.trigger( eventToTrigger ); + }, + /** + * Activate a mode on the frame. + * + * @param string mode Mode ID. + * @returns {this} Returns itself to allow chaining. + */ + activateMode: function( mode ) { + // Bail if the mode is already active. + if ( this.isModeActive( mode ) ) { + return; + } + this.activeModes.add( [ { id: mode } ] ); + // Add a CSS class to the frame so elements can be styled for the mode. + this.$el.addClass( 'mode-' + mode ); + + return this; + }, + /** + * Deactivate a mode on the frame. + * + * @param string mode Mode ID. + * @returns {this} Returns itself to allow chaining. + */ + deactivateMode: function( mode ) { + // Bail if the mode isn't active. + if ( ! this.isModeActive( mode ) ) { + return this; + } + this.activeModes.remove( this.activeModes.where( { id: mode } ) ); + this.$el.removeClass( 'mode-' + mode ); + /** + * Frame mode deactivation event. + * + * @event this#{mode}:deactivate + */ + this.trigger( mode + ':deactivate' ); + + return this; + }, + /** + * Check if a mode is enabled on the frame. + * + * @param string mode Mode ID. + * @return bool + */ + isModeActive: function( mode ) { + return Boolean( this.activeModes.where( { id: mode } ).length ); + } +}); + +// Make the `Frame` a `StateMachine`. +_.extend( Frame.prototype, StateMachine.prototype ); + +module.exports = Frame; +},{"../controllers/region.js":4,"../controllers/state-machine.js":5,"../controllers/state.js":6,"./view.js":55}],33:[function(require,module,exports){ +/*globals _, wp, jQuery */ + +/** + * A frame for editing the details of a specific media item. + * + * Opens in a modal by default. + * + * Requires an attachment model to be passed in the options hash under `model`. + * + * @constructor + * @augments wp.media.view.Frame + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + * @mixes wp.media.controller.StateMachine + */ +var Frame = require( '../frame.js' ), + MediaFrame = require( '../media-frame.js' ), + Modal = require( '../modal.js' ), + EditAttachmentMetadata = require( '../../controllers/edit-attachment-metadata.js' ), + TwoColumn = require( '../attachment/details-two-column.js' ), + AttachmentCompat = require( '../attachment-compat.js' ), + EditImageController = require( '../../controllers/edit-image.js' ), + DetailsView = require( '../edit-image-details.js' ), + $ = jQuery, + EditAttachments; + +EditAttachments = MediaFrame.extend({ + + className: 'edit-attachment-frame', + template: wp.template( 'edit-attachment-frame' ), + regions: [ 'title', 'content' ], + + events: { + 'click .left': 'previousMediaItem', + 'click .right': 'nextMediaItem' + }, + + initialize: function() { + Frame.prototype.initialize.apply( this, arguments ); + + _.defaults( this.options, { + modal: true, + state: 'edit-attachment' + }); + + this.controller = this.options.controller; + this.gridRouter = this.controller.gridRouter; + this.library = this.options.library; + + if ( this.options.model ) { + this.model = this.options.model; + } + + this.bindHandlers(); + this.createStates(); + this.createModal(); + + this.title.mode( 'default' ); + this.toggleNav(); + }, + + bindHandlers: function() { + // Bind default title creation. + this.on( 'title:create:default', this.createTitle, this ); + + // Close the modal if the attachment is deleted. + this.listenTo( this.model, 'change:status destroy', this.close, this ); + + this.on( 'content:create:edit-metadata', this.editMetadataMode, this ); + this.on( 'content:create:edit-image', this.editImageMode, this ); + this.on( 'content:render:edit-image', this.editImageModeRender, this ); + this.on( 'close', this.detach ); + }, + + createModal: function() { + var self = this; + + // Initialize modal container view. + if ( this.options.modal ) { + this.modal = new Modal({ + controller: this, + title: this.options.title + }); + + this.modal.on( 'open', function () { + $( 'body' ).on( 'keydown.media-modal', _.bind( self.keyEvent, self ) ); + } ); + + // Completely destroy the modal DOM element when closing it. + this.modal.on( 'close', function() { + self.modal.remove(); + $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */ + // Restore the original focus item if possible + $( 'li.attachment[data-id="' + self.model.get( 'id' ) +'"]' ).focus(); + self.resetRoute(); + } ); + + // Set this frame as the modal's content. + this.modal.content( this ); + this.modal.open(); + } + }, + + /** + * Add the default states to the frame. + */ + createStates: function() { + this.states.add([ + new EditAttachmentMetadata( { model: this.model } ) + ]); + }, + + /** + * Content region rendering callback for the `edit-metadata` mode. + * + * @param {Object} contentRegion Basic object with a `view` property, which + * should be set with the proper region view. + */ + editMetadataMode: function( contentRegion ) { + contentRegion.view = new TwoColumn({ + controller: this, + model: this.model + }); + + /** + * Attach a subview to display fields added via the + * `attachment_fields_to_edit` filter. + */ + contentRegion.view.views.set( '.attachment-compat', new AttachmentCompat({ + controller: this, + model: this.model + }) ); + + // Update browser url when navigating media details + if ( this.model ) { + this.gridRouter.navigate( this.gridRouter.baseUrl( '?item=' + this.model.id ) ); + } + }, + + /** + * Render the EditImage view into the frame's content region. + * + * @param {Object} contentRegion Basic object with a `view` property, which + * should be set with the proper region view. + */ + editImageMode: function( contentRegion ) { + var editImageController = new EditImageController( { + model: this.model, + frame: this + } ); + // Noop some methods. + editImageController._toolbar = function() {}; + editImageController._router = function() {}; + editImageController._menu = function() {}; + + contentRegion.view = new DetailsView( { + model: this.model, + frame: this, + controller: editImageController + } ); + }, + + editImageModeRender: function( view ) { + view.on( 'ready', view.loadEditor ); + }, + + toggleNav: function() { + this.$('.left').toggleClass( 'disabled', ! this.hasPrevious() ); + this.$('.right').toggleClass( 'disabled', ! this.hasNext() ); + }, + + /** + * Rerender the view. + */ + rerender: function() { + // Only rerender the `content` region. + if ( this.content.mode() !== 'edit-metadata' ) { + this.content.mode( 'edit-metadata' ); + } else { + this.content.render(); + } + + this.toggleNav(); + }, + + /** + * Click handler to switch to the previous media item. + */ + previousMediaItem: function() { + if ( ! this.hasPrevious() ) { + this.$( '.left' ).blur(); + return; + } + this.model = this.library.at( this.getCurrentIndex() - 1 ); + this.rerender(); + this.$( '.left' ).focus(); + }, + + /** + * Click handler to switch to the next media item. + */ + nextMediaItem: function() { + if ( ! this.hasNext() ) { + this.$( '.right' ).blur(); + return; + } + this.model = this.library.at( this.getCurrentIndex() + 1 ); + this.rerender(); + this.$( '.right' ).focus(); + }, + + getCurrentIndex: function() { + return this.library.indexOf( this.model ); + }, + + hasNext: function() { + return ( this.getCurrentIndex() + 1 ) < this.library.length; + }, + + hasPrevious: function() { + return ( this.getCurrentIndex() - 1 ) > -1; + }, + /** + * Respond to the keyboard events: right arrow, left arrow, except when + * focus is in a textarea or input field. + */ + keyEvent: function( event ) { + if ( ( 'INPUT' === event.target.nodeName || 'TEXTAREA' === event.target.nodeName ) && ! ( event.target.readOnly || event.target.disabled ) ) { + return; + } + + // The right arrow key + if ( 39 === event.keyCode ) { + this.nextMediaItem(); + } + // The left arrow key + if ( 37 === event.keyCode ) { + this.previousMediaItem(); + } + }, + + resetRoute: function() { + this.gridRouter.navigate( this.gridRouter.baseUrl( '' ) ); + } +}); + +module.exports = EditAttachments; +},{"../../controllers/edit-attachment-metadata.js":1,"../../controllers/edit-image.js":2,"../attachment-compat.js":14,"../attachment/details-two-column.js":20,"../edit-image-details.js":29,"../frame.js":32,"../media-frame.js":38,"../modal.js":41}],34:[function(require,module,exports){ +/*globals _, Backbone, wp, jQuery */ + +/** + * wp.media.view.MediaFrame.Manage + * + * A generic management frame workflow. + * + * Used in the media grid view. + * + * @constructor + * @augments wp.media.view.MediaFrame + * @augments wp.media.view.Frame + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + * @mixes wp.media.controller.StateMachine + */ +var MediaFrame = require( '../media-frame.js' ), + UploaderWindow = require( '../uploader/window.js' ), + AttachmentsBrowser = require( '../attachments/browser.js' ), + Router = require( '../../routers/manage.js' ), + Library = require( '../../controllers/library.js' ), + $ = jQuery, + Manage; + +Manage = MediaFrame.extend({ + /** + * @global wp.Uploader + */ + initialize: function() { + var self = this; + _.defaults( this.options, { + title: '', + modal: false, + selection: [], + library: {}, // Options hash for the query to the media library. + multiple: 'add', + state: 'library', + uploader: true, + mode: [ 'grid', 'edit' ] + }); + + this.$body = $( document.body ); + this.$window = $( window ); + this.$adminBar = $( '#wpadminbar' ); + this.$window.on( 'scroll resize', _.debounce( _.bind( this.fixPosition, this ), 15 ) ); + $( document ).on( 'click', '.add-new-h2', _.bind( this.addNewClickHandler, this ) ); + + // Ensure core and media grid view UI is enabled. + this.$el.addClass('wp-core-ui'); + + // Force the uploader off if the upload limit has been exceeded or + // if the browser isn't supported. + if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { + this.options.uploader = false; + } + + // Initialize a window-wide uploader. + if ( this.options.uploader ) { + this.uploader = new UploaderWindow({ + controller: this, + uploader: { + dropzone: document.body, + container: document.body + } + }).render(); + this.uploader.ready(); + $('body').append( this.uploader.el ); + + this.options.uploader = false; + } + + this.gridRouter = new Router(); + + // Call 'initialize' directly on the parent class. + MediaFrame.prototype.initialize.apply( this, arguments ); + + // Append the frame view directly the supplied container. + this.$el.appendTo( this.options.container ); + + this.createStates(); + this.bindRegionModeHandlers(); + this.render(); + + // Update the URL when entering search string (at most once per second) + $( '#media-search-input' ).on( 'input', _.debounce( function(e) { + var val = $( e.currentTarget ).val(), url = ''; + if ( val ) { + url += '?search=' + val; + } + self.gridRouter.navigate( self.gridRouter.baseUrl( url ) ); + }, 1000 ) ); + }, + + /** + * Create the default states for the frame. + */ + createStates: function() { + var options = this.options; + + if ( this.options.states ) { + return; + } + + // Add the default states. + this.states.add([ + new Library({ + library: wp.media.query( options.library ), + multiple: options.multiple, + title: options.title, + content: 'browse', + toolbar: 'select', + contentUserSetting: false, + filterable: 'all', + autoSelect: false + }) + ]); + }, + + /** + * Bind region mode activation events to proper handlers. + */ + bindRegionModeHandlers: function() { + this.on( 'content:create:browse', this.browseContent, this ); + + // Handle a frame-level event for editing an attachment. + this.on( 'edit:attachment', this.openEditAttachmentModal, this ); + + this.on( 'select:activate', this.bindKeydown, this ); + this.on( 'select:deactivate', this.unbindKeydown, this ); + }, + + handleKeydown: function( e ) { + if ( 27 === e.which ) { + e.preventDefault(); + this.deactivateMode( 'select' ).activateMode( 'edit' ); + } + }, + + bindKeydown: function() { + this.$body.on( 'keydown.select', _.bind( this.handleKeydown, this ) ); + }, + + unbindKeydown: function() { + this.$body.off( 'keydown.select' ); + }, + + fixPosition: function() { + var $browser, $toolbar; + if ( ! this.isModeActive( 'select' ) ) { + return; + } + + $browser = this.$('.attachments-browser'); + $toolbar = $browser.find('.media-toolbar'); + + // Offset doesn't appear to take top margin into account, hence +16 + if ( ( $browser.offset().top + 16 ) < this.$window.scrollTop() + this.$adminBar.height() ) { + $browser.addClass( 'fixed' ); + $toolbar.css('width', $browser.width() + 'px'); + } else { + $browser.removeClass( 'fixed' ); + $toolbar.css('width', ''); + } + }, + + /** + * Click handler for the `Add New` button. + */ + addNewClickHandler: function( event ) { + event.preventDefault(); + this.trigger( 'toggle:upload:attachment' ); + }, + + /** + * Open the Edit Attachment modal. + */ + openEditAttachmentModal: function( model ) { + // Create a new EditAttachment frame, passing along the library and the attachment model. + wp.media( { + frame: 'edit-attachments', + controller: this, + library: this.state().get('library'), + model: model + } ); + }, + + /** + * Create an attachments browser view within the content region. + * + * @param {Object} contentRegion Basic object with a `view` property, which + * should be set with the proper region view. + * @this wp.media.controller.Region + */ + browseContent: function( contentRegion ) { + var state = this.state(); + + // Browse our library of attachments. + this.browserView = contentRegion.view = new AttachmentsBrowser({ + controller: this, + collection: state.get('library'), + selection: state.get('selection'), + model: state, + sortable: state.get('sortable'), + search: state.get('searchable'), + filters: state.get('filterable'), + date: state.get('date'), + display: state.get('displaySettings'), + dragInfo: state.get('dragInfo'), + sidebar: 'errors', + + suggestedWidth: state.get('suggestedWidth'), + suggestedHeight: state.get('suggestedHeight'), + + AttachmentView: state.get('AttachmentView'), + + scrollElement: document + }); + this.browserView.on( 'ready', _.bind( this.bindDeferred, this ) ); + + this.errors = wp.Uploader.errors; + this.errors.on( 'add remove reset', this.sidebarVisibility, this ); + }, + + sidebarVisibility: function() { + this.browserView.$( '.media-sidebar' ).toggle( !! this.errors.length ); + }, + + bindDeferred: function() { + if ( ! this.browserView.dfd ) { + return; + } + this.browserView.dfd.done( _.bind( this.startHistory, this ) ); + }, + + startHistory: function() { + // Verify pushState support and activate + if ( window.history && window.history.pushState ) { + Backbone.history.start( { + root: _wpMediaGridSettings.adminUrl, + pushState: true + } ); + } + } +}); + +module.exports = Manage; +},{"../../controllers/library.js":3,"../../routers/manage.js":12,"../attachments/browser.js":24,"../media-frame.js":38,"../uploader/window.js":54}],35:[function(require,module,exports){ +/** + * wp.media.view.Iframe + * + * @class + * @augments wp.media.View + * @augments wp.Backbone.View + * @augments Backbone.View + */ +var View = require( './view.js' ), + Iframe; + +Iframe = View.extend({ + className: 'media-iframe', + /** + * @returns {wp.media.view.Iframe} Returns itself to allow chaining + */ + render: function() { + this.views.detach(); + this.$el.html( '