diff --git a/src/wp-admin/upload.php b/src/wp-admin/upload.php index ef13310b90..9903ae6a91 100644 --- a/src/wp-admin/upload.php +++ b/src/wp-admin/upload.php @@ -24,16 +24,8 @@ if ( 'grid' === $mode ) { wp_enqueue_media(); wp_enqueue_script( 'media-grid' ); wp_enqueue_script( 'media' ); - + require_once( ABSPATH . 'wp-admin/admin-header.php' ); - ?>
.media-button, .attachments-browser .media-toolbar-primary > .media-button-group, .attachments-browser .media-toolbar-secondary > .media-button, @@ -942,6 +986,92 @@ outline: none; } +.inline-toolbar { + position: absolute; + top: 0; + left: 0; + display: none; + z-index: 100; +} + +.inline-toolbar .remove { + display: none; +} + +.active-video .inline-toolbar .remove { + display: inline-block; +} + +.attachment:hover .inline-toolbar { + display: block; +} + +.inline-toolbar div, +.inline-toolbar .inline-media-control { + display: inline-block; + margin-top: 4px; + margin-left: 4px; + padding: 2px; + width: 20px; + height: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.5); + background-color: #000; + background-color: rgba(0,0,0,0.9); + cursor: pointer; + color: white; + font-size: 20px; +} + +.ie8 .inline-toolbar div, +.ie7 .inline-toolbar div { + display: inline; + padding: 0; +} + +.inline-media-control span { + display: block; + width: 16px; + height: 16px; + margin: 2px; + background: url(/wp-includes/js/mediaelement/controls.png) 0 0 no-repeat; +} + +.inline-media-control.active span { + margin: 2px; + background-position: 0 -16px; +} + +.inline-media-control.paused span { + margin: 2px; + background-position: 0 0; +} + +audio#inline-media-node { + display: none; +} + +video#inline-media-node { + position: relative; + z-index: 5; + top: 0; + left: 0; +} + +.inline-video-wrap { + width: 100%; + height: auto; + position: absolute; + z-index: 5; + background: #000; + padding: 10px 0 5px; + top: 0; + left: 0; +} + +.attachments-browser.hide-sidebar .attachments { + right: 0; +} + .attachments-browser .instructions { display: inline-block; margin-top: 16px; @@ -2388,11 +2518,11 @@ line-height: 29px; } -.media-grid-view-switch { - position: fixed; - right: 10px; - top: 44px; - z-index: 300; +.media-grid-view .view-switch { + display: inline-block; + float: none; + margin-top: 13px; + vertical-align: middle; } /** @@ -2427,7 +2557,221 @@ display: none; } +/** + * Copied styles from the Add theme toolbar. + * + * This should be OOCSS'd so both use a shared selector. + */ +.media-grid-view .media-toolbar { + background: #fff; + -webkit-box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); + box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + color: #555; + display: inline-block; + font-size: 13px; + padding: 0 20px; + position: relative; + width: 100%; +} + +/** + * The left and right buttons are copied from the expanded theme details modal. + * + * This should be OOCSS'd so both use a shared selector. + */ +.edit-attachment-frame .edit-media-header .left, +.edit-attachment-frame .edit-media-header .right { + cursor: pointer; + color: #777; + background-color: transparent; + height: 48px; + width: 54px; + float: left; + text-align: center; + border: 0; + border-right: 1px solid #ddd; +} + +.edit-attachment-frame .edit-media-header .right:before, +.edit-attachment-frame .edit-media-header .left:before { + font: normal 20px/50px 'dashicons' !important; + display: inline; + font-weight: 300; +} + + +.edit-attachment-frame .edit-media-header .left:before { + content: '\f340'; +} + +.edit-attachment-frame .edit-media-header .right:before { + content: '\f344'; +} + +.edit-attachment-frame .edit-media-header .left.disabled, +.edit-attachment-frame .edit-media-header .right.disabled, +.edit-attachment-frame .edit-media-header .left.disabled:hover, +.edit-attachment-frame .edit-media-header .right.disabled:hover { + color: #ccc; + background: inherit; + cursor: inherit; +} + +.edit-attachment-frame .edit-media-header .close:hover, +.edit-attachment-frame .edit-media-header .right:hover, +.edit-attachment-frame .edit-media-header .left:hover, +.edit-attachment-frame .edit-media-header .close:focus, +.edit-attachment-frame .edit-media-header .right:focus, +.edit-attachment-frame .edit-media-header .left:focus { + background: #0074a2; + color: #fff; +} + +.edit-attachment-frame .media-frame-content, +.edit-attachment-frame .media-frame-router { + left: 0; +} + +/* Hiding this for the moment instead of removing it from the template. */ +.edit-attachment-frame h3 { + display: none; +} + +.edit-attachment-frame .attachment-details { + position: absolute; + overflow: auto; + top: 0; + bottom: 0; + right: 0; + left: 0; +} + +.edit-attachment-frame .attachment-info { + border-bottom: 0; + border-right: 1px solid #ddd; + bottom: 0; + position: absolute; + top: 0; + left: 0; + margin-bottom: 0; + padding: 2% 4%; + right: 50%; +} + +.edit-attachment-frame .attachment-info .thumbnail { + max-width: none; + max-height: none; +} + +.edit-attachment-frame .attachment-info .thumbnail-image img { + margin: 0; +} + +.edit-attachment-frame .attachment-info .thumbnail-image:after { + -webkit-box-shadow: none; + box-shadow: none; +} + +.edit-attachment-frame .attachment-info .thumbnail img { + max-width: none; + max-height: 50%; +} + +.edit-attachment-frame .attachment-info .details { + float: none; +} + +.edit-attachment-frame .wp-media-wrapper { + margin-top: 20px; +} + +.edit-attachment-frame .attachment-fields { + bottom: 0; + padding: 2% 4%; + position: absolute; + top: 0; + left: 50%; + right: 0; +} + +.edit-attachment-frame .attachment-fields .setting { + display: block; + float: left; + width: 100%; + margin: 1px 0; +} + +.edit-attachment-frame .attachment-fields .setting label { + display: block; +} + +.edit-attachment-frame .attachment-fields .setting .link-to-custom { + margin: 3px 0; +} + +.edit-attachment-frame .attachment-fields .setting .name { + min-width: 30%; + margin-right: 4%; + font-size: 12px; + text-align: right; +} + +.edit-attachment-frame .attachment-fields .setting select { + max-width: 65%; +} + +.edit-attachment-frame .attachment-fields .setting input[type="checkbox"], +.edit-attachment-frame .attachment-fields .field input[type="checkbox"] { + width: 16px; + float: none; + margin: 8px 3px 0; + padding: 0; +} + +.edit-attachment-frame .attachment-fields .setting span { + float: left; + min-height: 22px; + padding-top: 8px; + line-height: 16px; + font-weight: normal; + color: #666; +} + +.edit-attachment-frame .attachment-fields .setting input[type="text"], +.edit-attachment-frame .attachment-fields .setting input[type="password"], +.edit-attachment-frame .attachment-fields .setting input[type="number"], +.edit-attachment-frame .attachment-fields .setting input[type="search"], +.edit-attachment-frame .attachment-fields .setting input[type="email"], +.edit-attachment-frame .attachment-fields .setting input[type="url"], +.edit-attachment-frame .attachment-fields .setting textarea, +.edit-attachment-frame .attachment-fields .setting .value { + margin: 1px; + width: 65%; + float: right; + padding: 6px 8px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.edit-attachment-frame .attachment-fields .setting textarea { + height: 62px; + resize: vertical; +} + +.edit-attachment-frame .attachment-fields select { + margin-top: 3px; +} + +.media-grid-view.hide-router .media-frame-title { + box-shadow: none; +} + .media-grid-view .media-frame-content { + background-color: transparent; bottom: 40px; } @media screen and (max-width: 782px) { diff --git a/src/wp-includes/js/media-audiovideo.js b/src/wp-includes/js/media-audiovideo.js index d8958319a0..7af10bfb89 100644 --- a/src/wp-includes/js/media-audiovideo.js +++ b/src/wp-includes/js/media-audiovideo.js @@ -26,6 +26,17 @@ } }, + removeAllPlayers: function() { + var p; + + if ( window.mejs && window.mejs.players ) { + for ( p in window.mejs.players ) { + window.mejs.players[p].pause(); + this.removePlayer( window.mejs.players[p] ); + } + } + }, + /** * Pauses the current object's instances of MediaElementPlayer */ diff --git a/src/wp-includes/js/media-grid.js b/src/wp-includes/js/media-grid.js index 1ba6e1ee0e..e97a97224b 100644 --- a/src/wp-includes/js/media-grid.js +++ b/src/wp-includes/js/media-grid.js @@ -1,4 +1,4 @@ -(function( $, _, Backbone, wp ) { +(function($, _, Backbone, wp) { var media = wp.media, l10n; // Link any localized strings. @@ -9,6 +9,96 @@ delete l10n.settings; } + /** + * A state for editing (cropping, etc.) an image. + * + * @constructor + * @augments wp.media.controller.State + * @augments Backbone.Model + */ + media.controller.EditImageNoFrame = media.controller._State.extend({ + defaults: { + id: 'edit-attachment', + title: l10n.editImage, + // Region mode defaults. + menu: false, + router: 'edit-metadata', + content: 'edit-metadata', + toolbar: 'toolbar', + + url: '' + }, + + initialize: function() { + media.controller._State.prototype.initialize.apply( this, arguments ); + }, + + activate: function() { + this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); + }, + + _postActivate: function() { + this._content(); + this._router(); + }, + + deactivate: function() { + this.stopListening( this.frame ); + }, + + toolbar: function() { + var frame = this.frame, + lastState = frame.lastState(), + previous = lastState && lastState.id; + + frame.toolbar.set( new media.view.Toolbar({ + controller: frame, + items: { + back: { + style: 'primary', + text: l10n.back, + priority: 20, + click: function() { + if ( previous ) { + frame.setState( previous ); + } else { + frame.close(); + } + } + } + } + }) ); + }, + + /** + * @access private + */ + _router: function() { + var router = this.frame.router, + mode = this.get('router'), + view; + + this.frame.$el.toggleClass( 'hide-router', ! mode ); + if ( ! mode ) { + return; + } + + this.frame.router.render( mode ); + + view = router.get(); + if ( view && view.select ) { + view.select( this.frame.content.mode() ); + } + }, + + _content: function() { + var mode = this.get( 'content' ); + if ( mode ) { + this.frame[ 'content' ].render( mode ); + } + } + }); + /** * wp.media.view.MediaFrame.Manage * @@ -36,7 +126,8 @@ library: {}, multiple: false, state: 'library', - uploader: true + uploader: true, + mode: [ 'grid', 'edit' ] }); // Ensure core and media grid view UI is enabled. @@ -111,15 +202,56 @@ router: false, content: 'browse', filterable: 'mime-types' - }), - - new media.controller.EditImage( { model: options.editImage } ) + }) ]); }, bindHandlers: function() { this.on( 'content:create:browse', this.browseContent, this ); this.on( 'content:render:edit-image', this.editImageContent, this ); + + // Handle a frame-level event for editing an attachment. + this.on( 'edit:attachment', this.editAttachment, this ); + this.on( 'edit:attachment:next', this.editNextAttachment, this ); + this.on( 'edit:attachment:previous', this.editPreviousAttachment, this ); + }, + + editPreviousAttachment: function( currentModel ) { + var library = this.state().get('library'), + currentModelIndex = library.indexOf( currentModel ); + this.trigger( 'edit:attachment', library.at( currentModelIndex - 1 ) ); + }, + + editNextAttachment: function( currentModel ) { + var library = this.state().get('library'), + currentModelIndex = library.indexOf( currentModel ); + this.trigger( 'edit:attachment', library.at( currentModelIndex + 1 ) ); + }, + + /** + * Open the Edit Attachment modal. + */ + editAttachment: function( model ) { + var library = this.state().get('library'), hasPrevious, hasNext; + if ( library.indexOf( model ) > 0 ) { + hasPrevious = true; + } + else { + hasPrevious = false; + } + if ( library.indexOf( model ) < library.length - 1 ) { + hasNext = true; + } + else { + hasNext = false; + } + + new media.view.Frame.EditAttachment({ + hasPrevious: hasPrevious, + hasNext: hasNext, + model: model, + gridController: this + }); }, /** @@ -143,6 +275,7 @@ display: state.get('displaySettings'), dragInfo: state.get('dragInfo'), bulkEdit: true, + sidebar: false, suggestedWidth: state.get('suggestedWidth'), suggestedHeight: state.get('suggestedHeight'), @@ -162,5 +295,194 @@ } }); - -}( jQuery, _, Backbone, wp )); \ No newline at end of file + + media.view.Attachment.Details.TwoColumn = media.view.Attachment.Details.extend({ + template: wp.template( 'attachment-details-two-column' ), + + initialize: function() { + this.$el.attr('aria-label', this.model.attributes.title).attr('aria-checked', false); + this.model.on( 'change:sizes change:uploading', this.render, this ); + this.model.on( 'change:title', this._syncTitle, this ); + this.model.on( 'change:caption', this._syncCaption, this ); + this.model.on( 'change:percent', this.progress, this ); + + // Update the selection. + this.model.on( 'add', this.select, this ); + this.model.on( 'remove', this.deselect, this ); + }, + + render: function() { + media.view.Attachment.Details.prototype.render.apply( this, arguments ); + + media.mixin.removeAllPlayers(); + $( 'audio, video', this.$el ).each( function (i, elem) { + var el = media.view.MediaDetails.prepareSrc( elem ); + new MediaElementPlayer( el, media.mixin.mejsSettings ); + } ); + } + }); + + /** + * 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`. + */ + media.view.Frame.EditAttachment = media.view.Frame.extend({ + + className: 'edit-attachment-frame', + template: media.template( 'edit-attachment-frame' ), + regions: [ 'router', 'content' ], + + events: { + 'click': 'collapse', + 'click .delete-media-item': 'deleteMediaItem', + 'click .left': 'previousMediaItem', + 'click .right': 'nextMediaItem' + }, + + initialize: function( options ) { + var self = this; + media.view.Frame.prototype.initialize.apply( this, arguments ); + + _.defaults( this.options, { + modal: true, + state: 'edit-attachment' + }); + + this.createStates(); + + this.on( 'content:render:edit-metadata', this.editMetadataContent, this ); + this.on( 'content:render:edit-image', this.editImageContentUgh, this ); + + // Only need a tab to Edit Image for images. + if ( this.model.get( 'type' ) === 'image' ) { + this.on( 'router:create', this.createRouter, this ); + this.on( 'router:render', this.browseRouter, this ); + } + + // Initialize modal container view. + if ( this.options.modal ) { + this.modal = new media.view.Modal({ + controller: this, + title: this.options.title + }); + + // Completely destroy the modal DOM element when closing it. + this.modal.close = function() { + self.modal.remove(); + }; + + this.modal.content( this ); + this.modal.open(); + } + }, + + /** + * Add the default states to the frame. + */ + createStates: function() { + this.states.add([ + new media.controller.EditImageNoFrame( { model: this.model } ) + ]); + }, + + /** + * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining + */ + render: function() { + // Activate the default state if no active state exists. + if ( ! this.state() && this.options.state ) { + this.setState( this.options.state ); + } + /** + * call 'render' directly on the parent class + */ + return media.view.Frame.prototype.render.apply( this, arguments ); + }, + + /** + * Content region rendering callback for the `edit-metadata` mode. + */ + editMetadataContent: function() { + var view = new media.view.Attachment.Details.TwoColumn({ + controller: this, + model: this.model + }); + this.content.set( view ); + }, + + /** + * For some reason the view doesn't exist in the DOM yet, don't have the + * patience to track this down right now. + */ + editImageContentUgh: function() { + _.defer( _.bind( this.editImageContent, this ) ); + }, + + /** + * Render the EditImage view into the frame's content region. + */ + editImageContent: function() { + var view = new media.view.EditImage( { model: this.model, controller: this } ).render(); + + this.content.set( view ); + + // after creating the wrapper view, load the actual editor via an ajax call + view.loadEditor(); + }, + + /** + * Create the router view. + * + * @param {Object} router + * @this wp.media.controller.Region + */ + createRouter: function( router ) { + router.view = new media.view.Router({ + controller: this + }); + }, + + /** + * Router rendering callback. + * + * @param media.view.Router view Instantiated in this.createRouter() + */ + browseRouter: function( view ) { + view.set({ + 'edit-metadata': { + text: 'Edit Metadata', + priority: 20 + }, + 'edit-image': { + text: 'Edit Image', + priority: 40 + } + }); + }, + + /** + * Click handler to switch to the previous media item. + */ + previousMediaItem: function() { + if ( ! this.options.hasPrevious ) + return; + this.modal.close(); + this.options.gridController.trigger( 'edit:attachment:previous', this.model ); + }, + + /** + * Click handler to switch to the next media item. + */ + nextMediaItem: function() { + if ( ! this.options.hasNext ) + return; + this.modal.close(); + this.options.gridController.trigger( 'edit:attachment:next', this.model ); + } + + }); + +}(jQuery, _, Backbone, wp)); \ No newline at end of file diff --git a/src/wp-includes/js/media-views.js b/src/wp-includes/js/media-views.js index 852cb85263..5220fada8a 100644 --- a/src/wp-includes/js/media-views.js +++ b/src/wp-includes/js/media-views.js @@ -337,16 +337,11 @@ }); /** - * wp.media.controller.State - * - * A state is a step in a workflow that when set will trigger the controllers - * for the regions to be updated as specified in the frame. This is the base - * class that the various states used in wp.media extend. - * - * @constructor - * @augments Backbone.Model + * A more abstracted state, because media.controller.State expects + * specific regions (menu, title, etc.) to exist on the frame, which do not + * exist in media.view.Frame.EditAttachment. */ - media.controller.State = Backbone.Model.extend({ + media.controller._State = Backbone.Model.extend({ constructor: function() { this.on( 'activate', this._preActivate, this ); this.on( 'activate', this.activate, this ); @@ -354,13 +349,11 @@ this.on( 'deactivate', this._deactivate, this ); this.on( 'deactivate', this.deactivate, this ); this.on( 'reset', this.reset, this ); - this.on( 'ready', this._ready, this ); this.on( 'ready', this.ready, this ); /** * Call parent constructor with passed arguments */ Backbone.Model.apply( this, arguments ); - this.on( 'change:menu', this._updateMenu, this ); }, /** @@ -382,15 +375,55 @@ /** * @access private */ - _ready: function() { - this._updateMenu(); + _preActivate: function() { + this.active = true; }, /** * @access private */ - _preActivate: function() { - this.active = true; + _postActivate: function() {}, + /** + * @access private + */ + _deactivate: function() { + this.active = false; + } + }); + + /** + * wp.media.controller.State + * + * A state is a step in a workflow that when set will trigger the controllers + * for the regions to be updated as specified in the frame. This is the base + * class that the various states used in wp.media extend. + * + * @constructor + * @augments Backbone.Model + */ + media.controller.State = media.controller._State.extend({ + constructor: function() { + this.on( 'activate', this._preActivate, this ); + this.on( 'activate', this.activate, this ); + this.on( 'activate', this._postActivate, this ); + this.on( 'deactivate', this._deactivate, this ); + this.on( 'deactivate', this.deactivate, this ); + this.on( 'reset', this.reset, this ); + this.on( 'ready', this._ready, this ); + this.on( 'ready', this.ready, this ); + /** + * Call parent constructor with passed arguments + */ + Backbone.Model.apply( this, arguments ); + this.on( 'change:menu', this._updateMenu, this ); }, + + /** + * @access private + */ + _ready: function() { + this._updateMenu(); + }, + /** * @access private */ @@ -1758,7 +1791,8 @@ _.defaults( this.options, { title: '', modal: true, - uploader: true + uploader: true, + mode: ['select'] }); // Ensure core UI is enabled. @@ -4530,7 +4564,7 @@ var selection = this.options.selection; this.$el.attr('aria-label', this.model.attributes.title).attr('aria-checked', false); - this.model.on( 'change:sizes change:uploading', this.render, this ); + this.model.on( 'change', this.render, this ); this.model.on( 'change:title', this._syncTitle, this ); this.model.on( 'change:caption', this._syncCaption, this ); this.model.on( 'change:percent', this.progress, this ); @@ -4583,7 +4617,7 @@ compat: false, alt: '', description: '' - }); + }, this.options ); options.buttons = this.buttons; options.describe = this.controller.state().get('describe'); @@ -4633,11 +4667,17 @@ */ toggleSelectionHandler: function( event ) { var method; - // Catch enter and space events if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { return; } + + // In the grid view, bubble up an edit:attachment event to the controller. + if ( _.contains( this.controller.options.mode, 'grid' ) ) { + this.controller.trigger( 'edit:attachment', this.model ); + return; + } + if ( event.shiftKey ) { method = 'between'; } else if ( event.ctrlKey || event.metaKey ) { @@ -5168,10 +5208,11 @@ */ createAttachmentView: function( attachment ) { var view = new this.options.AttachmentView({ - controller: this.controller, - model: attachment, - collection: this.collection, - selection: this.options.selection + controller: this.controller, + model: attachment, + collection: this.collection, + selection: this.options.selection, + showAttachmentFields: this.options.showAttachmentFields }); return this._viewsByCid[ attachment.cid ] = view; @@ -5468,7 +5509,6 @@ } }); - /** * wp.media.view.AttachmentsBrowser * @@ -5486,13 +5526,18 @@ filters: false, search: true, display: false, - + sidebar: true, + showAttachmentFields: getUserSetting( 'showAttachmentFields', [ 'title', 'uploadedTo', 'dateFormatted', 'mime' ] ), AttachmentView: media.view.Attachment.Library }); this.createToolbar(); this.updateContent(); - this.createSidebar(); + if ( this.options.sidebar ) { + this.createSidebar(); + } else { + this.$el.addClass( 'hide-sidebar' ); + } this.collection.on( 'add remove reset', this.updateContent, this ); }, @@ -5517,6 +5562,20 @@ this.views.add( this.toolbar ); + // 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 ( _.contains( this.controller.options.mode, 'grid' ) ) { + var libraryViewSwitcherConstructor = media.View.extend({ + className: 'view-switch media-grid-view-switch', + template: media.template( 'media-library-view-switcher') + }); + this.toolbar.set( 'libraryViewSwitcher', new libraryViewSwitcherConstructor({ + controller: this.controller, + priority: -90 + }).render() ); + } + filters = this.options.filters; if ( 'uploaded' === filters ) { FiltersConstructor = media.view.AttachmentFilters.Uploaded; @@ -5611,11 +5670,12 @@ this.removeContent(); this.attachments = new media.view.Attachments({ - controller: this.controller, - collection: this.collection, - selection: this.options.selection, - model: this.model, - sortable: this.options.sortable, + controller: this.controller, + collection: this.collection, + selection: this.options.selection, + model: this.model, + sortable: this.options.sortable, + showAttachmentFields: this.options.showAttachmentFields, // The single `Attachment` view to be used in the `Attachments` view. AttachmentView: this.options.AttachmentView diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 2c0ff80762..c950af6079 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -220,6 +220,15 @@ function wp_print_media_templates() { + + + + + +