stylus/manage/filters.js

289 lines
8.2 KiB
JavaScript
Raw Permalink Normal View History

/* global $ $$ $create messageBoxProxy */// dom.js
/* global API */
/* global debounce */// toolbox.js
/* global installed */// manage.js
/* global prefs */
/* global router */
/* global sorter */
/* global t */// localization.js
2017-08-15 11:40:36 +00:00
'use strict';
const filtersSelector = {
hide: '',
unhide: '',
numShown: 0,
numTotal: 0,
};
let filtersInitialized = false;
router.watch({search: ['search', 'searchMode']}, ([search, mode]) => {
$('#search').value = search || '';
if (mode) $('#searchMode').value = mode;
if (!filtersInitialized) {
2020-10-15 10:55:27 +00:00
initFilters();
filtersInitialized = true;
} else {
searchStyles();
}
});
2020-10-15 10:55:27 +00:00
function initFilters() {
$('#search').oninput = $('#searchMode').oninput = function (e) {
router.updateSearch(this.id, e.target.value);
};
$('#search-help').onclick = event => {
event.preventDefault();
messageBoxProxy.show({
className: 'help-text center-dialog',
title: t('search'),
contents:
$create('ul',
t('searchStylesHelp').split('\n').map(line =>
$create('li', line.split(/(<.*?>)/).map((s, i, words) => {
if (s.startsWith('<')) {
const num = words.length;
const className = i === num - 2 && !words[num - 1] ? '.last' : '';
return $create('mark' + className, s.slice(1, -1));
} else {
return s;
}
})))),
buttons: [t('confirmOK')],
});
};
2017-08-15 11:40:36 +00:00
$$('select[id$=".invert"]').forEach(el => {
const slave = $('#' + el.id.replace('.invert', ''));
const slaveData = slave.dataset;
const valueMap = new Map([
[false, slaveData.filter],
[true, slaveData.filterHide],
]);
// enable slave control when user switches the value
el.oninput = () => {
if (!slave.checked) {
// oninput occurs before onchange
setTimeout(() => {
if (!slave.checked) {
slave.checked = true;
slave.dispatchEvent(new Event('change', {bubbles: true}));
}
});
}
};
// swap slave control's filtering rules
el.onchange = event => {
const value = el.value === 'true';
const filter = valueMap.get(value);
2017-08-22 14:22:15 +00:00
if (slaveData.filter === filter) {
return;
}
slaveData.filter = filter;
slaveData.filterHide = valueMap.get(!value);
debounce(filterOnChange, 0, event);
};
2017-08-22 14:22:15 +00:00
el.onchange({target: el});
});
2017-08-15 11:40:36 +00:00
$$('[data-filter]').forEach(el => {
el.onchange = filterOnChange;
if (el.closest('.hidden')) {
el.checked = false;
}
});
$('#reset-filters').onclick = event => {
event.preventDefault();
if (!filtersSelector.hide) {
return;
}
for (const el of $$('#filters [data-filter]')) {
let value;
if (el.type === 'checkbox' && el.checked) {
value = el.checked = false;
} else if (el.value) {
value = el.value = '';
}
if (value !== undefined) {
el.lastValue = value;
if (prefs.knownKeys.includes(el.id)) {
prefs.set(el.id, false);
}
}
}
filterOnChange({forceRefilter: true});
router.updateSearch('search', '');
};
2017-08-22 14:22:15 +00:00
filterOnChange({forceRefilter: true});
}
2017-08-15 11:40:36 +00:00
function filterOnChange({target: el, forceRefilter, alreadySearched}) {
2017-08-15 11:40:36 +00:00
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
if (!forceRefilter) {
const value = getValue(el);
if (value === el.lastValue) {
return;
}
el.lastValue = value;
}
const enabledFilters = $$('#header [data-filter]').filter(el => getValue(el));
const buildFilter = hide =>
(hide ? '' : '.entry.hidden') +
[...enabledFilters.map(el =>
el.dataset[hide ? 'filterHide' : 'filter']
.split(/,\s*/)
.map(s => (hide ? '.entry:not(.hidden)' : '') + s)
.join(',')),
2017-08-15 11:40:36 +00:00
].join(hide ? ',' : '');
Object.assign(filtersSelector, {
hide: buildFilter(true),
unhide: buildFilter(false),
});
2017-08-22 14:22:15 +00:00
if (installed) {
reapplyFilter(installed, alreadySearched).then(sorter.updateStripes);
2017-08-22 14:22:15 +00:00
}
2017-08-15 11:40:36 +00:00
}
/* exported filterAndAppend */
2017-08-15 11:40:36 +00:00
function filterAndAppend({entry, container}) {
if (!container) {
container = [entry];
// reverse the visibility, otherwise reapplyFilter will see no need to work
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
entry.classList.add('hidden');
}
}
2018-02-26 19:57:03 +00:00
return reapplyFilter(container);
2017-08-15 11:40:36 +00:00
}
2018-02-26 19:57:03 +00:00
/**
* @returns {Promise} resolves on async search
*/
async function reapplyFilter(container = installed, alreadySearched) {
2018-01-01 17:02:49 +00:00
if (!alreadySearched && $('#search').value.trim()) {
await searchStyles({immediately: true, container});
2018-01-01 17:02:49 +00:00
}
2017-08-15 11:40:36 +00:00
// A: show
let toHide = [];
let toUnhide = [];
if (filtersSelector.hide) {
filterContainer({hide: false});
} else {
toUnhide = container;
}
// showStyles() is building the page and no filters are active
if (toUnhide instanceof DocumentFragment) {
installed.appendChild(toUnhide);
return;
2017-08-15 11:40:36 +00:00
}
// filtering needed or a single-element job from handleUpdate()
for (const entry of toUnhide.children || toUnhide) {
2017-12-23 00:11:46 +00:00
if (!entry.parentNode) {
installed.appendChild(entry);
2017-08-15 11:40:36 +00:00
}
if (entry.classList.contains('hidden')) {
entry.classList.remove('hidden');
}
}
// B: hide
if (filtersSelector.hide) {
filterContainer({hide: true});
}
if (!toHide.length) {
2017-12-04 18:14:31 +00:00
showFiltersStats();
return;
2017-08-15 11:40:36 +00:00
}
for (const entry of toHide) {
entry.classList.add('hidden');
}
// showStyles() is building the page with filters active so we need to:
// 1. add all hidden entries to the end
// 2. add the visible entries before the first hidden entry
if (container instanceof DocumentFragment) {
2017-12-23 00:11:46 +00:00
installed.appendChild(container);
2017-08-15 11:40:36 +00:00
showFiltersStats();
return;
2017-08-15 11:40:36 +00:00
}
// single-element job from handleEvent(): add the last wraith
if (toHide.length === 1 && toHide[0].parentElement !== installed) {
installed.appendChild(toHide[0]);
}
showFiltersStats();
return;
2017-08-15 11:40:36 +00:00
/***************************************/
function filterContainer({hide}) {
const selector = filtersSelector[hide ? 'hide' : 'unhide'];
if (container.filter) {
if (hide) {
// already filtered in previous invocation
return;
}
for (const el of container) {
(el.matches(selector) ? toUnhide : toHide).push(el);
}
return;
} else if (hide) {
toHide = $$(selector, container);
} else {
toUnhide = $$(selector, container);
}
}
}
2017-12-04 18:14:31 +00:00
function showFiltersStats() {
const active = filtersSelector.hide !== '';
$('#filters summary').classList.toggle('active', active);
$('#reset-filters').disabled = !active;
const numTotal = installed.childElementCount;
2017-08-15 11:40:36 +00:00
const numHidden = installed.getElementsByClassName('entry hidden').length;
2018-01-01 17:02:49 +00:00
const numShown = numTotal - numHidden;
2017-08-15 11:40:36 +00:00
if (filtersSelector.numShown !== numShown ||
filtersSelector.numTotal !== numTotal) {
filtersSelector.numShown = numShown;
filtersSelector.numTotal = numTotal;
$('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]);
document.body.classList.toggle('all-styles-hidden-by-filters',
!numShown && numTotal && filtersSelector.hide);
2017-08-15 11:40:36 +00:00
}
}
async function searchStyles({immediately, container} = {}) {
2018-01-01 17:02:49 +00:00
const el = $('#search');
const elMode = $('#searchMode');
2018-01-01 17:02:49 +00:00
const query = el.value.trim();
const mode = elMode.value;
if (query === el.lastValue && mode === elMode.lastValue && !immediately && !container) {
2017-08-15 11:40:36 +00:00
return;
}
if (!immediately) {
debounce(searchStyles, 150, {immediately: true});
return;
}
2018-01-01 17:02:49 +00:00
el.lastValue = query;
elMode.lastValue = mode;
2017-08-15 11:40:36 +00:00
const all = installed.children;
const entries = container && container.children || container || all;
const idsToSearch = entries !== all && [...entries].map(el => el.styleId);
const ids = entries[0]
? await API.styles.searchDB({query, mode, ids: idsToSearch})
: [];
let needsRefilter = false;
for (const entry of entries) {
const isMatching = ids.includes(entry.styleId);
if (entry.classList.contains('not-matching') !== !isMatching) {
entry.classList.toggle('not-matching', !isMatching);
needsRefilter = true;
}
}
if (needsRefilter && !container) {
filterOnChange({forceRefilter: true, alreadySearched: true});
}
return container;
2017-08-15 11:40:36 +00:00
}