2020-02-02 04:36:54 +00:00
|
|
|
/* global installed messageBox sorter $ $$ $create t debounce prefs API router */
|
2018-11-07 06:09:29 +00:00
|
|
|
/* exported filterAndAppend */
|
2017-08-15 11:40:36 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const filtersSelector = {
|
|
|
|
hide: '',
|
|
|
|
unhide: '',
|
|
|
|
numShown: 0,
|
|
|
|
numTotal: 0,
|
|
|
|
};
|
|
|
|
|
2020-02-02 04:36:54 +00:00
|
|
|
let initialized = false;
|
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
router.watch({search: ['search', 'searchMode']}, ([search, mode]) => {
|
2020-02-02 04:36:54 +00:00
|
|
|
$('#search').value = search || '';
|
2020-11-18 11:17:15 +00:00
|
|
|
if (mode) $('#searchMode').value = mode;
|
2020-02-02 04:36:54 +00:00
|
|
|
if (!initialized) {
|
2020-10-15 10:55:27 +00:00
|
|
|
initFilters();
|
2020-02-02 04:36:54 +00:00
|
|
|
initialized = true;
|
|
|
|
} else {
|
|
|
|
searchStyles();
|
|
|
|
}
|
|
|
|
});
|
2017-08-22 16:29:26 +00:00
|
|
|
|
2017-08-22 14:22:15 +00:00
|
|
|
HTMLSelectElement.prototype.adjustWidth = function () {
|
2020-11-18 11:17:15 +00:00
|
|
|
const sel = this.selectedOptions[0];
|
|
|
|
if (!sel) return;
|
|
|
|
const wOld = parseFloat(this.style.width);
|
|
|
|
const opts = [...this];
|
|
|
|
opts.forEach(opt => opt !== sel && opt.remove());
|
|
|
|
this.style.width = '';
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
const w = this.offsetWidth;
|
|
|
|
if (w && wOld !== w) this.style.width = w + 'px';
|
|
|
|
this.append(...opts);
|
|
|
|
});
|
2017-08-22 14:22:15 +00:00
|
|
|
};
|
2017-08-15 11:40:36 +00:00
|
|
|
|
2020-10-15 10:55:27 +00:00
|
|
|
function initFilters() {
|
2020-11-18 11:17:15 +00:00
|
|
|
$('#search').oninput = $('#searchMode').oninput = function (e) {
|
|
|
|
router.updateSearch(this.id, e.target.value);
|
2020-02-02 04:36:54 +00:00
|
|
|
};
|
|
|
|
|
2017-12-09 15:25:44 +00:00
|
|
|
$('#search-help').onclick = event => {
|
|
|
|
event.preventDefault();
|
2017-12-06 06:39:45 +00:00
|
|
|
messageBox({
|
|
|
|
className: 'help-text',
|
2020-11-18 11:17:15 +00:00
|
|
|
title: t('search'),
|
2017-12-06 06:39:45 +00:00
|
|
|
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
|
|
|
|
2017-08-21 18:07:41 +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);
|
|
|
|
// avoid triggering MutationObserver during page load
|
|
|
|
if (document.readyState === 'complete') {
|
|
|
|
el.adjustWidth();
|
2017-08-21 18:07:41 +00:00
|
|
|
}
|
|
|
|
};
|
2017-08-22 14:22:15 +00:00
|
|
|
el.onchange({target: el});
|
2017-08-21 18:07:41 +00:00
|
|
|
});
|
|
|
|
|
2017-08-15 11:40:36 +00:00
|
|
|
$$('[data-filter]').forEach(el => {
|
|
|
|
el.onchange = filterOnChange;
|
|
|
|
if (el.closest('.hidden')) {
|
|
|
|
el.checked = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-12-06 01:18:51 +00:00
|
|
|
$('#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;
|
2018-11-07 06:09:29 +00:00
|
|
|
if (el.id in prefs.defaults) {
|
2017-12-06 01:18:51 +00:00
|
|
|
prefs.set(el.id, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filterOnChange({forceRefilter: true});
|
2020-02-02 04:36:54 +00:00
|
|
|
router.updateSearch('search', '');
|
2017-12-06 01:18:51 +00:00
|
|
|
};
|
|
|
|
|
2017-12-05 21:05:23 +00:00
|
|
|
// Adjust width after selects are visible
|
|
|
|
prefs.subscribe(['manage.filters.expanded'], () => {
|
|
|
|
const el = $('#filters');
|
|
|
|
if (el.open) {
|
2020-11-18 11:17:15 +00:00
|
|
|
$$('.filter-selection select', el).forEach(select => select.adjustWidth());
|
2017-12-05 21:05:23 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-08-22 14:22:15 +00:00
|
|
|
filterOnChange({forceRefilter: true});
|
2020-02-02 04:36:54 +00:00
|
|
|
}
|
2017-08-15 11:40:36 +00:00
|
|
|
|
|
|
|
|
2020-11-18 11:17:15 +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)
|
2020-11-18 11:17:15 +00:00
|
|
|
.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) {
|
2020-11-18 11:17:15 +00:00
|
|
|
reapplyFilter(installed, alreadySearched).then(sorter.updateStripes);
|
2017-08-22 14:22:15 +00:00
|
|
|
}
|
2017-08-15 11:40:36 +00:00
|
|
|
}
|
|
|
|
|
2018-02-26 19:57:03 +00:00
|
|
|
/**
|
|
|
|
* @returns {Promise} resolves on async search
|
|
|
|
*/
|
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
|
|
|
|
*/
|
2018-01-01 17:02:49 +00:00
|
|
|
function reapplyFilter(container = installed, alreadySearched) {
|
|
|
|
if (!alreadySearched && $('#search').value.trim()) {
|
2018-02-26 19:57:03 +00:00
|
|
|
return searchStyles({immediately: true, container})
|
2018-01-01 17:02:49 +00:00
|
|
|
.then(() => reapplyFilter(container, true));
|
|
|
|
}
|
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);
|
2018-02-26 19:57:03 +00:00
|
|
|
return Promise.resolve();
|
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();
|
2018-02-26 19:57:03 +00:00
|
|
|
return Promise.resolve();
|
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();
|
2018-02-26 19:57:03 +00:00
|
|
|
return Promise.resolve();
|
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();
|
2018-02-26 19:57:03 +00:00
|
|
|
return Promise.resolve();
|
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() {
|
2017-12-12 18:33:41 +00:00
|
|
|
const active = filtersSelector.hide !== '';
|
|
|
|
$('#filters summary').classList.toggle('active', active);
|
|
|
|
$('#reset-filters').disabled = !active;
|
2018-01-01 17:02:49 +00:00
|
|
|
const numTotal = installed.children.length;
|
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]);
|
2017-12-04 18:00:06 +00:00
|
|
|
document.body.classList.toggle('all-styles-hidden-by-filters',
|
|
|
|
!numShown && numTotal && filtersSelector.hide);
|
2017-08-15 11:40:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
async function searchStyles({immediately, container} = {}) {
|
2018-01-01 17:02:49 +00:00
|
|
|
const el = $('#search');
|
2020-11-18 11:17:15 +00:00
|
|
|
const elMode = $('#searchMode');
|
2018-01-01 17:02:49 +00:00
|
|
|
const query = el.value.trim();
|
2020-11-18 11:17:15 +00:00
|
|
|
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;
|
2020-11-18 11:17:15 +00:00
|
|
|
elMode.lastValue = mode;
|
2017-08-15 11:40:36 +00:00
|
|
|
|
2020-11-18 11:17:15 +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.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;
|
2017-12-06 06:39:45 +00:00
|
|
|
}
|
2020-11-18 11:17:15 +00:00
|
|
|
}
|
|
|
|
if (needsRefilter && !container) {
|
|
|
|
filterOnChange({forceRefilter: true, alreadySearched: true});
|
|
|
|
}
|
|
|
|
return container;
|
2017-08-15 11:40:36 +00:00
|
|
|
}
|