Media Grid: add Bulk Selection mode for deleting attachments.

* Toolbar is sticky when `select` mode is active
* Selection is toggled when clicking an attachment preview
* Unselected attachments fade out, selected fade in.

See #28842.


git-svn-id: https://develop.svn.wordpress.org/trunk@29484 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Scott Taylor 2014-08-13 22:44:48 +00:00
parent ef86826322
commit 3ed9e02d0b
4 changed files with 162 additions and 138 deletions

View File

@ -729,6 +729,17 @@
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
opacity: 1;
-webkit-transition: opacity 250ms;
transition: opacity 250ms;
}
.media-frame.mode-select .attachment {
opacity: 0.65;
}
.media-frame.mode-select .attachment.selected {
opacity: 1;
}
.attachment:focus {
@ -741,6 +752,15 @@
outline: none;
}
.media-frame.mode-grid .attachment:focus {
-webkit-box-shadow:
inset 0 0 0 6px #f1f1f1,
inset 0 0 1px 7px #5b9dd9;
box-shadow:
inset 0 0 0 6px #f1f1f1,
inset 0 0 1px 7px #5b9dd9;
}
.selected.attachment {
-webkit-box-shadow:
inset 0 0 0 5px #fff,
@ -750,6 +770,15 @@
inset 0 0 0 7px #ccc;
}
.media-frame.mode-grid .selected.attachment {
-webkit-box-shadow:
inset 0 0 0 6px #f1f1f1,
inset 0 0 0 7px #ccc;
box-shadow:
inset 0 0 0 6px #f1f1f1,
inset 0 0 0 7px #ccc;
}
.attachment-preview {
position: relative;
-webkit-box-shadow:
@ -922,8 +951,7 @@
}
.selected.attachment:focus,
.attachment.details,
.media-frame.mode-grid .selected.attachment {
.attachment.details {
-webkit-box-shadow:
inset 0 0 0 3px #fff,
inset 0 0 0 7px #1e8cbe;
@ -932,6 +960,15 @@
inset 0 0 0 7px #1e8cbe;
}
.media-frame.mode-grid .selected.attachment:focus {
-webkit-box-shadow:
inset 0 0 0 3px #f1f1f1,
inset 0 0 0 7px #1e8cbe;
box-shadow:
inset 0 0 0 3px #f1f1f1,
inset 0 0 0 7px #1e8cbe;
}
.attachment.details .check,
.attachment.selected .check:focus,
.media-frame.mode-grid .attachment.selected .check {
@ -944,14 +981,6 @@
0 0 0 2px #1e8cbe;
}
.media-frame.mode-grid .attachment .check {
display: block;
}
.media-frame.mode-grid .attachment .check div {
background-position: 21px 0;
}
.attachment.details .check div,
.media-frame.mode-grid .attachment.selected .check div {
background-position: -21px 0;
@ -997,7 +1026,7 @@
.attachments-browser .media-toolbar-primary > .media-button-group,
.attachments-browser .media-toolbar-secondary > .media-button,
.attachments-browser .media-toolbar-secondary > .media-button-group {
margin-top: 10px;
margin: 11px 0;
}
.attachments-browser .attachments,
@ -2453,7 +2482,6 @@
/* Regions we don't use at all */
.media-frame.mode-grid .media-frame-title,
.media-frame.mode-grid .media-frame-toolbar,
.media-frame.mode-grid .media-frame-router,
.media-frame.mode-grid .media-frame-menu {
display: none;
@ -2488,6 +2516,10 @@
padding: 2px;
}
.media-frame.mode-select .attachments {
padding: 2px;
}
/**
* Copied styles from the Add theme toolbar.
*
@ -2511,6 +2543,14 @@
border: none;
}
.media-frame.mode-select .attachments-browser .media-toolbar.fixed {
position: fixed;
top: 28px;
left: 182px;
right: 20px;
width: auto;
}
.media-frame.mode-grid input[type="search"] {
margin: 1px;
padding: 3px 5px;
@ -2539,6 +2579,10 @@
margin-top: 15px;
}
.attachments-browser .media-toolbar-secondary > .select-mode-toggle-button {
margin-right: 10px;
}
.media-frame.mode-grid .attachments-browser {
padding: 0;
}

View File

@ -62,10 +62,13 @@
multiple: 'add',
state: 'library',
uploader: true,
mode: [ 'grid' ]
mode: [ 'grid', 'edit' ]
});
$(document).on( 'click', '.add-new-h2', _.bind( this.addNewClickHandler, this ) );
this.$window = $( window );
this.$adminBar = $( '#wpadminbar' );
this.$window.on( 'scroll', _.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');
@ -96,6 +99,8 @@
// Call 'initialize' directly on the parent class.
media.view.MediaFrame.prototype.initialize.apply( this, arguments );
this.on( 'all', function () { console.log( arguments ); } );
// Append the frame view directly the supplied container.
this.$el.appendTo( this.options.container );
@ -130,6 +135,7 @@
multiple: options.multiple,
title: options.title,
content: 'browse',
toolbar: 'select',
contentUserSetting: false,
filterable: 'all'
})
@ -146,6 +152,21 @@
this.on( 'edit:attachment', this.openEditAttachmentModal, this );
},
fixPosition: function() {
var $browser;
if ( ! this.isModeActive( 'select' ) ) {
return;
}
$browser = this.$('.attachments-browser');
if ( $browser.offset().top < this.$window.scrollTop() + this.$adminBar.height() ) {
$browser.find('.media-toolbar').addClass( 'fixed' );
} else {
$browser.find('.media-toolbar').removeClass( 'fixed' );
}
},
/**
* Click handler for the `Add New` button.
*/
@ -542,126 +563,60 @@
}
});
/**
* Controller for bulk selection.
*/
media.view.BulkSelection = media.View.extend({
className: 'bulk-select',
initialize: function() {
this.model = new Backbone.Model({
currentAction: ''
});
this.views.add( new media.view.Label({
value: l10n.bulkActionsLabel,
attributes: {
'for': 'bulk-select-dropdown'
}
}) );
this.views.add(
new media.view.BulkSelectionActionDropdown({
controller: this
})
);
this.views.add(
new media.view.BulkSelectionActionButton({
disabled: true,
text: l10n.apply,
controller: this
})
);
}
});
/**
* Bulk Selection dropdown view.
*
* @constructor
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
*/
media.view.BulkSelectionActionDropdown = media.View.extend({
tagName: 'select',
id: 'bulk-select-dropdown',
media.view.SelectModeToggleButton = media.view.Button.extend({
initialize: function() {
media.view.Button.prototype.initialize.apply( this, arguments );
this.listenTo( this.controller.controller.state().get( 'selection' ), 'add remove reset', _.bind( this.enabled, this ) );
this.$el.append( $('<option></option>').val( '' ).html( l10n.bulkActions ) )
.append( $('<option></option>').val( 'delete' ).html( l10n.deletePermanently ) );
this.$el.prop( 'disabled', true );
this.$el.on( 'change', _.bind( this.changeHandler, this ) );
this.listenTo( this.controller, 'select:activate select:deactivate', this.toggleBulkEditHandler );
},
/**
* Change handler for the dropdown.
*
* Sets the bulk selection controller's currentAction.
*/
changeHandler: function() {
this.controller.model.set( { 'currentAction': this.$el.val() } );
},
/**
* Enable or disable the dropdown if attachments have been selected.
*/
enabled: function() {
var disabled = ! this.controller.controller.state().get('selection').length;
this.$el.prop( 'disabled', disabled );
}
});
/**
* Bulk Selection dropdown view.
*
* @constructor
*
* @augments wp.media.view.Button
* @augments wp.media.View
* @augments wp.Backbone.View
* @augments Backbone.View
*/
media.view.BulkSelectionActionButton = media.view.Button.extend({
tagName: 'button',
initialize: function() {
media.view.Button.prototype.initialize.apply( this, arguments );
this.listenTo( this.controller.model, 'change', this.enabled, this );
this.listenTo( this.controller.controller.state().get( 'selection' ), 'add remove reset', _.bind( this.enabled, this ) );
},
/**
* Button click handler.
*/
click: function() {
var selection = this.controller.controller.state().get('selection');
media.view.Button.prototype.click.apply( this, arguments );
if ( 'delete' === this.controller.model.get( 'currentAction' ) ) {
// Currently assumes delete is the only action
if ( confirm( l10n.warnBulkDelete ) ) {
while ( selection.length > 0 ) {
selection.at(0).destroy();
}
}
if ( this.controller.isModeActive( 'select' ) ) {
this.controller.deactivateMode( 'select' ).activateMode( 'edit' );
} else {
this.controller.deactivateMode( 'edit' ).activateMode( 'select' );
}
this.enabled();
},
/**
* Enable or disable the button depending if a bulk action is selected
* in the bulk select dropdown, and if attachments have been selected.
*/
enabled: function() {
var currentAction = this.controller.model.get( 'currentAction' ),
selection = this.controller.controller.state().get('selection'),
disabled = ! currentAction || ! selection.length;
this.$el.prop( 'disabled', disabled );
render: function() {
media.view.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 > *');
if ( this.controller.isModeActive( 'select' ) ) {
this.model.set( 'text', l10n.cancelSelection );
children.not( '.delete-selected-button' ).hide();
toolbar.$( '.select-mode-toggle-button' ).show();
toolbar.$( '.delete-selected-button' ).removeClass( 'hidden' );
} else {
this.model.set( 'text', l10n.bulkSelect );
toolbar.$( '.delete-selected-button' ).addClass( 'hidden' );
children.not( '.spinner, .delete-selected-button' ).show();
this.controller.state().get( 'selection' ).reset();
}
}
});
media.view.DeleteSelectedButton = media.view.Button.extend({
initialize: function() {
media.view.Button.prototype.initialize.apply( this, arguments );
this.listenTo( this.controller, 'selection:toggle', this.toggleDisabled );
},
toggleDisabled: function() {
this.$el.attr( 'disabled', ! this.controller.state().get( 'selection' ).length );
},
render: function() {
media.view.Button.prototype.render.apply( this, arguments );
this.$el.addClass( 'delete-selected-button hidden' );
return this;
}
});

View File

@ -1792,7 +1792,7 @@
deactivateMode: function( mode ) {
// Bail if the mode isn't active.
if ( ! this.isModeActive( mode ) ) {
return;
return this;
}
this.activeModes.remove( this.activeModes.where( { id: mode } ) );
this.$el.removeClass( 'mode-' + mode );
@ -4821,12 +4821,18 @@
// In the grid view, bubble up an edit:attachment event to the controller.
if ( this.controller.isModeActive( 'grid' ) ) {
// Pass the current target to restore focus when closing
this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
if ( this.controller.isModeActive( 'edit' ) ) {
// Pass the current target to restore focus when closing
this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
// Don't scroll the view and don't attempt to submit anything.
event.stopPropagation();
return;
// Don't scroll the view and don't attempt to submit anything.
event.stopPropagation();
return;
}
if ( this.controller.isModeActive( 'select' ) ) {
method = 'toggle';
}
}
if ( event.shiftKey ) {
@ -4839,6 +4845,8 @@
method: method
});
this.controller.trigger( 'selection:toggle' );
// Don't scroll the view and don't attempt to submit anything.
event.stopPropagation();
},
@ -5784,12 +5792,6 @@
priority: -90
}).render() );
// BulkSelection is a <div> with subviews, including screen reader text
this.toolbar.set( 'bulkSelection', new media.view.BulkSelection({
controller: this.controller,
priority: -70
}).render() );
// DateFilter is a <select>, screen reader text needs to be rendered before
this.toolbar.set( 'dateFilterLabel', new media.view.Label({
value: l10n.filterByDate,
@ -5803,6 +5805,26 @@
model: this.collection.props,
priority: -75
}).render() );
// BulkSelection is a <div> with subviews, including screen reader text
this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
text: l10n.bulkSelect,
controller: this.controller,
priority: -70
}).render() );
this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
style: 'primary',
disabled: true,
text: l10n.deleteSelected,
controller: this.controller,
priority: -60,
click: function() {
while ( this.controller.state().get( 'selection' ).length > 0 ) {
this.controller.state().get( 'selection' ).at( 0 ).destroy();
}
}
}).render() );
}
this.toolbar.set( 'spinner', new media.view.Spinner({

View File

@ -2936,6 +2936,9 @@ function wp_enqueue_media( $args = array() ) {
'warnBulkDelete' => __( "You are about to permanently delete these items.\n 'Cancel' to stop, 'OK' to delete." ),
'bulkActions' => __( 'Bulk Actions' ),
'bulkActionsLabel' => __( 'Select bulk action' ),
'bulkSelect' => __( 'Bulk Select' ),
'cancelSelection' => __( 'Cancel Selection' ),
'deleteSelected' => __( 'Delete Selected' ),
'deletePermanently' => __( 'Delete Permanently' ),
'apply' => __( 'Apply' ),
'filterByDate' => __( 'Filter by date' ),