Add multisort to header & labels
This commit is contained in:
parent
9368c27990
commit
d379e5f34a
|
@ -1253,37 +1253,13 @@
|
|||
"shortcutsNote": {
|
||||
"message": "Define keyboard shortcuts"
|
||||
},
|
||||
"sortDateNewestFirst": {
|
||||
"message": "newest first",
|
||||
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
|
||||
},
|
||||
"sortDateOldestFirst": {
|
||||
"message": "oldest first",
|
||||
"description": "Text added to indicate that sorting a date would add the oldest entries at the top"
|
||||
},
|
||||
"sortHeader": {
|
||||
"message": "Sort",
|
||||
"description": "Title of sort column, indicating that the style can be manually sorted by dragging & dropping"
|
||||
},
|
||||
"sortLabel": {
|
||||
"message": "Select a sort to apply to the installed styles",
|
||||
"description": "Title on the sort select to indicate it is used for sorting entries"
|
||||
},
|
||||
"sortLabelTitleAsc": {
|
||||
"message": "Title Ascending",
|
||||
"description": "Text added to option group to indicate a block of options that apply a title ascending (A to Z) sort"
|
||||
},
|
||||
"sortLabelTitleDesc": {
|
||||
"message": "Title Descending",
|
||||
"description": "Text added to option group to indicate a block of options that apply a title descending (Z to A) sort"
|
||||
},
|
||||
"sortStylesHelp": {
|
||||
"message": "Choose the type of sort to apply to the installed entries from within the sort dropdown. The default setting applies an ascending sort (A to Z) to the entry titles. Sorts within the \"Title Descending\" group will apply a descending sort (Z to A) to the title.\nThere are other presets that will allow sorting the entries by multiple criteria. Think of this like sorting a table with multiple columns and each category in a select (between the plus signs) represents a column, or group.\nFor example, if the setting is \"Enabled (first) + Title\", the entries would sort so that all the enabled entries are sorted to the top of the list, then an entry title ascending sort (A to Z) is applied to both the enabled and disabled entries separately.",
|
||||
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the sort input field on the Manage styles page"
|
||||
},
|
||||
"sortStylesHelpTitle": {
|
||||
"message": "Sort contents",
|
||||
"description": "Label for the sort info popup on the Manage styles page"
|
||||
"message": "Click to sort; Shift + click to sort multiple columns",
|
||||
"description": "Title added to links in the manager page header to inform the user on how to sort the columns"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp is invalid.",
|
||||
|
|
15
manage.html
15
manage.html
|
@ -364,7 +364,6 @@
|
|||
<button id="manage-shortcuts-button"></button>
|
||||
<button id="sorter-help"></button>
|
||||
<input id="manage.newUI" type="checkbox">
|
||||
<select id="manage.newUI.sort"></select>
|
||||
<input id="manage.newUI.faviconsGray" type="checkbox">
|
||||
<input id="manage.newUI.targets" type="number" min="1" max="100" value="3">
|
||||
<input id="manage.newUI.favicons" type="checkbox">
|
||||
|
@ -381,13 +380,15 @@
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="entry-col header-id">#</div>
|
||||
<div class="entry-col header-state center-text" i18n-text="genericEnabledLabel"></div>
|
||||
<div class="entry-col header-name" i18n-text="genericName"></div>
|
||||
<a href="#" class="entry-col sortable header-id" data-type="order" i18n-title="sortLabel">#</a>
|
||||
<a href="#" class="entry-col sortable header-state center-text" i18n-text="genericEnabledLabel" i18n-title="sortLabel" data-type="disabled"></a>
|
||||
<div class="entry-col header-name">
|
||||
<a href="#" class="sortable" i18n-text="genericName" i18n-title="sortLabel" data-type="title"></a>
|
||||
</div>
|
||||
<div class="entry-col header-actions" i18n-text="optionsActions"></div>
|
||||
<div class="entry-col header-sort center-text" i18n-text="sortHeader"></div>
|
||||
<div class="entry-col header-version">v#</div>
|
||||
<div class="entry-col header-last-update center-text" i18n-text="searchResultUpdated"></div>
|
||||
<div class="entry-col header-sort center-text" i18n-text="sortHeader" i18n-title="sortOrderLabel"></div>
|
||||
<a href="#" class="entry-col sortable header-version" i18n-title="sortLabel" data-type="version">v#</a>
|
||||
<a href="#" class="entry-col sortable header-last-update center-text" i18n-text="searchResultUpdated" i18n-title="sortLabel" data-type="dateUpdated"></a>
|
||||
<div class="entry-col header-applies-to" i18n-text="appliesLabel"></div>
|
||||
</header>
|
||||
</div>
|
||||
|
|
|
@ -128,7 +128,8 @@ Object.assign(handleEvent, {
|
|||
'.update': 'update',
|
||||
'.entry-delete': 'delete',
|
||||
'.entry-configure-usercss': 'config',
|
||||
'#toggle-actions': 'toggleBulkActions'
|
||||
'#toggle-actions': 'toggleBulkActions',
|
||||
'.sortable': 'updateSort'
|
||||
},
|
||||
|
||||
entryClicked(event) {
|
||||
|
@ -204,6 +205,12 @@ Object.assign(handleEvent, {
|
|||
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
|
||||
},
|
||||
|
||||
updateSort(event) {
|
||||
event.preventDefault();
|
||||
sorter.updateSort(event);
|
||||
removeSelection();
|
||||
},
|
||||
|
||||
delete(event, entry) {
|
||||
event.preventDefault();
|
||||
const id = entry.styleId;
|
||||
|
@ -427,6 +434,17 @@ function updateBulkFilters({target}) {
|
|||
}
|
||||
}
|
||||
|
||||
function removeSelection() {
|
||||
const sel = window.getSelection ? window.getSelection() : document.selection;
|
||||
if (sel) {
|
||||
if (sel.removeAllRanges) {
|
||||
sel.removeAllRanges();
|
||||
} else if (sel.empty) {
|
||||
sel.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lazyLoad() {
|
||||
setTimeout(() => {
|
||||
$$('link[data-href]').forEach(link => {
|
||||
|
|
|
@ -46,6 +46,7 @@ const UI = {
|
|||
},
|
||||
|
||||
showStyles: (styles = [], matchUrlIds) => {
|
||||
UI.addHeaderLabels();
|
||||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
|
@ -275,17 +276,34 @@ const UI = {
|
|||
}
|
||||
},
|
||||
|
||||
addHeaderLabels: () => {
|
||||
const header = $('.header-name');
|
||||
const labels = document.createElement('span');
|
||||
const label = document.createElement('a');
|
||||
labels.className = 'header-labels';
|
||||
label.className = 'header-label sortable';
|
||||
label.title = t('sortLabel');
|
||||
label.href = '#';
|
||||
Object.keys(UI.labels).forEach(item => {
|
||||
const newLabel = label.cloneNode(true);
|
||||
newLabel.dataset.type = item;
|
||||
newLabel.textContent = UI.labels[item].text;
|
||||
labels.appendChild(newLabel);
|
||||
});
|
||||
header.appendChild(labels);
|
||||
},
|
||||
|
||||
addLabels: entry => {
|
||||
const style = entry.styleMeta;
|
||||
const container = $('.entry-labels', entry);
|
||||
const label = document.createElement('span');
|
||||
const labels = document.createElement('span');
|
||||
labels.className = 'entry-labels';
|
||||
label.className = 'entry-label ';
|
||||
label.className = 'entry-label';
|
||||
Object.keys(UI.labels).forEach(item => {
|
||||
if (UI.labels[item].is({entry, style})) {
|
||||
const newLabel = label.cloneNode(true);
|
||||
newLabel.dataset.label = item;
|
||||
newLabel.dataset.type = item;
|
||||
newLabel.textContent = UI.labels[item].text;
|
||||
labels.appendChild(newLabel);
|
||||
}
|
||||
|
|
|
@ -211,6 +211,31 @@ a:hover {
|
|||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.entry-header:not(.sortable) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sortable[data-sort-dir="asc"]:after,
|
||||
.sortable[data-sort-dir="desc"]:after {
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.sortable[data-sort-dir="asc"]:after {
|
||||
content: '▴';
|
||||
}
|
||||
|
||||
.sortable[data-sort-dir="desc"]:after {
|
||||
content: '▾';
|
||||
}
|
||||
|
||||
.entry-sort {
|
||||
cursor: move;
|
||||
}
|
||||
|
@ -267,6 +292,12 @@ body.dragging .entry:not(.dragging) .entry-name:before {
|
|||
fill: #000;
|
||||
}
|
||||
|
||||
.header-labels {
|
||||
margin-left: 16px;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.header-label,
|
||||
.entry-label {
|
||||
padding: 2px 4px;
|
||||
margin-left: 2px;
|
||||
|
@ -275,13 +306,15 @@ body.dragging .entry:not(.dragging) .entry-name:before {
|
|||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.entry-label[data-label="usercss"] {
|
||||
.header-label[data-type="usercss"],
|
||||
.entry-label[data-type="usercss"] {
|
||||
background-color: hsla(180, 100%, 20%, 1);
|
||||
color: white;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.entry-label[data-label="disabled"] {
|
||||
.header-label[data-type="disabled"],
|
||||
.entry-label[data-type="disabled"] {
|
||||
background: rgba(128, 128, 128, .2);
|
||||
color: #222;
|
||||
}
|
||||
|
|
192
manage/sort.js
192
manage/sort.js
|
@ -1,9 +1,18 @@
|
|||
/* global installed messageBox t $ $create prefs */
|
||||
/* global installed t $ prefs */
|
||||
/* exported sorter */
|
||||
'use strict';
|
||||
|
||||
const sorter = (() => {
|
||||
|
||||
// Set up for only one column
|
||||
const defaultSort = 'title,asc';
|
||||
|
||||
const sortOrder = [
|
||||
'asc',
|
||||
'desc',
|
||||
'' // unsorted
|
||||
];
|
||||
|
||||
const sorterType = {
|
||||
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
|
||||
number: (a, b) => (a || 0) - (b || 0),
|
||||
|
@ -15,115 +24,130 @@ const sorter = (() => {
|
|||
parse: ({name}) => name,
|
||||
sorter: sorterType.alpha
|
||||
},
|
||||
order: {
|
||||
text: '#',
|
||||
parse: ({style}) => style.id,
|
||||
sorter: sorterType.number,
|
||||
},
|
||||
usercss: {
|
||||
text: 'Usercss',
|
||||
parse: ({style}) => style.usercssData ? 0 : 1,
|
||||
sorter: sorterType.number
|
||||
},
|
||||
disabled: {
|
||||
text: '', // added as either "enabled" or "disabled" by the addOptions function
|
||||
parse: ({style}) => style.enabled ? 1 : 0,
|
||||
text: t('genericDisabledLabel'), // added as either "enabled" or "disabled" by the addOptions function
|
||||
parse: ({style}) => style.enabled ? 0 : 1,
|
||||
sorter: sorterType.number
|
||||
},
|
||||
version: {
|
||||
text: '#',
|
||||
parse: ({style}) => (style.usercssData && style.usercssData.version || '')
|
||||
.split(/[.-]/)
|
||||
.splice(0, 3) // ignore extra labels; e.g. 1.2.3-beta.1
|
||||
.map(n => n ? n.padStart(4, '0') : '')
|
||||
.join(''),
|
||||
sorter: sorterType.number
|
||||
},
|
||||
dateInstalled: {
|
||||
text: t('dateInstalled'),
|
||||
parse: ({style}) => style.installDate,
|
||||
parse: ({style}) => style.installDate || '',
|
||||
sorter: sorterType.number
|
||||
},
|
||||
dateUpdated: {
|
||||
text: t('dateUpdated'),
|
||||
parse: ({style}) => style.updateDate,
|
||||
parse: ({style}) => style.updateDate || '',
|
||||
sorter: sorterType.number
|
||||
}
|
||||
};
|
||||
|
||||
// Adding (assumed) most commonly used ('title,asc' should always be first)
|
||||
// whitespace before & after the comma is ignored
|
||||
const selectOptions = [
|
||||
'{groupAsc}',
|
||||
'title,asc',
|
||||
'dateInstalled,desc, title,asc',
|
||||
'dateInstalled,asc, title,asc',
|
||||
'dateUpdated,desc, title,asc',
|
||||
'dateUpdated,asc, title,asc',
|
||||
'usercss,asc, title,asc',
|
||||
'usercss,desc, title,asc',
|
||||
'disabled,asc, title,asc',
|
||||
'disabled,desc, title,asc',
|
||||
'disabled,desc, usercss,asc, title,asc',
|
||||
'{groupDesc}',
|
||||
'title,desc',
|
||||
'usercss,asc, title,desc',
|
||||
'usercss,desc, title,desc',
|
||||
'disabled,desc, title,desc',
|
||||
'disabled,desc, usercss,asc, title,desc'
|
||||
];
|
||||
|
||||
const splitRegex = /\s*,\s*/;
|
||||
const whitespace = /\s+/g;
|
||||
|
||||
let columns = 1;
|
||||
|
||||
function addOptions() {
|
||||
let container;
|
||||
const select = $('#manage.newUI.sort');
|
||||
const renderBin = document.createDocumentFragment();
|
||||
const option = $create('option');
|
||||
const optgroup = $create('optgroup');
|
||||
const meta = {
|
||||
desc: ' \u21E9',
|
||||
enabled: t('genericEnabledLabel'),
|
||||
disabled: t('genericDisabledLabel'),
|
||||
dateNew: ` (${t('sortDateNewestFirst')})`,
|
||||
dateOld: ` (${t('sortDateOldestFirst')})`,
|
||||
groupAsc: t('sortLabelTitleAsc'),
|
||||
groupDesc: t('sortLabelTitleDesc')
|
||||
};
|
||||
const optgroupRegex = /\{\w+\}/;
|
||||
selectOptions.forEach(sort => {
|
||||
if (optgroupRegex.test(sort)) {
|
||||
if (container) {
|
||||
renderBin.appendChild(container);
|
||||
}
|
||||
container = optgroup.cloneNode();
|
||||
container.label = meta[sort.substring(1, sort.length - 1)];
|
||||
return;
|
||||
}
|
||||
let lastTag = '';
|
||||
const opt = option.cloneNode();
|
||||
opt.textContent = sort.split(splitRegex).reduce((acc, val) => {
|
||||
if (tagData[val]) {
|
||||
lastTag = val;
|
||||
return acc + (acc !== '' ? ' + ' : '') + tagData[val].text;
|
||||
}
|
||||
if (lastTag.indexOf('date') > -1) return acc + meta[val === 'desc' ? 'dateNew' : 'dateOld'];
|
||||
if (lastTag === 'disabled') return acc + meta[val === 'desc' ? 'enabled' : 'disabled'];
|
||||
return acc + (meta[val] || '');
|
||||
}, '');
|
||||
opt.value = sort;
|
||||
container.appendChild(opt);
|
||||
});
|
||||
renderBin.appendChild(container);
|
||||
select.appendChild(renderBin);
|
||||
select.value = prefs.get('manage.newUI.sort');
|
||||
}
|
||||
let lastSort;
|
||||
|
||||
function sort({styles}) {
|
||||
const sortBy = prefs.get('manage.newUI.sort').split(splitRegex);
|
||||
let sortBy = prefs.get('manage.newUI.sort').replace(whitespace, '');
|
||||
if (lastSort === sortBy) {
|
||||
return styles;
|
||||
}
|
||||
sortBy = sortBy.split(splitRegex);
|
||||
if (sortBy.join('') === '') {
|
||||
sortBy = defaultSort.split(splitRegex);
|
||||
}
|
||||
updateHeaders(sortBy);
|
||||
const len = sortBy.length;
|
||||
return styles.sort((a, b) => {
|
||||
let types, direction;
|
||||
let types, direction, x, y;
|
||||
let result = 0;
|
||||
let index = 0;
|
||||
// multi-sort
|
||||
while (result === 0 && index < len) {
|
||||
types = tagData[sortBy[index++]];
|
||||
direction = sortBy[index++] === 'asc' ? 1 : -1;
|
||||
result = types.sorter(types.parse(a), types.parse(b)) * direction;
|
||||
x = types.parse(a);
|
||||
y = types.parse(b);
|
||||
// sort empty values to the bottom
|
||||
if (x === '') {
|
||||
result = 1;
|
||||
} else if (y === '') {
|
||||
result = -1;
|
||||
} else {
|
||||
direction = sortBy[index++] === 'asc' ? 1 : -1;
|
||||
result = types.sorter(x, y) * direction;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
// Update default sort on init & when all other columns are unsorted
|
||||
function updateHeaders(sortBy) {
|
||||
let header, sortDir;
|
||||
let i = 0;
|
||||
const len = sortBy.length;
|
||||
while (i < len) {
|
||||
header = $(`.entry-header [data-type="${sortBy[i++]}"]`);
|
||||
sortDir = sortBy[i++];
|
||||
if (header) {
|
||||
header.dataset.sortDir = sortDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSort(event) {
|
||||
const sortables = $$('.entry-header .sortable');
|
||||
const elm = event.target;
|
||||
// default sort column only allows asc, desc; not unsorted
|
||||
const len = sortOrder.length - (elm.dataset.type === defaultSort.split(splitRegex)[0] ? 1 : 0);
|
||||
let index = (sortOrder.indexOf(elm.dataset.sortDir) + 1) % len;
|
||||
// shift key for multi-column sorting
|
||||
if (!event.shiftKey) {
|
||||
sortables.forEach(el => {
|
||||
el.dataset.sortDir = '';
|
||||
el.dataset.timestamp = '';
|
||||
});
|
||||
}
|
||||
elm.dataset.sortDir = sortOrder[index];
|
||||
elm.dataset.timestamp = Date.now();
|
||||
|
||||
const newSort = sortables
|
||||
.filter(el => el.dataset.sortDir !== '')
|
||||
.reduce((acc, el) => {
|
||||
const {sortDir, type, timestamp = new Date()} = el.dataset;
|
||||
if (sortDir) {
|
||||
acc.push({sortDir, type, timestamp: parseFloat(timestamp)});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.reduce((acc, item) => {
|
||||
acc = acc.concat(item.type, item.sortDir);
|
||||
return acc;
|
||||
}, [])
|
||||
.join(',');
|
||||
prefs.set('manage.newUI.sort', newSort || defaultSort);
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (!installed) return;
|
||||
const current = [...installed.children];
|
||||
|
@ -176,25 +200,11 @@ const sorter = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
function showHelp(event) {
|
||||
event.preventDefault();
|
||||
messageBox({
|
||||
className: 'help-text',
|
||||
title: t('sortStylesHelpTitle'),
|
||||
contents:
|
||||
$create('div',
|
||||
t('sortStylesHelp').split('\n').map(line =>
|
||||
$create('p', line))),
|
||||
buttons: [t('confirmOK')],
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
prefs.subscribe(['manage.newUI.sort'], update);
|
||||
$('#sorter-help').onclick = showHelp;
|
||||
addOptions();
|
||||
// addOptions();
|
||||
updateColumnCount();
|
||||
}
|
||||
|
||||
return {init, update, sort, updateStripes};
|
||||
return {init, update, sort, updateSort, updateStripes};
|
||||
})();
|
||||
|
|
Loading…
Reference in New Issue
Block a user