From 61a9471024d1effdcf417a7aa013e0b426be49be Mon Sep 17 00:00:00 2001 From: Ryan Boren Date: Tue, 4 Dec 2012 01:26:03 +0000 Subject: [PATCH] Final round of media UX improvements. Props koopersmith fixes #21390 #22502 git-svn-id: http://core.svn.wordpress.org/trunk@23006 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/js/custom-background.js | 4 +- wp-admin/js/custom-header.js | 4 +- wp-includes/css/media-views-rtl.css | 79 +- wp-includes/css/media-views.css | 358 +++++---- wp-includes/js/media-editor.js | 10 +- wp-includes/js/media-views.js | 1158 ++++++++++++++++----------- wp-includes/media.php | 37 +- 7 files changed, 930 insertions(+), 720 deletions(-) diff --git a/wp-admin/js/custom-background.js b/wp-admin/js/custom-background.js index 7656ce455e..594bff3aef 100644 --- a/wp-admin/js/custom-background.js +++ b/wp-admin/js/custom-background.js @@ -37,8 +37,8 @@ } }); - frame.toolbar.on( 'activate:select', function() { - frame.toolbar.view().set({ + frame.on( 'toolbar:render:select', function( view ) { + view.set({ select: { style: 'primary', text: $el.data('update'), diff --git a/wp-admin/js/custom-header.js b/wp-admin/js/custom-header.js index 40c0128249..fa7f1e4f88 100644 --- a/wp-admin/js/custom-header.js +++ b/wp-admin/js/custom-header.js @@ -24,8 +24,8 @@ } }); - frame.toolbar.on( 'activate:select', function() { - frame.toolbar.view().set({ + frame.on( 'toolbar:render:select', function( view ) { + view.set({ select: { style: 'primary', text: $el.data('update'), diff --git a/wp-includes/css/media-views-rtl.css b/wp-includes/css/media-views-rtl.css index 7eef9514f0..195ab08b4c 100644 --- a/wp-includes/css/media-views-rtl.css +++ b/wp-includes/css/media-views-rtl.css @@ -1,25 +1,14 @@ /** * Modal */ -.media-modal-title { - left: auto; - right: 0; -} - .media-modal-close { right: auto; - left: 0; + left: 7px; } /** * Toolbar */ -.media-frame-toolbar > .media-toolbar { - left: 0; - right: 200px; -} - - .media-toolbar-primary { float: left; } @@ -93,21 +82,51 @@ * Menu */ .media-menu { - left: auto; - right: 0; border-right: 0; border-left: 1px solid #d9d9d9; box-shadow: inset 6px 0 6px -6px rgba( 0, 0, 0, 0.2 ) } +/** + * Router + */ +.media-router > a { + float: right; + border-right: 0; + border-left: 1px solid #dfdfdf; +} +.media-router > a:last-child { + border-left: 0; +} + /** * Frame */ -.media-frame .region-content { +.media-frame-menu { + left: auto; + right: 0; +} + +.media-frame-title, +.media-frame-router, +.media-frame-content, +.media-frame-toolbar { left: 0; right: 200px; } +.media-frame.hide-menu .media-frame-title, +.media-frame.hide-menu .media-frame-router, +.media-frame.hide-menu .media-frame-toolbar, +.media-frame.hide-menu .media-frame-content { + right: 0; +} + +.media-frame.hide-menu .media-frame-menu { + left: auto; + right: -200px; +} + /** * Attachment Browser Filters */ @@ -225,30 +244,6 @@ background-image: linear-gradient(to right, rgba( 255, 255, 255, 1 ) , rgba( 255, 255, 255, 0 ) ); } -/** - * Selection Preview - */ -.selected-img { - float: right; - margin-right: 0; - margin-left: 14px; -} - -.selection-preview img { - float: right; - margin-left: 0; - margin-right: 1px; -} - -.selection-preview .count { - right: auto; - left: 0; -} - -.selection-preview .clear-selection { - float: right; -} - /** * Attachment Details */ @@ -289,8 +284,10 @@ * Responsive layout */ @media only screen and (max-width: 900px) { - .media-frame .region-content, - .media-frame-toolbar > .media-toolbar { + .media-frame-title, + .media-frame-router, + .media-frame-content, + .media-frame-toolbar { left: 0; right: 140px; } diff --git a/wp-includes/css/media-views.css b/wp-includes/css/media-views.css index 9df32550e6..b3a31bf2a4 100644 --- a/wp-includes/css/media-views.css +++ b/wp-includes/css/media-views.css @@ -80,10 +80,10 @@ */ .media-modal { position: fixed; - top: 60px; - left: 40px; - right: 40px; - bottom: 40px; + top: 30px; + left: 30px; + right: 30px; + bottom: 30px; z-index: 160000; } @@ -94,42 +94,24 @@ right: 0; bottom: 0; background: #000; - opacity: 0.8; + opacity: 0.7; z-index: 159900; } -.media-modal-backdrop div, -.uploader-window-content { - position: absolute; - top: 10px; - left: 10px; - right: 10px; - bottom: 10px; - border: 1px dashed rgba( 255, 255, 255, 0.5 ); -} - -.media-modal-title { - position: absolute; - top: -40px; - left: 0; - height: 40px; - padding: 0; - margin: 0; - - line-height: 40px; - color: #fff; - font-size: 16px; - font-weight: 200; - text-shadow: 0 0 16px rgba( 0, 0, 0, 0.6 ); -} - .media-modal-close { position: absolute; - top: -27px; - right: 0; - height: 15px; + top: 7px; + right: 7px; + width: 30px; + height: 30px; + z-index: 1000; +} +.media-modal-close span { + display: block; + margin: 8px auto 0; width: 15px; - background-position: -80px 0; + height: 15px; + background-position: -100px 0; } .media-modal-close:active { @@ -165,18 +147,6 @@ border: 0 solid #dfdfdf; } -.media-frame-toolbar > .media-toolbar { - top: auto; - left: 200px; - bottom: 0; - border-width: 1px 0 0 0; - box-shadow: 0 -4px 4px -4px rgba( 0, 0, 0, 0.1 ); -} - -.hide-toolbar .media-frame-toolbar > .media-toolbar { - bottom: -61px; -} - .media-toolbar-primary { float: right; } @@ -238,11 +208,6 @@ width: 100%; } -.media-sidebar .selection-preview { - display: block; - padding-top: 5px; -} - .media-sidebar h3 { position: relative; font-weight: bold; @@ -361,11 +326,10 @@ position: absolute; top: 0; left: 0; + right: 0; bottom: 0; - width: 199px; margin: 0; padding: 16px 0; - z-index: 200; border-right: 1px solid #d9d9d9; box-shadow: inset -6px 0 6px -6px rgba( 0, 0, 0, 0.2 ); -webkit-user-select: none; @@ -374,7 +338,8 @@ user-select: none; } -.media-menu li { +.media-menu > a { + display: block; position: relative; padding: 4px 20px; margin: 0; @@ -382,16 +347,18 @@ font-size: 14px; color: #21759B; text-shadow: 0 1px 0 #fff; + text-decoration: none; } -.media-menu-item { - cursor: pointer; -} - -.media-menu li:hover { +.media-menu > a:hover { + color: #21759B; background: rgba( 0, 0, 0, 0.04 ); } +.media-menu > a:active { + outline: none; +} + .media-menu .active, .media-menu .active:hover { color: #333; @@ -406,6 +373,64 @@ border-bottom: 1px solid #fff; } +/** + * Menu + */ +.media-router { + position: relative; + padding: 0 6px; + margin: 0; + clear: both; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.media-router > a { + position: relative; + float: left; + padding: 2px 10px; + margin: 0; + height: 18px; + line-height: 18px; + font-size: 14px; + border-right: 1px solid #dfdfdf; + text-shadow: 0 1px 0 #fff; + text-decoration: none; +} + +.media-router > a:last-child { + border-right: 0; +} + +.media-router > a:active, +.media-router > a:focus { + outline: none; +} + +.media-router .active, +.media-router .active:hover { + color: #333; +} + +.media-router .active:after { + content: ''; + display: block; + margin: -100px auto 0; + width: 7px; + height: 7px; + background: #fff; + box-shadow: 1px 1px 1px rgba( 0, 0, 0, 0.2 ); + z-index: 300; + + -webkit-transform: rotate( 45deg ) translate( 75px, 75px ); + -moz-transform: rotate( 45deg ) translate( 75px, 75px ); + -ms-transform: rotate( 45deg ) translate( 75px, 75px ); + -o-transform: rotate( 45deg ) translate( 75px, 75px ); + transform: rotate( 45deg ) translate( 75px, 75px ); +} + /** * Frame */ @@ -418,9 +443,38 @@ bottom: 0; } -.media-frame .region-content { +.media-frame-menu { position: absolute; top: 0; + left: 0; + bottom: 0; + width: 199px; + z-index: 150; +} + +.media-frame-title { + position: absolute; + top: 0; + left: 200px; + right: 0; + height: 45px; + z-index: 200; +} + +.media-frame-router { + position: absolute; + top: 45px; + left: 200px; + right: 0; + height: 30px; + z-index: 200; + border-bottom: 1px solid #dfdfdf; + box-shadow: 0 4px 4px -4px rgba( 0, 0, 0, 0.1 ); +} + +.media-frame-content { + position: absolute; + top: 75px; left: 200px; right: 0; bottom: 61px; @@ -430,14 +484,62 @@ overflow: auto; } -.media-frame.hide-toolbar .region-content { +.media-frame-toolbar { + position: absolute; + left: 200px; + right: 0; bottom: 0; + height: 60px; + z-index: 100; + border: 0 solid #dfdfdf; + border-width: 1px 0 0 0; + box-shadow: 0 -4px 4px -4px rgba( 0, 0, 0, 0.1 ); +} + +.media-frame.hide-menu .media-frame-title, +.media-frame.hide-menu .media-frame-router, +.media-frame.hide-menu .media-frame-toolbar, +.media-frame.hide-menu .media-frame-content { + left: 0; +} + +.media-frame.hide-menu .media-frame-menu { + left: -200px; +} + +.media-frame.hide-toolbar .media-frame-content { + bottom: 0; +} + +.media-frame.hide-toolbar .media-frame-toolbar { + bottom: -61px; +} + +.media-frame.hide-router .media-frame-content { + top: 45px; +} + +.media-frame.hide-router .media-frame-router { + display: none; +} + +.media-frame.hide-router .media-frame-title { + border-bottom: 1px solid #dfdfdf; + box-shadow: 0 4px 4px -4px rgba( 0, 0, 0, 0.1 ); } .media-frame .media-toolbar .add-to-gallery { display: none; } +.media-frame-title h1 { + padding: 0 16px; + font-size: 22px; + font-weight: 200; + line-height: 45px; + margin: 0; +} + /** * Iframes */ @@ -721,6 +823,9 @@ * Attachments Browser */ .media-frame .attachments-browser { + position: relative; + width: 100%; + height: 100%; overflow: hidden; } @@ -903,7 +1008,12 @@ } .uploader-window-content { - border-color: #fff; + position: absolute; + top: 10px; + left: 10px; + right: 10px; + bottom: 10px; + border: 1px dashed #fff; } .uploader-window h3 { @@ -956,6 +1066,10 @@ margin: 4em 0; } +.uploader-inline .has-upload-message .upload-ui { + margin: 0 0 4em; +} + .uploader-inline h3 { font-size: 20px; line-height: 28px; @@ -963,6 +1077,12 @@ margin-bottom: 1.6em; } +.uploader-inline .has-upload-message .upload-instructions { + font-size: 14px; + color: #464646; + font-weight: normal; +} + .uploader-inline .drop-instructions { display: none; } @@ -1058,7 +1178,7 @@ vertical-align: top; } -.media-selection .attachment img { +.media-selection .attachment .icon { width: 50%; } @@ -1097,69 +1217,6 @@ display: none; } -/** - * Selection Preview - */ -.selection-preview { - position: relative; - height: 60px; - overflow: hidden; -} - -.selected-img { - float: left; - position: relative; - margin-right: 14px; -} - -.selection-preview img { - max-width: 40px; - max-height: 40px; - float: left; - margin-top: 6px; - margin-left: 1px; - border: 2px solid white; - box-shadow: - 0 0 0 1px #ccc, - 3px 3px 0 0 #fff, - 3px 3px 0 1px #ccc, - 6px 6px 0 0 #fff, - 6px 6px 0 1px #ccc; -} - -.selection-preview .selected-count-1 img { - margin-top: 8px; - box-shadow: 0 0 0 1px #ccc; -} - -.selection-preview .selected-count-2 img { - margin-top: 7px; - box-shadow: - 0 0 0 1px #ccc, - 3px 3px 0 0 #fff, - 3px 3px 0 1px #ccc; -} - -.selection-preview .count { - position: absolute; - bottom: 0; - right: 0; - height: 16px; - min-width: 8px; - padding: 0 4px; - font-size: 12px; - text-align: center; - font-weight: bold; - color: #999; - background: #fff; - box-shadow: -1px -1px 2px -1px rgba( 0, 0, 0, 0.2 ); -} - -.selection-preview .clear-selection { - float: left; - line-height: 60px; -} - /** * Spinner */ @@ -1297,21 +1354,17 @@ .embed-url { display: block; position: relative; - height: 75px; - padding: 16px 16px; + height: 40px; + padding: 0 16px 16px; margin: 0; - z-index: 50; + z-index: 250; + background: #fff; border-bottom: 1px solid #dfdfdf; box-shadow: 0 4px 4px -4px rgba( 0, 0, 0, 0.1 ); font-size: 18px; font-weight: 200; } -.embed-url span { - display: block; - padding: 4px 0 6px 2px; -} - .media-frame .embed-url input { font-size: 18px; padding: 12px 14px; @@ -1324,7 +1377,7 @@ .embed-image-settings { position: absolute; background: #f5f5f5; - top: 108px; + top: 57px; left: 0; right: 0; bottom: 0; @@ -1388,41 +1441,18 @@ * Responsive layout */ @media only screen and (max-width: 900px) { - .media-modal { - bottom: 20px; - left: 20px; - right: 20px; - top: 40px; - } - - .media-modal-title { - height: 30px; - line-height: 30px; - top: -30px; - } - - .media-modal-close { - top: -23px; - } - - .media-modal-backdrop div, - .uploader-window-content { - top: 5px; - left: 5px; - right: 5px; - bottom: 5px; - } - - .media-menu { + .media-frame-menu { width: 139px; } - .media-menu li { + .media-menu > a { padding: 4px 10px; } - .media-frame .region-content, - .media-frame-toolbar > .media-toolbar { + .media-frame-title, + .media-frame-router, + .media-frame-content, + .media-frame-toolbar { left: 140px; } diff --git a/wp-includes/js/media-editor.js b/wp-includes/js/media-editor.js index a00534c32c..f24479ffd3 100644 --- a/wp-includes/js/media-editor.js +++ b/wp-includes/js/media-editor.js @@ -384,7 +384,7 @@ workflow = workflows[ id ] = wp.media( _.defaults( options || {}, { frame: 'post', - state: 'upload', + state: 'insert', title: wp.media.view.l10n.addMedia, multiple: true } ) ); @@ -408,11 +408,13 @@ }, this ); workflow.state('embed').on( 'select', function() { - var embed = workflow.state().toJSON(); + var state = workflow.state(), + type = state.get('type'), + embed = state.props.toJSON(); embed.url = embed.url || ''; - if ( 'link' === embed.type ) { + if ( 'link' === type ) { _.defaults( embed, { title: embed.url, linkUrl: embed.url @@ -420,7 +422,7 @@ this.send.link( embed ); - } else if ( 'image' === embed.type ) { + } else if ( 'image' === type ) { _.defaults( embed, { title: embed.url, linkUrl: '', diff --git a/wp-includes/js/media-views.js b/wp-includes/js/media-views.js index 89358ec1f1..704005af4e 100644 --- a/wp-includes/js/media-views.js +++ b/wp-includes/js/media-views.js @@ -68,65 +68,71 @@ * wp.media.controller.Region */ media.controller.Region = function( options ) { - _.extend( this, _.pick( options || {}, 'id', 'controller', 'selector' ) ); - - this.on( 'activate:empty', this.empty, this ); - this.mode('empty'); + _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); }; // Use Backbone's self-propagating `extend` inheritance method. media.controller.Region.extend = Backbone.Model.extend; - _.extend( media.controller.Region.prototype, Backbone.Events, { - trigger: (function() { - var eventSplitter = /\s+/, - trigger = Backbone.Events.trigger; - - return function( events ) { - var mode = ':' + this._mode, - modeEvents = events.split( eventSplitter ).join( mode ) + mode; - - trigger.apply( this, arguments ); - trigger.apply( this, [ modeEvents ].concat( _.rest( arguments ) ) ); - return this; - }; - }()), - + _.extend( media.controller.Region.prototype, { mode: function( mode ) { - if ( mode ) { - this.trigger( 'deactivate', this ); - this._mode = mode; - return this.trigger( 'activate', this ); - } - return this._mode; + if ( ! mode ) + return this._mode; + + // Bail if we're trying to change to the current mode. + if ( mode === this._mode ) + return this; + + this.trigger('deactivate'); + this._mode = mode; + this.render( mode ); + this.trigger('activate'); + return this; }, - view: function( view ) { - var previous = this._view, - mode = this._mode, - id = this.id; + render: function( mode ) { + // If no mode is provided, just re-render the current mode. + // If the provided mode isn't active, perform a full switch. + if ( mode && mode !== this._mode ) + return this.mode( mode ); - // If no argument is provided, return the current view. - if ( ! view ) - return previous; + var set = { view: null }, + view; - // If we're attempting to switch to the current view, bail. - if ( view === previous ) + this.trigger( 'create', set ); + view = set.view; + this.trigger( 'render', view ); + if ( view ) + this.set( view ); + return this; + }, + + get: function() { + return this.view.views.first( this.selector ); + }, + + set: function( views, options ) { + if ( options ) + options.add = false; + return this.view.views.set( this.selector, views, options ); + }, + + trigger: function( event ) { + var base; + if ( ! this._mode ) return; - // Add classes to the new view. - if ( id ) - view.$el.addClass( 'region-' + id ); + var args = _.toArray( arguments ); + base = this.id + ':' + event; - if ( mode ) - view.$el.addClass( 'mode-' + mode ); + // Trigger `region:action:mode` event. + args[0] = base + ':' + this._mode; + this.view.trigger.apply( this.view, args ); - this.controller.views.set( this.selector, view ); - this._view = view; - }, - - empty: function() { - this.view( new media.View() ); + // Trigger `region:action` event. + args[0] = base; + this.view.trigger.apply( this.view, args ); + return this; } }); @@ -208,31 +214,86 @@ // wp.media.controller.State // --------------------------- media.controller.State = Backbone.Model.extend({ - initialize: function() { - this.on( 'activate', this._activate, this ); + 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 ); + + this.on( 'change:menu', this._updateMenu, this ); + + Backbone.Model.apply( this, arguments ); }, + ready: function() {}, activate: function() {}, - _activate: function() { - this.active = true; - - this.menu(); - this.toolbar(); - this.content(); - }, - deactivate: function() {}, - _deactivate: function() { - this.active = false; - }, - reset: function() {}, - menu: function() { + _ready: function() { + this._updateMenu(); + }, + + _preActivate: function() { + this.active = true; + }, + + _postActivate: function() { + this.on( 'change:menu', this._menu, this ); + this.on( 'change:titleMode', this._title, this ); + this.on( 'change:content', this._content, this ); + this.on( 'change:toolbar', this._toolbar, this ); + + this.frame.on( 'title:render:default', this._renderTitle, this ); + + this._title(); + this._menu(); + this._toolbar(); + this._content(); + this._router(); + }, + + + _deactivate: function() { + this.active = false; + + this.frame.off( 'title:render:default', this._renderTitle, this ); + + this.off( 'change:menu', this._menu, this ); + this.off( 'change:titleMode', this._title, this ); + this.off( 'change:content', this._content, this ); + this.off( 'change:toolbar', this._toolbar, this ); + }, + + _title: function() { + this.frame.title.render( this.get('titleMode') || 'default' ); + }, + + _renderTitle: function( view ) { + view.$el.text( this.get('title') || '' ); + }, + + _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.select ) + view.select( this.frame.content.mode() ); + }, + + _menu: function() { var menu = this.frame.menu, mode = this.get('menu'), view; @@ -240,20 +301,48 @@ if ( ! mode ) return; - if ( menu.mode() !== mode ) - menu.mode( mode ); + menu.mode( mode ); - view = menu.view(); + view = menu.get(); if ( view.select ) view.select( this.id ); + }, + + _updateMenu: function() { + var previous = this.previous('menu'), + menu = this.get('menu'); + + if ( previous ) + this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); + + if ( menu ) + this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); + }, + + _renderMenu: function( view ) { + var menuItem = this.get('menuItem'), + title = this.get('title'), + priority = this.get('priority'); + + if ( ! menuItem && title ) { + menuItem = { text: title }; + + if ( priority ) + menuItem.priority = priority; + } + + if ( ! menuItem ) + return; + + view.set( this.id, menuItem ); } }); _.each(['toolbar','content'], function( region ) { - media.controller.State.prototype[ region ] = function() { + media.controller.State.prototype[ '_' + region ] = function() { var mode = this.get( region ); if ( mode ) - this.frame[ region ].mode( mode ); + this.frame[ region ].render( mode ); }; }); @@ -262,15 +351,22 @@ media.controller.Library = media.controller.State.extend({ defaults: { id: 'library', - multiple: false, + multiple: false, // false, 'add', 'reset' describe: false, - toolbar: 'main-attachments', + toolbar: 'select', sidebar: 'settings', - content: 'browse', + content: 'upload', + router: 'browse', searchable: true, filterable: false, - uploads: true, - sortable: true + sortable: true, + title: l10n.mediaLibraryTitle, + + // Uses a user setting to override the content mode. + contentUserSetting: true, + + // Sync the selection from the last state when 'multiple' matches. + syncLastSelection: true }, initialize: function() { @@ -290,31 +386,37 @@ this.set( 'gutter', 8 ); this.resetDisplays(); - - media.controller.State.prototype.initialize.apply( this, arguments ); }, activate: function() { var library = this.get('library'), - selection = this.get('selection'); + selection = this.get('selection'), + mode; + + if ( this.get('syncLastSelection') ) { + this.getLastSelection(); + } this._excludeStateLibrary(); this.buildComposite(); this.on( 'change:library change:exclude', this.buildComposite, this ); this.on( 'change:excludeState', this._excludeState, this ); - // If we're in a workflow that supports multiple attachments, - // automatically select any uploading attachments. - if ( this.get('multiple') ) - wp.Uploader.queue.on( 'add', this.selectUpload, this ); + wp.Uploader.queue.on( 'add', this.uploading, this ); selection.on( 'add remove reset', this.refreshSelection, this ); - this.refresh(); this.on( 'insert', this._insertDisplaySettings, this ); + + if ( this.get('contentUserSetting') ) { + this.frame.on( 'content:activate', this.saveContentMode, this ); + this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); + } }, deactivate: function() { + this.frame.off( 'content:activate', this.saveContentMode, this ); + // Unbind all event handlers that use this state as the context // from the selection. this.get('selection').off( null, null, this ); @@ -330,11 +432,7 @@ reset: function() { this.get('selection').reset(); this.resetDisplays(); - }, - - refresh: function() { - this.content(); - this.refreshSelection(); + this.refreshContent(); }, resetDisplays: function() { @@ -371,19 +469,66 @@ setUserSetting( 'urlbutton', display.link ); }, - refreshSelection: function() { + getLastSelection: function() { var selection = this.get('selection'), - mode = this.frame.content.mode(); + lastState = this.frame.lastState(), + lastSelection = lastState && lastState.get('selection'), + lastMultiple, thisMultiple; - this.frame.toolbar.view().refresh(); - this.trigger( 'refresh:selection', this, selection ); + if ( ! lastSelection ) + return; - if ( ! selection.length && 'browse' !== mode && 'upload' !== mode ) - this.content(); + // We don't care about the method of multiple selection the + // selections use, just that they both support (or don't support) + // multiple selection. + lastMultiple = !! lastSelection.multiple; + thisMultiple = !! selection.multiple; + + if ( lastMultiple !== thisMultiple ) + return; + + selection.reset( lastSelection.toArray() ).single( lastSelection.single() ); }, - selectUpload: function( attachment ) { - this.get('selection').add( attachment ); + refreshSelection: function() { + this.frame.toolbar.get().refresh(); + this.trigger( 'refresh:selection', this, this.get('selection') ); + this.refreshContent(); + }, + + refreshContent: function() { + var selection = this.get('selection'), + frame = this.frame, + router = frame.router.get(), + mode = frame.content.mode(); + + if ( this.active&& ! selection.length && ! router.get( mode ) ) + this.frame.content.render( this.get('content') ); + }, + + uploading: function( attachment ) { + var content = this.frame.content; + + // If the uploader was selected, navigate to the browser. + if ( 'upload' === content.mode() ) + this.frame.content.mode('browse'); + + // If we're in a workflow that supports multiple attachments, + // automatically select any uploading attachments. + if ( this.get('multiple') ) + this.get('selection').add( attachment ); + }, + + saveContentMode: function() { + // Only track the browse router on library states. + if ( 'browse' !== this.get('router') ) + return; + + var mode = this.frame.content.mode(), + view = this.frame.router.get(); + + if ( view && view.get( mode ) ) + setUserSetting( 'libraryContent', mode ); }, buildComposite: function() { @@ -448,45 +593,9 @@ } }); - - // wp.media.controller.Upload - // --------------------------- - media.controller.Upload = media.controller.State.extend({ - defaults: _.defaults({ - id: 'upload', - content: 'upload', - toolbar: 'empty', - uploads: true, - - // The state to navigate to when files are uploading. - libraryState: 'library' - }, media.controller.State.prototype.defaults ), - - initialize: function() { - media.controller.State.prototype.initialize.apply( this, arguments ); - }, - - activate: function() { - wp.Uploader.queue.on( 'add', this.uploading, this ); - media.controller.State.prototype.activate.apply( this, arguments ); - }, - - deactivate: function() { - wp.Uploader.queue.off( null, null, this ); - media.controller.State.prototype.deactivate.apply( this, arguments ); - }, - - uploading: function( attachment ) { - var library = this.get('libraryState'); - - this.frame.state( library ).get('selection').add( attachment ); - this.frame.setState( library ); - } - }); - - // wp.media.controller.Gallery - // --------------------------- - media.controller.Gallery = media.controller.Library.extend({ + // wp.media.controller.GalleryEdit + // ------------------------------- + media.controller.GalleryEdit = media.controller.Library.extend({ defaults: { id: 'gallery-edit', multiple: false, @@ -496,7 +605,10 @@ sortable: true, searchable: false, toolbar: 'gallery-edit', - content: 'browse' + content: 'browse', + title: l10n.editGalleryTitle, + priority: 60, + dragInfo: true }, initialize: function() { @@ -519,7 +631,7 @@ // Watch for uploaded attachments. this.get('library').observe( wp.Uploader.queue ); - this.frame.content.on( 'activate:browse', this.gallerySettings, this ); + this.frame.on( 'content:render:browse', this.gallerySettings, this ); media.controller.Library.prototype.activate.apply( this, arguments ); }, @@ -528,21 +640,19 @@ // Stop watching for uploaded attachments. this.get('library').unobserve( wp.Uploader.queue ); - this.frame.content.off( null, null, this ); + this.frame.off( 'content:render:browse', this.gallerySettings, this ); + media.controller.Library.prototype.deactivate.apply( this, arguments ); }, - gallerySettings: function() { - var library = this.get('library'), - browser; + gallerySettings: function( browser ) { + var library = this.get('library'); - if ( ! library ) + if ( ! library || ! browser ) return; library.gallery = library.gallery || new Backbone.Model(); - browser = this.frame.content.view(); - browser.sidebar.set({ gallery: new media.view.Settings.Gallery({ controller: this, @@ -570,7 +680,9 @@ filterable: 'uploaded', multiple: false, menu: 'main', - toolbar: 'featured-image' + toolbar: 'featured-image', + title: l10n.featuredImageTitle, + priority: 60 }, media.controller.Library.prototype.defaults ), initialize: function() { @@ -629,7 +741,10 @@ menu: 'main', content: 'embed', toolbar: 'main-embed', - type: 'link' + type: 'link', + + title: l10n.fromUrlTitle, + priority: 120 }, // The amount of time used when debouncing the scan. @@ -637,9 +752,9 @@ initialize: function() { this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); - this.on( 'change:url', this.debouncedScan, this ); + this.props = new Backbone.Model({ url: '' }); + this.props.on( 'change:url', this.debouncedScan, this ); this.on( 'scan', this.scanImage, this ); - media.controller.State.prototype.initialize.apply( this, arguments ); }, scan: function() { @@ -652,11 +767,11 @@ scanImage: function( attributes ) { var frame = this.frame, state = this, - url = this.get('url'), + url = this.props.get('url'), image = new Image(); image.onload = function() { - if ( state !== frame.state() || url !== state.get('url') ) + if ( state !== frame.state() || url !== state.props.get('url') ) return; state.set({ @@ -670,14 +785,10 @@ }, reset: function() { - _.each( _.difference( _.keys( this.attributes ), _.keys( this.defaults ) ), function( key ) { - this.unset( key ); - }, this ); - - this.set( 'url', '' ); + this.props = new Backbone.Model({ url: '' }); if ( this.id === this.frame.state().id ) - this.frame.toolbar.view().refresh(); + this.frame.toolbar.get().refresh(); } }); @@ -1022,9 +1133,13 @@ // The constructor for the `Views` manager. Views: media.Views, - constructor: function() { + constructor: function( options ) { this.views = new this.Views( this, this.views ); this.on( 'ready', this.ready, this ); + + if ( options && options.controller ) + this.controller = options.controller; + Backbone.View.apply( this, arguments ); }, @@ -1097,9 +1212,9 @@ // Initialize regions. _.each( this.regions, function( region ) { this[ region ] = new media.controller.Region({ - controller: this, - id: region, - selector: '.media-frame-' + region + view: this, + id: region, + selector: '.media-frame-' + region }); }, this ); }, @@ -1113,6 +1228,7 @@ // Ensure states have a reference to the frame. this.states.on( 'add', function( model ) { model.frame = this; + model.trigger('ready'); }, this ); }, @@ -1131,7 +1247,7 @@ media.view.MediaFrame = media.view.Frame.extend({ className: 'media-frame', template: media.template('media-frame'), - regions: ['menu','content','toolbar'], + regions: ['menu','title','content','toolbar','router'], initialize: function() { media.view.Frame.prototype.initialize.apply( this, arguments ); @@ -1173,6 +1289,10 @@ } this.on( 'attach', _.bind( this.views.ready, this.views ), this ); + + // Bind default title creation. + this.on( 'title:create:default', this.createTitle, this ); + this.title.mode('default'); }, render: function() { @@ -1183,6 +1303,31 @@ return media.view.Frame.prototype.render.apply( this, arguments ); }, + createTitle: function( title ) { + title.view = new media.View({ + controller: this, + tagName: 'h1' + }); + }, + + createMenu: function( menu ) { + menu.view = new media.view.Menu({ + controller: this + }); + }, + + createToolbar: function( toolbar ) { + menu.view = new media.view.Toolbar({ + controller: this + }); + }, + + createRouter: function( router ) { + router.view = new media.view.Router({ + controller: this + }); + }, + createIframeStates: function( options ) { var settings = media.view.settings, tabs = settings.tabs, @@ -1208,22 +1353,25 @@ }, options ) ); }, this ); - this.content.on( 'activate:iframe', this.iframeContent, this ); - this.menu.on( 'activate:main', this.iframeMenu, this ); + this.on( 'content:create:iframe', this.iframeContent, this ); + this.on( 'menu:render:main', this.iframeMenu, this ); this.on( 'open', this.hijackThickbox, this ); this.on( 'close', this.restoreThickbox, this ); }, - iframeContent: function() { + iframeContent: function( content ) { this.$el.addClass('hide-toolbar'); - this.content.view( new media.view.Iframe({ + content.view = new media.view.Iframe({ controller: this - }).render() ); + }); }, - iframeMenu: function() { + iframeMenu: function( view ) { var views = {}; + if ( ! view ) + return; + _.each( media.view.settings.tabs, function( title, id ) { views[ 'iframe:' + id ] = { text: this.state( 'iframe:' + id ).get('title'), @@ -1231,7 +1379,7 @@ }; }, this ); - this.menu.view().set( views ); + view.set( views ); }, hijackThickbox: function() { @@ -1305,83 +1453,77 @@ new media.controller.Library({ selection: options.selection, library: media.query( options.library ), - multiple: this.options.multiple, + multiple: options.multiple, menu: 'main', - toolbar: 'select' - }), - - new media.controller.Upload({ - menu: 'main' + title: options.title, + priority: 20 }) ]); }, bindHandlers: function() { - this.menu.on( 'activate:main', this.mainMenu, this ); - this.content.on( 'activate:browse', this.browseContent, this ); - this.content.on( 'activate:upload', this.uploadContent, this ); - this.toolbar.on( 'activate:select', this.selectToolbar, this ); + this.on( 'menu:create:main', this.createMenu, this ); + this.on( 'router:create:browse', this.createRouter, this ); + this.on( 'router:render:browse', this.browseRouter, this ); + this.on( 'content:create:browse', this.browseContent, this ); + this.on( 'content:render:upload', this.uploadContent, this ); + this.on( 'toolbar:create:select', this.createSelectToolbar, this ); this.on( 'refresh:selection', this.refreshSelectToolbar, this ); }, - mainMenu: function( options ) { - this.menu.view( new media.view.Menu({ - controller: this, - silent: options && options.silent, - - views: { - upload: { - text: l10n.uploadFilesTitle, - priority: 20 - }, - library: { - text: l10n.mediaLibraryTitle, - priority: 40 - } + // Routers + browseRouter: function( view ) { + view.set({ + upload: { + text: l10n.uploadFilesTitle, + priority: 20 + }, + browse: { + text: l10n.mediaLibraryTitle, + priority: 40 } - }) ); + }); }, // Content - browseContent: function() { + browseContent: function( content ) { var state = this.state(); this.$el.removeClass('hide-toolbar'); // Browse our library of attachments. - this.content.view( new media.view.AttachmentsBrowser({ + content.view = new media.view.AttachmentsBrowser({ controller: this, collection: state.get('library'), selection: state.get('selection'), model: state, sortable: state.get('sortable'), search: state.get('searchable'), - uploads: state.get('uploads'), filters: state.get('filterable'), display: state.get('displaySettings'), + dragInfo: state.get('dragInfo'), AttachmentView: state.get('AttachmentView') - }) ); + }); }, uploadContent: function() { - this.$el.addClass('hide-toolbar'); - - this.content.view( new media.view.UploaderInline({ + this.$el.removeClass('hide-toolbar'); + this.content.set( new media.view.UploaderInline({ controller: this }) ); }, // Toolbars - selectToolbar: function( options ) { + createSelectToolbar: function( toolbar, options ) { options = _.defaults( options || {}, { event: 'select', silent: false, state: false }); - this.toolbar.view( new media.view.Toolbar({ + toolbar.view = new media.view.Toolbar({ controller: this, silent: options.silent, @@ -1402,7 +1544,7 @@ } } } - }) ); + }); }, refreshSelectToolbar: function() { @@ -1411,7 +1553,7 @@ if ( ! selection || 'select' !== this.toolbar.mode() ) return; - this.toolbar.view().get('select').model.set( 'disabled', ! selection.length ); + this.toolbar.get().get('select').model.set( 'disabled', ! selection.length ); } }); @@ -1430,18 +1572,23 @@ }, createStates: function() { - var options = this.options; + var options = this.options, + selection = options.selection; // Add the default states. this.states.add([ // Main states. new media.controller.Library({ - selection: options.selection, - library: media.query( options.library ), - editable: true, - filterable: 'all', - multiple: this.options.multiple, + id: 'insert', + title: l10n.insertMediaTitle, + priority: 20, menu: 'main', + toolbar: 'main-insert', + filterable: 'all', + library: media.query( options.library ), + selection: selection, + multiple: options.multiple ? 'reset' : false, + editable: true, // Show the attachment display settings. displaySettings: true, @@ -1450,15 +1597,30 @@ displayUserSettings: true }), - new media.controller.Upload({ - menu: 'main' + new media.controller.Library({ + id: 'gallery', + title: l10n.createGalleryTitle, + priority: 40, + menu: 'main', + toolbar: 'main-gallery', + filterable: 'uploaded', + multiple: 'add', + editable: true, + + library: media.query( _.defaults({ + type: 'image' + }, options.library ) ), + + selection: new media.model.Selection( selection.models, { + multiple: 'add' + }) }), // Embed states. new media.controller.Embed(), // Gallery states. - new media.controller.Gallery({ + new media.controller.GalleryEdit({ library: options.selection, editing: options.editing, menu: 'gallery' @@ -1468,16 +1630,12 @@ id: 'gallery-library', library: media.query({ type: 'image' }), filterable: 'uploaded', - multiple: true, + multiple: 'add', menu: 'gallery', toolbar: 'gallery-add', - excludeState: 'gallery-edit' - }), - - new media.controller.Upload({ - id: 'gallery-upload', - menu: 'gallery', - libraryState: 'gallery-edit' + excludeState: 'gallery-edit', + title: l10n.addToGalleryTitle, + priority: 100 }) ]); @@ -1492,9 +1650,13 @@ bindHandlers: function() { media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); + this.on( 'menu:create:gallery', this.createMenu, this ); + this.on( 'toolbar:create:main-insert', this.createSelectionToolbar, this ); + this.on( 'toolbar:create:main-gallery', this.createSelectionToolbar, this ); var handlers = { menu: { + 'main': 'mainMenu', 'gallery': 'galleryMenu' }, @@ -1504,7 +1666,8 @@ }, toolbar: { - 'main-attachments': 'mainAttachmentsToolbar', + 'main-insert': 'mainInsertToolbar', + 'main-gallery': 'mainGalleryToolbar', 'main-embed': 'mainEmbedToolbar', 'featured-image': 'featuredImageToolbar', 'gallery-edit': 'galleryEditToolbar', @@ -1514,70 +1677,42 @@ _.each( handlers, function( regionHandlers, region ) { _.each( regionHandlers, function( callback, handler ) { - this[ region ].on( 'activate:' + handler, this[ callback ], this ); + this.on( region + ':render:' + handler, this[ callback ], this ); }, this ); }, this ); }, // Menus - mainMenu: function() { - media.view.MediaFrame.Select.prototype.mainMenu.call( this, { silent: true }); - - this.menu.view().set({ + mainMenu: function( view ) { + view.set({ 'library-separator': new media.View({ className: 'separator', - priority: 60 - }), - 'embed': { - text: l10n.fromUrlTitle, - priority: 80 - } - }); - - if ( media.view.settings.post.featuredImageId ) { - this.menu.view().set( 'featured-image', { - text: l10n.featuredImageTitle, priority: 100 - }); - } + }) + }); }, - galleryMenu: function() { + galleryMenu: function( view ) { var lastState = this.lastState(), previous = lastState && lastState.id, frame = this; - this.menu.view( new media.view.Menu({ - controller: this, - views: { - cancel: { - text: l10n.cancelGalleryTitle, - priority: 20, - click: function() { - if ( previous ) - frame.setState( previous ); - else - frame.close(); - } - }, - separateCancel: new media.View({ - className: 'separator', - priority: 40 - }), - 'gallery-edit': { - text: l10n.editGalleryTitle, - priority: 60 - }, - 'gallery-upload': { - text: l10n.uploadImagesTitle, - priority: 80 - }, - 'gallery-library': { - text: l10n.mediaLibraryTitle, - priority: 100 + view.set({ + cancel: { + text: l10n.cancelGalleryTitle, + priority: 20, + click: function() { + if ( previous ) + frame.setState( previous ); + else + frame.close(); } - } - }) ); + }, + separateCancel: new media.View({ + className: 'separator', + priority: 40 + }) + }); }, // Content @@ -1587,7 +1722,7 @@ model: this.state() }).render(); - this.content.view( view ); + this.content.set( view ); view.url.focus(); }, @@ -1603,6 +1738,7 @@ model: state, sortable: true, search: false, + dragInfo: true, AttachmentView: media.view.Attachment.EditSelection }).render(); @@ -1617,37 +1753,62 @@ }); // Browse our library of attachments. - this.content.view( view ); - }, - - // Sidebars - onSidebarGallerySettings: function( options ) { - var library = this.state().get('library'); - - if ( ! library ) - return; - - library.gallery = library.gallery || new Backbone.Model(); - - this.sidebar.view().set({ - gallery: new media.view.Settings.Gallery({ - controller: this, - model: library.gallery, - priority: 40 - }).render() - }, options ); + this.content.set( view ); }, // Toolbars - mainAttachmentsToolbar: function() { - this.toolbar.view( new media.view.Toolbar.Insert({ + createSelectionToolbar: function( toolbar ) { + toolbar.view = new media.view.Toolbar.Selection({ controller: this, editable: this.state().get('editable') - }) ); + }); + }, + + mainInsertToolbar: function( view ) { + var controller = this; + + view.button = 'insert'; + view.set( 'insert', { + style: 'primary', + priority: 80, + text: l10n.insertIntoPost, + + click: function() { + var state = controller.state(), + selection = state.get('selection'); + + controller.close(); + state.trigger( 'insert', selection ).reset(); + } + }); + }, + + mainGalleryToolbar: function( view ) { + var controller = this; + + view.button = 'gallery'; + view.set( 'gallery', { + style: 'primary', + text: l10n.createNewGallery, + priority: 60, + + click: function() { + var selection = controller.state().get('selection'), + edit = controller.state('gallery-edit'), + models = selection.where({ type: 'image' }); + + edit.set( 'library', new media.model.Selection( models, { + props: selection.props.toJSON(), + multiple: true + }) ); + + this.controller.setState('gallery-edit'); + } + }); }, featuredImageToolbar: function() { - this.toolbar.view( new media.view.Toolbar.Select({ + this.toolbar.set( new media.view.Toolbar.Select({ controller: this, text: l10n.setFeaturedImage, state: this.options.state || 'upload' @@ -1655,7 +1816,7 @@ }, mainEmbedToolbar: function() { - this.toolbar.view( new media.view.Toolbar.Embed({ + this.toolbar.set( new media.view.Toolbar.Embed({ controller: this }) ); @@ -1664,7 +1825,7 @@ galleryEditToolbar: function() { var editing = this.state().get('editing'); - this.toolbar.view( new media.view.Toolbar({ + this.toolbar.set( new media.view.Toolbar({ controller: this, items: { insert: { @@ -1689,7 +1850,7 @@ }, galleryAddToolbar: function() { - this.toolbar.view( new media.view.Toolbar({ + this.toolbar.set( new media.view.Toolbar({ controller: this, items: { insert: { @@ -1704,7 +1865,7 @@ edit.get('library').add( state.get('selection').models ); state.trigger('reset'); - controller.state('gallery-edit'); + controller.setState('gallery-edit'); } } } @@ -1729,8 +1890,6 @@ }, initialize: function() { - this.controller = this.options.controller; - _.defaults( this.options, { container: document.body, title: '', @@ -1838,8 +1997,6 @@ initialize: function() { var uploader; - this.controller = this.options.controller; - this.$browser = $('').hide().appendTo('body'); uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { @@ -1906,7 +2063,10 @@ template: media.template('uploader-inline'), initialize: function() { - this.controller = this.options.controller; + _.defaults( this.options, { + message: '', + status: true + }); if ( ! this.options.$browser && this.controller.uploader ) this.options.$browser = this.controller.uploader.$browser; @@ -1914,9 +2074,11 @@ if ( _.isUndefined( this.options.postId ) ) this.options.postId = media.view.settings.post.id; - this.views.set( '.upload-inline-status', new media.view.UploaderStatus({ - controller: this.controller - }) ); + if ( this.options.status ) { + this.views.set( '.upload-inline-status', new media.view.UploaderStatus({ + controller: this.controller + }) ); + } }, ready: function() { @@ -1951,8 +2113,6 @@ }, initialize: function() { - this.controller = this.options.controller; - this.queue = wp.Uploader.queue; this.queue.on( 'add remove reset', this.visibility, this ); this.queue.on( 'add remove reset change:percent', this.progress, this ); @@ -2060,8 +2220,6 @@ className: 'media-toolbar', initialize: function() { - this.controller = this.options.controller; - this._views = {}; this.$primary = $('
').prependTo( this.$el ); this.$secondary = $('
').prependTo( this.$el ); @@ -2199,50 +2357,32 @@ // --------------------------- media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({ initialize: function() { - var controller = this.options.controller; - _.defaults( this.options, { text: l10n.insertIntoPost }); media.view.Toolbar.Select.prototype.initialize.apply( this, arguments ); - controller.on( 'change:url', this.refresh, this ); + this.controller.state().props.on( 'change:url', this.refresh, this ); }, refresh: function() { - var url = this.controller.state().get('url'); + var url = this.controller.state().props.get('url'); this.get('select').model.set( 'disabled', ! url || /^https?:\/\/$/.test(url) ); } }); - // wp.media.view.Toolbar.Insert - // ---------------------------- - media.view.Toolbar.Insert = media.view.Toolbar.extend({ + // wp.media.view.Toolbar.Selection + // ------------------------------- + media.view.Toolbar.Selection = media.view.Toolbar.extend({ + button: 'insert', + initialize: function() { - var controller = this.options.controller, - selection = controller.state().get('selection'), - selectionToLibrary; - - selectionToLibrary = function( state, filter ) { - return function() { - var controller = this.controller, - selection = controller.state().get('selection'), - edit = controller.state( state ), - models = filter ? filter( selection ) : selection.models; - - edit.set( 'library', new media.model.Selection( models, { - props: selection.props.toJSON(), - multiple: true - }) ); - - this.controller.setState( state ); - }; - }; + var controller = this.controller; this.options.items = _.defaults( this.options.items || {}, { selection: new media.view.Selection({ controller: controller, - collection: selection, + collection: controller.state().get('selection'), priority: -40, // If the selection is editable, pass the callback to @@ -2250,26 +2390,7 @@ editable: this.options.editable && function() { this.controller.content.mode('edit-selection'); } - }).render(), - - insert: { - style: 'primary', - priority: 80, - text: l10n.insertIntoPost, - - click: function() { - controller.close(); - controller.state().trigger( 'insert', selection ).reset(); - } - }, - - gallery: { - text: l10n.createNewGallery, - priority: 40, - click: selectionToLibrary('gallery-edit', function( selection ) { - return selection.where({ type: 'image' }); - }) - } + }).render() }); media.view.Toolbar.prototype.initialize.apply( this, arguments ); @@ -2277,14 +2398,12 @@ refresh: function() { var selection = this.controller.state().get('selection'), - count = selection.length; + button = this.get( this.button ); - this.get('insert').model.set( 'disabled', ! selection.length ); + if ( ! button ) + return; - // Check if any attachment in the selection is an image. - this.get('gallery').$el.toggle( count > 1 && selection.any( function( attachment ) { - return 'image' === attachment.get('type'); - }) ); + button.model.set( 'disabled', ! selection.length ); } }); @@ -2388,8 +2507,7 @@ tagName: 'div', initialize: function() { - this.controller = this.options.controller; - this._views = {}; + this._views = {}; this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); delete this.options.views; @@ -2457,50 +2575,37 @@ } }); - /** - * wp.media.view.Menu + * wp.media.view.MenuItem */ - media.view.Menu = media.view.PriorityList.extend({ - tagName: 'ul', - className: 'media-menu', - - toView: function( options, state ) { - options = options || {}; - options.state = options.state || state; - return new media.view.MenuItem( options ).render(); - }, - - select: function( state ) { - var view = this.get( state ); - - if ( ! view ) - return; - - this.deselect(); - view.$el.addClass('active'); - }, - - deselect: function() { - this.$el.children().removeClass('active'); - } - }); - media.view.MenuItem = media.View.extend({ - tagName: 'li', + tagName: 'a', className: 'media-menu-item', + attributes: { + href: '#' + }, + events: { - 'click': 'click' + 'click': '_click' + }, + + _click: function( event ) { + var clickOverride = this.options.click; + + if ( event ) + event.preventDefault(); + + if ( clickOverride ) + clickOverride.call( this ); + else + this.click(); }, click: function() { - var options = this.options; - - if ( options.click ) - options.click.call( this ); - else if ( options.state ) - this.controller.setState( options.state ); + var state = this.options.state; + if ( state ) + this.controller.setState( state ); }, render: function() { @@ -2515,6 +2620,96 @@ } }); + /** + * wp.media.view.Menu + */ + media.view.Menu = media.view.PriorityList.extend({ + tagName: 'div', + className: 'media-menu', + property: 'state', + ItemView: media.view.MenuItem, + region: 'menu', + + toView: function( options, id ) { + options = options || {}; + options[ this.property ] = options[ this.property ] || id; + return new this.ItemView( options ).render(); + }, + + ready: function() { + media.view.PriorityList.prototype.ready.apply( this, arguments ); + this.visibility(); + }, + + set: function() { + media.view.PriorityList.prototype.set.apply( this, arguments ); + this.visibility(); + }, + + unset: function() { + media.view.PriorityList.prototype.unset.apply( this, arguments ); + this.visibility(); + }, + + visibility: function() { + var region = this.region, + view = this.controller[ region ].get(), + views = this.views.get(), + hide = ! views || views.length < 2; + + if ( this === view ) + this.controller.$el.toggleClass( 'hide-' + region, hide ); + }, + + select: function( id ) { + var view = this.get( id ); + + if ( ! view ) + return; + + this.deselect(); + view.$el.addClass('active'); + }, + + deselect: function() { + this.$el.children().removeClass('active'); + } + }); + + /** + * wp.media.view.RouterItem + */ + media.view.RouterItem = media.view.MenuItem.extend({ + click: function() { + var contentMode = this.options.contentMode; + if ( contentMode ) + this.controller.content.mode( contentMode ); + } + }); + + /** + * wp.media.view.Router + */ + media.view.Router = media.view.Menu.extend({ + tagName: 'div', + className: 'media-router', + property: 'contentMode', + ItemView: media.view.RouterItem, + region: 'router', + + initialize: function() { + this.controller.on( 'content:render', this.update, this ); + media.view.Menu.prototype.initialize.apply( this, arguments ); + }, + + update: function() { + var mode = this.controller.content.mode(); + if ( mode ) + this.select( mode ); + } + }); + + /** * wp.media.view.Sidebar */ @@ -2531,7 +2726,7 @@ template: media.template('attachment'), events: { - 'click .attachment-preview': 'toggleSelection', + 'click .attachment-preview': 'toggleSelectionHandler', 'change [data-setting]': 'updateSetting', 'change [data-setting] input': 'updateSetting', 'change [data-setting] select': 'updateSetting', @@ -2546,8 +2741,6 @@ initialize: function() { var selection = this.options.selection; - this.controller = this.options.controller; - this.model.on( 'change:sizes change:uploading change:caption change:title', this.render, this ); this.model.on( 'change:percent', this.progress, this ); @@ -2623,20 +2816,70 @@ this.$bar.width( this.model.get('percent') + '%' ); }, - toggleSelection: function( event ) { - var selection = this.options.selection, - model = this.model; + toggleSelectionHandler: function( event ) { + var method; + + if ( event.shiftKey ) + method = 'between'; + else if ( event.ctrlKey || event.metaKey ) + method = 'toggle'; + + this.toggleSelection({ + method: method + }); + }, + + toggleSelection: function( options ) { + var collection = this.collection, + selection = this.options.selection, + model = this.model, + method = options && options.method, + single, between, 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 ).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 ).single( model ); + return; + } + + 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[ selection.single() === model ? 'remove' : 'single' ]( model ); + selection[ single === model ? 'remove' : 'single' ]( model ); } else { - selection.add( model ).single( model ); + // 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 ).single( model ); } }, @@ -2838,7 +3081,6 @@ }, initialize: function() { - this.controller = this.options.controller; this.el.id = _.uniqueId('__attachments-view-'); _.defaults( this.options, { @@ -2950,13 +3192,16 @@ collection.remove( model, { silent: true }).add( model, { - at: ui.item.index(), - silent: true + 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(); @@ -3200,12 +3445,9 @@ className: 'attachments-browser', initialize: function() { - this.controller = this.options.controller; - _.defaults( this.options, { filters: false, search: true, - uploads: false, display: false, AttachmentView: media.view.Attachment.Library @@ -3255,7 +3497,7 @@ }).render() ); } - if ( this.options.sortable && ! this.options.filters ) { + if ( this.options.dragInfo ) { this.toolbar.set( 'dragInfo', new media.View({ el: $( '
' + l10n.dragInfo + '
' )[0], priority: -40 @@ -3290,7 +3532,9 @@ this.removeContent(); this.uploader = new media.view.UploaderInline({ - controller: this.controller + controller: this.controller, + status: false, + message: l10n.noItemsFound }); this.views.add( this.uploader ); @@ -3322,7 +3566,7 @@ this.views.add( sidebar ); - if ( options.uploads && this.controller.uploader ) { + if ( this.controller.uploader ) { sidebar.set( 'uploads', new media.view.UploaderStatus({ controller: this.controller, priority: 40 @@ -3372,57 +3616,6 @@ } }); - /** - * wp.media.view.SelectionPreview - */ - media.view.SelectionPreview = media.View.extend({ - tagName: 'div', - className: 'selection-preview', - template: media.template('media-selection-preview'), - - events: { - 'click .clear-selection': 'clear' - }, - - initialize: function() { - _.defaults( this.options, { - clearable: true - }); - - this.controller = this.options.controller; - this.collection.on( 'add change:url remove', this.render, this ); - this.render(); - }, - - render: function() { - var options = _.clone( this.options ), - last, sizes, amount; - - // If nothing is selected, display nothing. - if ( ! this.collection.length ) { - this.$el.empty(); - return this; - } - - options.count = this.collection.length; - last = this.collection.last(); - sizes = last.get('sizes'); - - if ( 'image' === last.get('type') ) - options.thumbnail = ( sizes && sizes.thumbnail ) ? sizes.thumbnail.url : last.get('url'); - else - options.thumbnail = last.get('icon'); - - this.$el.html( this.template( options ) ); - return this; - }, - - clear: function( event ) { - event.preventDefault(); - this.collection.reset(); - } - }); - /** * wp.media.view.Selection */ @@ -3442,7 +3635,6 @@ clearable: true }); - this.controller = this.options.controller; this.attachments = new media.view.Attachments({ controller: this.controller, collection: this.collection, @@ -3659,22 +3851,24 @@ $input = this.$('.link-to-custom'), attachment = this.options.attachment; - if ( 'none' === linkTo ) { + if ( 'none' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { $input.hide(); return; } - $input.show(); + if ( attachment ) { + if ( 'post' === linkTo ) { + $input.val( attachment.get('link') ); + } else if ( 'file' === linkTo ) { + $input.val( attachment.get('url') ); + } else if ( ! this.model.get('linkUrl') ) { + $input.val('http://'); + } - if ( 'post' === linkTo ) { - $input.val( attachment.get('link') ); - } else if ( 'file' === linkTo ) { - $input.val( attachment.get('url') ); - } else if ( ! this.model.get('linkUrl') ) { - $input.val('http://'); + $input.prop( 'readonly', 'custom' !== linkTo ); } - $input.prop( 'readonly', 'custom' !== linkTo ); + $input.show(); // If the input is visible, focus and select its contents. if ( $input.is(':visible') ) @@ -3768,12 +3962,10 @@ media.view.Iframe = media.View.extend({ className: 'media-iframe', - initialize: function() { - this.controller = this.options.controller; - }, - render: function() { + this.views.detach(); this.$el.html( '