Modularize sorter

This commit is contained in:
Rob Garrison 2017-12-23 19:14:39 -06:00
parent aa17234894
commit 2646d910ab
4 changed files with 203 additions and 199 deletions

View File

@ -1,5 +1,5 @@
/* global installed messageBox */ /* global installed messageBox */
/* global updateSort */ /* global sorter */
'use strict'; 'use strict';
const filtersSelector = { const filtersSelector = {
@ -156,7 +156,7 @@ function filterOnChange({target: el, forceRefilter}) {
if (installed) { if (installed) {
reapplyFilter(); reapplyFilter();
} }
debounce(updateSort); debounce(sorter().updateSort);
} }

View File

@ -3,7 +3,7 @@
/* global checkUpdate, handleUpdateInstalled */ /* global checkUpdate, handleUpdateInstalled */
/* global objectDiff */ /* global objectDiff */
/* global configDialog */ /* global configDialog */
/* global sortInit, sortStyles, updateSort */ /* global sorter */
'use strict'; 'use strict';
let installed; let installed;
@ -85,7 +85,7 @@ function initGlobalEvents() {
// N.B. triggers existing onchange listeners // N.B. triggers existing onchange listeners
setupLivePrefs(); setupLivePrefs();
sortInit(); sorter().sortInit();
$$('[id^="manage.newUI"]') $$('[id^="manage.newUI"]')
.forEach(el => (el.oninput = (el.onchange = switchUI))); .forEach(el => (el.oninput = (el.onchange = switchUI)));
@ -108,7 +108,7 @@ function initGlobalEvents() {
function showStyles(styles = []) { function showStyles(styles = []) {
const sorted = sortStyles({ const sorted = sorter().sortStyles({
parser: 'style', parser: 'style',
styles: styles.map(style => ({ styles: styles.map(style => ({
style, style,
@ -459,7 +459,7 @@ function handleUpdate(style, {reason, method} = {}) {
handleUpdateInstalled(entry, reason); handleUpdateInstalled(entry, reason);
} }
filterAndAppend({entry}); filterAndAppend({entry});
debounce(updateSort); debounce(sorter().updateSort);
if (!entry.matches('.hidden') && reason !== 'import') { if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry); animateElement(entry);
scrollElementIntoView(entry); scrollElementIntoView(entry);
@ -568,18 +568,6 @@ function switchUI({styleOnly} = {}) {
} }
function updateStripes() {
let index = 0;
[...installed.children].forEach(entry => {
const list = entry.classList;
if (!list.contains('hidden')) {
list.add(index % 2 ? 'odd' : 'even');
list.remove(index++ % 2 ? 'even' : 'odd');
}
});
}
function rememberScrollPosition() { function rememberScrollPosition() {
history.replaceState({scrollY: window.scrollY}, document.title); history.replaceState({scrollY: window.scrollY}, document.title);
} }

View File

@ -2,195 +2,211 @@
/* global messageBox */ /* global messageBox */
'use strict'; 'use strict';
const sorterType = { var sorter = (() => {
alpha: (a, b) => (a < b ? -1 : a === b ? 0 : 1),
number: (a, b) => a - b
};
const tagData = { const sorterType = {
title: { alpha: (a, b) => (a < b ? -1 : a === b ? 0 : 1),
text: t('genericTitle'), number: (a, b) => a - b
parse: {
style: ({name}) => name,
entry: entry => entry.styleNameLowerCase,
},
sorter: sorterType.alpha
},
usercss: {
text: 'Usercss',
parse: {
style: ({style}) => (style.usercssData ? 0 : 1),
entry: entry => (entry.classList.contains('usercss') ? 0 : 1)
},
sorter: sorterType.number
},
disabled: {
text: '', // added as either "enabled" or "disabled" by the addSortOptions function
parse: {
style: ({style}) => (style.enabled ? 1 : 0),
entry: entry => (entry.classList.contains('enabled') ? 1 : 0)
},
sorter: sorterType.number
},
dateInstalled: {
text: t('dateInstalled'),
parse: {
style: ({style}) => style.installDate,
entry: entry => entry.dataset.installdate
},
sorter: sorterType.number
},
dateUpdated: {
text: t('dateUpdated'),
parse: {
style: ({style}) => style.updateDate,
entry: entry => entry.dataset.updatedate
},
sorter: sorterType.number
}
};
// Adding (assumed) most commonly used ('title,asc' should always be first)
// whitespace before & after the comma is ignored
const sortSelectOptions = [
'{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 sortByRegex = /\s*,\s*/;
function addSortOptions() {
let container;
const select = $('#sort-select');
const renderBin = document.createDocumentFragment();
const option = $create('option');
const optgroup = $create('optgroup');
const meta = {
desc: ' \uea4d',
enabled: t('genericEnabledLabel'),
disabled: t('genericDisabledLabel'),
dateNew: ` (${t('sortDateNewestFirst')})`,
dateOld: ` (${t('sortDateOldestFirst')})`,
labelFirst: ` (${t('sortLabelFirst')})`,
labelLast: ` (${t('sortLabelLast')})`,
groupAsc: t('sortLabelTitleAsc'),
groupDesc: t('sortLabelTitleDesc')
}; };
const optgroupRegex = /\{\w+\}/;
sortSelectOptions.forEach(sort => { const tagData = {
if (optgroupRegex.test(sort)) { title: {
if (container) { text: t('genericTitle'),
renderBin.appendChild(container); parse: {
style: ({name}) => name,
entry: entry => entry.styleNameLowerCase,
},
sorter: sorterType.alpha
},
usercss: {
text: 'Usercss',
parse: {
style: ({style}) => (style.usercssData ? 0 : 1),
entry: entry => (entry.classList.contains('usercss') ? 0 : 1)
},
sorter: sorterType.number
},
disabled: {
text: '', // added as either "enabled" or "disabled" by the addSortOptions function
parse: {
style: ({style}) => (style.enabled ? 1 : 0),
entry: entry => (entry.classList.contains('enabled') ? 1 : 0)
},
sorter: sorterType.number
},
dateInstalled: {
text: t('dateInstalled'),
parse: {
style: ({style}) => style.installDate,
entry: entry => entry.dataset.installdate
},
sorter: sorterType.number
},
dateUpdated: {
text: t('dateUpdated'),
parse: {
style: ({style}) => style.updateDate,
entry: entry => entry.dataset.updatedate
},
sorter: sorterType.number
}
};
// Adding (assumed) most commonly used ('title,asc' should always be first)
// whitespace before & after the comma is ignored
const sortSelectOptions = [
'{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 sortByRegex = /\s*,\s*/;
function addSortOptions() {
let container;
const select = $('#sort-select');
const renderBin = document.createDocumentFragment();
const option = $create('option');
const optgroup = $create('optgroup');
const meta = {
desc: ' \uea4d',
enabled: t('genericEnabledLabel'),
disabled: t('genericDisabledLabel'),
dateNew: ` (${t('sortDateNewestFirst')})`,
dateOld: ` (${t('sortDateOldestFirst')})`,
labelFirst: ` (${t('sortLabelFirst')})`,
labelLast: ` (${t('sortLabelLast')})`,
groupAsc: t('sortLabelTitleAsc'),
groupDesc: t('sortLabelTitleDesc')
};
const optgroupRegex = /\{\w+\}/;
sortSelectOptions.forEach(sort => {
if (optgroupRegex.test(sort)) {
if (container) {
renderBin.appendChild(container);
}
container = optgroup.cloneNode();
container.label = meta[sort.substring(1, sort.length - 1)];
return;
} }
container = optgroup.cloneNode(); let lastTag = '';
container.label = meta[sort.substring(1, sort.length - 1)]; const opt = option.cloneNode();
return; opt.textContent = sort.split(sortByRegex).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'] + meta['labelFirst'];
if (lastTag !== 'title') return acc + meta[val === 'desc' ? 'labelLast' : 'labelFirst'];
return acc + (meta[val] || '');
}, '');
opt.value = sort;
container.appendChild(opt);
});
renderBin.appendChild(container);
select.appendChild(renderBin);
select.value = prefs.get('manage.newUI.sort');
}
function sortStyles({styles, parser}) {
if (!styles) {
styles = [...installed.children];
parser = 'entry';
} else {
parser = 'style';
} }
let lastTag = ''; const sortBy = prefs.get('manage.newUI.sort').split(sortByRegex); // 'title,asc'
const opt = option.cloneNode(); const len = sortBy.length;
opt.textContent = sort.split(sortByRegex).reduce((acc, val) => { return styles.sort((a, b) => {
if (tagData[val]) { let types, direction;
lastTag = val; let result = 0;
return acc + (acc !== '' ? ' + ' : '') + tagData[val].text; let indx = 0;
// multi-sort
while (result === 0 && indx < len) {
types = tagData[sortBy[indx++]];
direction = sortBy[indx++] === 'asc' ? 1 : -1;
result = types.sorter(types.parse[parser](a), types.parse[parser](b)) * direction;
} }
if (lastTag.indexOf('date') > -1) return acc + meta[val === 'desc' ? 'dateNew' : 'dateOld']; return result;
if (lastTag === 'disabled') return acc + meta[val === 'desc' ? 'enabled' : 'disabled'] + meta['labelFirst']; });
if (lastTag !== 'title') return acc + meta[val === 'desc' ? 'labelLast' : 'labelFirst'];
return acc + (meta[val] || '');
}, '');
opt.value = sort;
container.appendChild(opt);
});
renderBin.appendChild(container);
select.appendChild(renderBin);
select.value = prefs.get('manage.newUI.sort');
}
function sortStyles({styles, parser}) {
if (!styles) {
styles = [...installed.children];
parser = 'entry';
} else {
parser = 'style';
} }
const sortBy = prefs.get('manage.newUI.sort').split(sortByRegex); // 'title,asc'
const len = sortBy.length;
return styles.sort((a, b) => {
let types, direction;
let result = 0;
let indx = 0;
// multi-sort
while (result === 0 && indx < len) {
types = tagData[sortBy[indx++]];
direction = sortBy[indx++] === 'asc' ? 1 : -1;
result = types.sorter(types.parse[parser](a), types.parse[parser](b)) * direction;
}
return result;
});
}
function manageSort(event) { function updateSort() {
event.preventDefault(); const renderBin = document.createDocumentFragment();
prefs.set('manage.newUI.sort', this.value); const entries = sortStyles({parser: 'entry'});
debounce(updateSort); const isDiffSort = [...installed.children].find((entry, index) => entry.id !== entries[index].id);
} let index = 0;
function moveEntries() {
function updateSort() { const t0 = performance.now();
const renderBin = document.createDocumentFragment(); let moved = 0;
const entries = sortStyles({parser: 'entry'}); while (
const isDiffSort = [...installed.children].find((entry, index) => entry.id !== entries[index].id); index < entries.length &&
let index = 0; (++moved < 10 || performance.now() - t0 < 10)
function moveEntries() { ) {
const t0 = performance.now(); renderBin.appendChild(entries[index++]);
let moved = 0; }
while ( if (index < entries.length) {
index < entries.length && requestAnimationFrame(moveEntries);
(++moved < 10 || performance.now() - t0 < 10) return;
) { }
renderBin.appendChild(entries[index++]);
} }
if (index < entries.length) { if (isDiffSort !== undefined) {
requestAnimationFrame(moveEntries); moveEntries();
return; installed.appendChild(renderBin);
updateStripes();
} }
} }
if (isDiffSort !== undefined) {
moveEntries(); function manageSort(event) {
installed.appendChild(renderBin); event.preventDefault();
updateStripes(); prefs.set('manage.newUI.sort', this.value);
debounce(updateSort);
} }
}
function showSortHelp(event) { function showSortHelp(event) {
event.preventDefault(); event.preventDefault();
messageBox({ messageBox({
className: 'help-text', className: 'help-text',
title: t('sortStylesHelpTitle'), title: t('sortStylesHelpTitle'),
contents: contents:
$create('div', $create('div',
t('sortStylesHelp').split('\n').map(line => t('sortStylesHelp').split('\n').map(line =>
$create('p', line))), $create('p', line))),
buttons: [t('confirmOK')], buttons: [t('confirmOK')],
}); });
} }
function sortInit() { function sortInit() {
$('#sort-select').addEventListener('change', manageSort); $('#sort-select').addEventListener('change', manageSort);
$('#sorter-help').onclick = showSortHelp; $('#sorter-help').onclick = showSortHelp;
addSortOptions(); addSortOptions();
} }
function updateStripes() {
let index = 0;
[...installed.children].forEach(entry => {
const list = entry.classList;
if (!list.contains('hidden')) {
list.add(index % 2 ? 'odd' : 'even');
list.remove(index++ % 2 ? 'even' : 'odd');
}
});
}
return {sortInit, updateSort, sortStyles, updateStripes};
});

View File

@ -1,6 +1,6 @@
/* global messageBox */ /* global messageBox */
/* global ENTRY_ID_PREFIX, newUI */ /* global ENTRY_ID_PREFIX, newUI */
/* global filtersSelector, filterAndAppend, updateSort */ /* global filtersSelector, filterAndAppend, sorter */
'use strict'; 'use strict';
onDOMready().then(() => { onDOMready().then(() => {
@ -144,7 +144,7 @@ function reportUpdateState(state, style, details) {
} }
if (filtersSelector.hide) { if (filtersSelector.hide) {
filterAndAppend({entry}); filterAndAppend({entry});
debounce(updateSort); debounce(sorter().updateSort);
} }
} }