Add bulk action panel

This commit is contained in:
Rob Garrison 2018-11-26 21:28:11 -06:00
parent 5c38441393
commit fbcc7aac08
5 changed files with 235 additions and 138 deletions

View File

@ -30,7 +30,7 @@
<template data-id="style-header">
<div class="entry-header">
<div class="entry-col header-filter center-text">
<a href="#" id="toggle-all">
<a href="#" id="toggle-actions">
<svg class="svg-icon" width="20" height="20" viewBox="0 0 14 14">
<path d="M6.42 7.58L2.92 3.5h8.75l-3.5 4.08v4.09c-1 0-1.75-.76-1.75-1.75V7.58z"/>
</svg>
@ -50,7 +50,7 @@
<template data-id="style">
<div class="entry">
<div class="entry-col entry-filter">
<label>
<label class="checkmate" tabindex="0">
<input class="entry-filter" type="checkbox">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
@ -60,7 +60,7 @@
</div>
<div class="entry-col entry-id"></div>
<div class="entry-col entry-state">
<label>
<label class="checkmate" tabindex="0">
<input class="entry-state-toggle" type="checkbox">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
@ -216,6 +216,16 @@
</svg>
<span class="ext-name">Stylus</span>
<span class="ext-version"></span>
<span class="filter-stats-wrapper">
<span id="filters-stats"></span>
<a id="reset-filters" href="#" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<title i18n-text="genericResetLabel"></title>
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</svg>
</a>
</span>
<span class="tabs">
<a href="#" class="active">Manage</a>
<a href="#">Options</a>
@ -223,7 +233,101 @@
</span>
</h1>
<div id="manage-bulk-actions" style="display: none">
<div id="manage-bulk-actions" class="hidden">
<div id="filters" class="manage-row">
<span id="search-wrapper">
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
<a href="#" id="search-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<strong i18n-text="manageFilters"></strong>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
<path class="checkmark" d="M16.59 7.52L10 14.11l-2.59-2.58L6 12.94l4 4 8-8z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
<path class="checkmark" d="M16.59 7.52L10 14.11l-2.59-2.58L6 12.94l4 4 8-8z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<span class="filter-selection">
<label class="checkmate" tabindex="0">
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
<path class="checkmark" d="M16.59 7.52L10 14.11l-2.59-2.58L6 12.94l4 4 8-8z"/>
</svg>
</label>
<span class="select-resizer">
<select id="manage.onlyUsercss.invert">
<option i18n-text="manageOnlyUsercss" value="false"></option>
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</span>
</span>
<label id="only-updates" class="hidden">
<label class="checkmate" tabindex="0">
<input type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
data-filter-hide=":not(.updatable):not(.update-done),
.no-update:not(.update-problem),
.updatable:not(.can-update):not(.update-problem):not(.update-done)">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
<path class="checkmark" d="M16.59 7.52L10 14.11l-2.59-2.58L6 12.94l4 4 8-8z"/>
</svg>
<span i18n-text="manageOnlyUpdates"></span>
</label>
</div>
<div class="manage-row">
<label class="checkmate" tabindex="0">
<input class="toggle-all-filters" type="checkbox">
<svg class="svg-icon checkbox" viewBox="0 0 24 24">
<path class="circle" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8.01 8.01 0 0 1 0-16 8.01 8.01 0 0 1 0 16z"/>
<path class="checkmark" d="M16.59 7.52L10 14.11l-2.59-2.58L6 12.94l4 4 8-8z"/>
</svg>
</label>
</div>
<div style="display: none">
<!-- placeholders -->
<input id="search" type="search">
<button id="search-help"></button>
<!-- <button id="reset-filters"></button> -->
@ -243,103 +347,13 @@
<input id="manage.newUI.favicons" type="checkbox">
</div>
</div>
<div id="installed" class="manage-col-entries"></div>
<!-- <div id="manage-settings">
<div class="settings-column">
-->
<details id="filters" data-pref="manage.filters.expanded" style="display: none">
<summary>
<h2 i18n-text="manageFilters">:
<div class="filter-stats-wrapper">
<span id="filters-stats"></span>
<a id="reset-filters" href="#" tabindex="0">
<svg class="svg-icon" viewBox="0 0 20 20">
<title i18n-text="genericResetLabel"></title>
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</svg>
</a>
</div>
</h2>
</summary>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyEnabled" type="checkbox"
data-filter=".enabled"
data-filter-hide=".disabled">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</label>
<div class="select-resizer">
<select id="manage.onlyEnabled.invert">
<option i18n-text="manageOnlyEnabled" value="false"></option>
<option i18n-text="manageOnlyDisabled" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyLocal" type="checkbox"
data-filter=":not(.updatable):not(.update-done)"
data-filter-hide=".updatable, .update-done">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</label>
<div class="select-resizer">
<select id="manage.onlyLocal.invert" i18n-title="manageOnlyLocalTooltip">
<option i18n-text="manageOnlyLocal" value="false"></option>
<option i18n-text="manageOnlyExternal" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="filter-selection">
<label>
<div class="checkmate">
<input id="manage.onlyUsercss" type="checkbox"
data-filter=".usercss"
data-filter-hide=":not(.usercss)">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</label>
<div class="select-resizer">
<select id="manage.onlyUsercss.invert">
<option i18n-text="manageOnlyUsercss" value="false"></option>
<option i18n-text="manageOnlyNonUsercss" value="true"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<label id="only-updates" class="hidden">
<input type="checkbox"
data-filter=".can-update, .update-problem, .update-done"
data-filter-hide=":not(.updatable):not(.update-done),
.no-update:not(.update-problem),
.updatable:not(.can-update):not(.update-problem):not(.update-done)">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageOnlyUpdates"></span>
</label>
<div id="search-wrapper">
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false"
data-filter=":not(.not-matching)"
data-filter-hide=".not-matching">
<a href="#" id="search-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</details>
<!--
<div id="sort-wrapper">
<div class="sorter-selection" i18n-title="sortLabel">
<select id="manage.newUI.sort"></select>

View File

@ -101,9 +101,6 @@ function init() {
$$('[data-filter]').forEach(el => {
el.onchange = filterOnChange;
if (el.closest('.hidden')) {
el.checked = false;
}
});
$('#reset-filters').onclick = event => {
@ -150,7 +147,7 @@ function filterOnChange({target: el, forceRefilter}) {
}
el.lastValue = value;
}
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
const enabledFilters = $$('#filters [data-filter]').filter(el => getValue(el));
const buildFilter = hide =>
(hide ? '' : '.entry.hidden') +
[...enabledFilters.map(el =>
@ -262,9 +259,9 @@ function reapplyFilter(container = installed, alreadySearched) {
function showFiltersStats() {
const active = filtersSelector.hide !== '';
$('#filters summary').classList.toggle('active', active);
$('.filter-stats-wrapper').classList.toggle('active', active);
$('#reset-filters').disabled = !active;
const numTotal = installed.children.length;
const numTotal = installed.children.length - 1; // Don't include the header
const numHidden = installed.getElementsByClassName('entry hidden').length;
const numShown = numTotal - numHidden;
if (filtersSelector.numShown !== numShown ||

View File

@ -82,6 +82,7 @@ onDOMready().then(() => {
// focus search field on "/" key
if (key === '/' || !key && k === 191 && !event.shiftKey) {
event.preventDefault();
$('#manage-bulk-actions').classList.remove('hidden');
$('#search').focus();
return;
}

View File

@ -74,12 +74,17 @@ function initGlobalEvents() {
document.addEventListener('keydown', event => {
if (event.which === 27) {
// close all open applies-to details
// close all open "applies-to" details
$$('.applies-to-extra[open]').forEach(el => {
el.removeAttribute('open');
});
// Close bulk actions
$('#manage-bulk-actions').classList.add('hidden');
} else if (event.which === 32 && event.target.classList.contains('checkmate')) {
// pressing space toggles the containing checkbox
$('input[type="checkbox"]', event.target).click();
}
})
});
$$('[data-toggle-on-click]').forEach(el => {
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
@ -123,7 +128,8 @@ Object.assign(handleEvent, {
'.check-update': 'check',
'.update': 'update',
'.entry-delete': 'delete',
'.entry-configure-usercss': 'config'
'.entry-configure-usercss': 'config',
'#toggle-actions': 'toggleBulkActions'
},
entryClicked(event) {
@ -182,6 +188,10 @@ Object.assign(handleEvent, {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
},
toggleBulkActions() {
$('#manage-bulk-actions').classList.toggle('hidden');
},
check(event, entry) {
event.preventDefault();
checkUpdate(entry, {single: true});

View File

@ -2,6 +2,7 @@
--icon-size: 20px;
--narrow-column: 60px;
--header-height: 40px;
--bulk-action-height: 60px;
--entry-header-height: 25px;
--onoffswitch-width: 60px;
}
@ -121,12 +122,7 @@ a:hover {
justify-content: center;
}
.entry-header {
margin: 0;
padding: 0 4px;
}
#installed .entry:nth-child(2) {
#installed {
margin-top: calc(var(--header-height) + var(--entry-header-height));
}
@ -134,6 +130,11 @@ a:hover {
margin-bottom: 10px;
}
.entry-header {
margin: 0;
padding: 0 4px;
}
.entry {
margin: 0;
padding: 4px 8px;
@ -284,23 +285,18 @@ a svg, .svg-icon.sort {
}
/* Checkbox */
input.entry-filter:checked + svg.checkbox,
input.entry-state-toggle:checked + svg.checkbox {
.checkmate input:checked + svg.checkbox {
background: #66bb6a;
border-radius: var(--icon-size);
}
input[type="checkbox"].entry-filter,
input[type="checkbox"].entry-state-toggle,
input.entry-filter:checked + svg.checkbox .circle,
input.entry-state-toggle:checked + svg.checkbox .circle,
input.entry-filter:not(:checked) + svg .checkmark,
input.entry-state-toggle:not(:checked) + svg .checkmark {
.checkmate input[type="checkbox"],
.checkmate input:checked + svg.checkbox .circle,
.checkmate input:not(:checked) + svg .checkmark {
display: none;
}
input.entry-filter:checked + svg.checkbox .checkmark,
input.entry-state-toggle:checked + svg.checkbox .checkmark {
.checkmate input:checked + svg.checkbox .checkmark {
fill: white;
}
@ -310,6 +306,7 @@ input.entry-state-toggle:checked + svg.checkbox .checkmark {
pointer-events: none;
}
/* Update icons */
.can-update .update,
.no-update:not(.update-problem):not(.update-done) .up-to-date,
.no-update.update-problem .check-update,
@ -508,6 +505,29 @@ details.applies-to-extra[open] {
display: block;
}
/* bulk actions */
#manage-bulk-actions {
background: #ddd;
width: 100vw;
position: fixed;
top: var(--header-height);
left: 0;
height: var(--bulk-action-height);
padding: 4px 16px;
z-index: 100;
}
#manage-bulk-actions:not(.hidden) + #installed {
margin-top: calc(var(--header-height) + var(--bulk-action-height) + var(--entry-header-height));
}
#manage-bulk-actions:not(.hidden) + #installed .entry-header {
top: calc(var(--header-height) + var(--bulk-action-height));
}
.manage-row {
padding-left: 10px;
}
/* highlight updated/added styles */
.highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98);
@ -529,6 +549,61 @@ details.applies-to-extra[open] {
display: none !important;
}
#filters > label {
display: flex;
align-items: center;
left: -9px;
padding: 2px 0 2px 24px;
}
#filters > label input[type="checkbox"]:not(.slider),
#filters > label input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
top: 2px;
left: 8px;
}
#filters > label:hover {
background-color: hsla(0, 0%, 50%, .4);
}
#filters {
border: 1px solid transparent;
}
.active #filters-stats {
background-color: darkcyan;
border-color: darkcyan;
color: white;
font-size: 0.7rem;
font-weight: normal;
padding: 2px 5px;
position: relative;
white-space: nowrap;
}
.filter-stats-wrapper {
margin-left: .2rem;
}
#reset-filters svg {
fill: hsla(180, 50%, 27%, .5);
width: 24px; /* widen the click area a bit */
height: 20px;
padding: 2px;
box-sizing: border-box;
}
#reset-filters:hover svg {
fill: hsla(180, 50%, 27%, 1);
}
.filter-stats-wrapper:not(.active) #reset-filters,
.filter-stats-wrapper:not(.active) #filters-stats {
display: none;
}
/* drag-n-drop on import button */
.dropzone:after {
background-color: rgba(0, 0, 0, 0.7);