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"> <template data-id="style-header">
<div class="entry-header"> <div class="entry-header">
<div class="entry-col header-filter center-text"> <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"> <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"/> <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> </svg>
@ -50,7 +50,7 @@
<template data-id="style"> <template data-id="style">
<div class="entry"> <div class="entry">
<div class="entry-col entry-filter"> <div class="entry-col entry-filter">
<label> <label class="checkmate" tabindex="0">
<input class="entry-filter" type="checkbox"> <input class="entry-filter" type="checkbox">
<svg class="svg-icon checkbox" viewBox="0 0 24 24"> <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="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>
<div class="entry-col entry-id"></div> <div class="entry-col entry-id"></div>
<div class="entry-col entry-state"> <div class="entry-col entry-state">
<label> <label class="checkmate" tabindex="0">
<input class="entry-state-toggle" type="checkbox"> <input class="entry-state-toggle" type="checkbox">
<svg class="svg-icon checkbox" viewBox="0 0 24 24"> <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="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> </svg>
<span class="ext-name">Stylus</span> <span class="ext-name">Stylus</span>
<span class="ext-version"></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"> <span class="tabs">
<a href="#" class="active">Manage</a> <a href="#" class="active">Manage</a>
<a href="#">Options</a> <a href="#">Options</a>
@ -223,7 +233,101 @@
</span> </span>
</h1> </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"> <input id="search" type="search">
<button id="search-help"></button> <button id="search-help"></button>
<!-- <button id="reset-filters"></button> --> <!-- <button id="reset-filters"></button> -->
@ -241,105 +345,15 @@
<input id="manage.newUI.faviconsGray" type="checkbox"> <input id="manage.newUI.faviconsGray" type="checkbox">
<input id="manage.newUI.targets" type="number" min="1" max="100" value="3"> <input id="manage.newUI.targets" type="number" min="1" max="100" value="3">
<input id="manage.newUI.favicons" type="checkbox"> <input id="manage.newUI.favicons" type="checkbox">
</div>
</div> </div>
<div id="installed" class="manage-col-entries"></div> <div id="installed" class="manage-col-entries"></div>
<!-- <div id="manage-settings"> <!-- <div id="manage-settings">
<div class="settings-column"> <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 id="sort-wrapper">
<div class="sorter-selection" i18n-title="sortLabel"> <div class="sorter-selection" i18n-title="sortLabel">
<select id="manage.newUI.sort"></select> <select id="manage.newUI.sort"></select>

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
--icon-size: 20px; --icon-size: 20px;
--narrow-column: 60px; --narrow-column: 60px;
--header-height: 40px; --header-height: 40px;
--bulk-action-height: 60px;
--entry-header-height: 25px; --entry-header-height: 25px;
--onoffswitch-width: 60px; --onoffswitch-width: 60px;
} }
@ -121,12 +122,7 @@ a:hover {
justify-content: center; justify-content: center;
} }
.entry-header { #installed {
margin: 0;
padding: 0 4px;
}
#installed .entry:nth-child(2) {
margin-top: calc(var(--header-height) + var(--entry-header-height)); margin-top: calc(var(--header-height) + var(--entry-header-height));
} }
@ -134,6 +130,11 @@ a:hover {
margin-bottom: 10px; margin-bottom: 10px;
} }
.entry-header {
margin: 0;
padding: 0 4px;
}
.entry { .entry {
margin: 0; margin: 0;
padding: 4px 8px; padding: 4px 8px;
@ -284,23 +285,18 @@ a svg, .svg-icon.sort {
} }
/* Checkbox */ /* Checkbox */
input.entry-filter:checked + svg.checkbox, .checkmate input:checked + svg.checkbox {
input.entry-state-toggle:checked + svg.checkbox {
background: #66bb6a; background: #66bb6a;
border-radius: var(--icon-size); border-radius: var(--icon-size);
} }
input[type="checkbox"].entry-filter, .checkmate input[type="checkbox"],
input[type="checkbox"].entry-state-toggle, .checkmate input:checked + svg.checkbox .circle,
input.entry-filter:checked + svg.checkbox .circle, .checkmate input:not(:checked) + svg .checkmark {
input.entry-state-toggle:checked + svg.checkbox .circle,
input.entry-filter:not(:checked) + svg .checkmark,
input.entry-state-toggle:not(:checked) + svg .checkmark {
display: none; display: none;
} }
input.entry-filter:checked + svg.checkbox .checkmark, .checkmate input:checked + svg.checkbox .checkmark {
input.entry-state-toggle:checked + svg.checkbox .checkmark {
fill: white; fill: white;
} }
@ -310,6 +306,7 @@ input.entry-state-toggle:checked + svg.checkbox .checkmark {
pointer-events: none; pointer-events: none;
} }
/* Update icons */
.can-update .update, .can-update .update,
.no-update:not(.update-problem):not(.update-done) .up-to-date, .no-update:not(.update-problem):not(.update-done) .up-to-date,
.no-update.update-problem .check-update, .no-update.update-problem .check-update,
@ -508,6 +505,29 @@ details.applies-to-extra[open] {
display: block; 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 updated/added styles */
.highlight { .highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98); animation: highlight 10s cubic-bezier(0,.82,.47,.98);
@ -529,6 +549,61 @@ details.applies-to-extra[open] {
display: none !important; 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 */ /* drag-n-drop on import button */
.dropzone:after { .dropzone:after {
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);