| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- /* global installed */
- 'use strict';
- const filtersSelector = {
- hide: '',
- unhide: '',
- numShown: 0,
- numTotal: 0,
- };
- const urlFilterParam = new URLSearchParams(location.search).get('url');
- if (location.search) {
- history.replaceState(0, document.title, location.origin + location.pathname);
- }
- HTMLSelectElement.prototype.adjustWidth = function () {
- const parent = this.parentNode;
- const singleSelect = this.cloneNode(false);
- singleSelect.style.width = '';
- singleSelect.appendChild(this.selectedOptions[0].cloneNode(true));
- parent.replaceChild(singleSelect, this);
- if (this.style.width !== singleSelect.offsetWidth + 'px') {
- this.style.width = singleSelect.offsetWidth + 'px';
- }
- parent.replaceChild(this, singleSelect);
- };
- onDOMready().then(onBackgroundReady).then(() => {
- $('#search').oninput = searchStyles;
- if (urlFilterParam) {
- $('#search').value = 'url:' + urlFilterParam;
- }
- $$('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);
- 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();
- }
- };
- el.onchange({target: el});
- });
- $$('[data-filter]').forEach(el => {
- el.onchange = filterOnChange;
- if (el.closest('.hidden')) {
- el.checked = false;
- }
- });
- filterOnChange({forceRefilter: true});
- });
- function filterOnChange({target: el, forceRefilter}) {
- 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(','))
- ].join(hide ? ',' : '');
- Object.assign(filtersSelector, {
- hide: buildFilter(true),
- unhide: buildFilter(false),
- });
- if (installed) {
- reapplyFilter();
- }
- }
- 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');
- }
- } else if ($('#search').value.trim()) {
- searchStyles({immediately: true, container});
- }
- reapplyFilter(container);
- }
- function reapplyFilter(container = installed) {
- // 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;
- } else if (toUnhide.length && $('#search').value.trim()) {
- searchStyles({immediately: true, container: toUnhide});
- filterContainer({hide: false});
- }
- // filtering needed or a single-element job from handleUpdate()
- const entries = installed.children;
- const numEntries = entries.length;
- let numVisible = numEntries - $$('.entry.hidden').length;
- for (const entry of toUnhide.children || toUnhide) {
- const next = findInsertionPoint(entry);
- if (entry.nextElementSibling !== next) {
- installed.insertBefore(entry, next);
- }
- if (entry.classList.contains('hidden')) {
- entry.classList.remove('hidden');
- numVisible++;
- }
- }
- // B: hide
- if (filtersSelector.hide) {
- filterContainer({hide: true});
- }
- if (!toHide.length) {
- showFiltersStats();
- return;
- }
- 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) {
- for (const entry of toHide) {
- installed.appendChild(entry);
- }
- installed.insertBefore(container, $('.entry.hidden'));
- showFiltersStats();
- return;
- }
- // normal filtering of the page or a single-element job from handleUpdate()
- // we need to keep the visible entries together at the start
- // first pass only moves one hidden entry in hidden groups with odd number of items
- shuffle(false);
- setTimeout(shuffle, 0, true);
- // single-element job from handleEvent(): add the last wraith
- if (toHide.length === 1 && toHide[0].parentElement !== installed) {
- installed.appendChild(toHide[0]);
- }
- showFiltersStats();
- return;
- /***************************************/
- 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);
- }
- }
- function shuffle(fullPass) {
- if (fullPass && !document.body.classList.contains('update-in-progress')) {
- $('#check-all-updates').disabled = !$('.updatable:not(.can-update)');
- }
- // 1. skip the visible group on top
- let firstHidden = $('#installed > .hidden');
- let entry = firstHidden;
- let i = [...entries].indexOf(entry);
- let horizon = entries[numVisible];
- const skipGroup = state => {
- const start = i;
- const first = entry;
- while (entry && entry.classList.contains('hidden') === state) {
- entry = entry.nextElementSibling;
- i++;
- }
- return {first, start, len: i - start};
- };
- let prevGroup = i ? {first: entries[0], start: 0, len: i} : skipGroup(true);
- // eslint-disable-next-line no-unmodified-loop-condition
- while (entry) {
- // 2a. find the next hidden group's start and end
- // 2b. find the next visible group's start and end
- const isHidden = entry.classList.contains('hidden');
- const group = skipGroup(isHidden);
- const hidden = isHidden ? group : prevGroup;
- const visible = isHidden ? prevGroup : group;
- // 3. move the shortest group; repeat 2-3
- if (hidden.len < visible.len && (fullPass || hidden.len % 2)) {
- // 3a. move hidden under the horizon
- for (let j = 0; j < (fullPass ? hidden.len : 1); j++) {
- const entry = entries[hidden.start];
- installed.insertBefore(entry, horizon);
- horizon = entry;
- i--;
- }
- prevGroup = isHidden ? skipGroup(false) : group;
- firstHidden = entry;
- } else if (isHidden || !fullPass) {
- prevGroup = group;
- } else {
- // 3b. move visible above the horizon
- for (let j = 0; j < visible.len; j++) {
- const entry = entries[visible.start + j];
- installed.insertBefore(entry, firstHidden);
- }
- prevGroup = {
- first: firstHidden,
- start: hidden.start + visible.len,
- len: hidden.len + skipGroup(true).len,
- };
- }
- }
- }
- function findInsertionPoint(entry) {
- const nameLLC = entry.styleNameLowerCase;
- let a = 0;
- let b = Math.min(numEntries, numVisible) - 1;
- if (b < 0) {
- return entries[numVisible];
- }
- if (entries[0].styleNameLowerCase > nameLLC) {
- return entries[0];
- }
- if (entries[b].styleNameLowerCase <= nameLLC) {
- return entries[numVisible];
- }
- // bisect
- while (a < b - 1) {
- const c = (a + b) / 2 | 0;
- if (nameLLC < entries[c].styleNameLowerCase) {
- b = c;
- } else {
- a = c;
- }
- }
- if (entries[a].styleNameLowerCase > nameLLC) {
- return entries[a];
- }
- while (a <= b && entries[a].styleNameLowerCase < nameLLC) {
- a++;
- }
- return entries[entries[a].styleNameLowerCase <= nameLLC ? a + 1 : a];
- }
- }
- function showFiltersStats({immediately} = {}) {
- if (!immediately) {
- debounce(showFiltersStats, 100, {immediately: true});
- return;
- }
- $('#filters').classList.toggle('active', filtersSelector.hide !== '');
- const numTotal = BG.cachedStyles.list.length;
- const numHidden = installed.getElementsByClassName('entry hidden').length;
- const numShown = Math.min(numTotal - numHidden, installed.children.length);
- if (filtersSelector.numShown !== numShown ||
- filtersSelector.numTotal !== numTotal) {
- filtersSelector.numShown = numShown;
- filtersSelector.numTotal = numTotal;
- $('#filters-stats').textContent = t('filteredStyles', [numShown, numTotal]);
- }
- }
- function searchStyles({immediately, container}) {
- const searchElement = $('#search');
- const urlMode = /^\s*url:/i.test(searchElement.value);
- const query = urlMode
- ? searchElement.value.replace(/^\s*url:/i, '').trim()
- : searchElement.value.toLocaleLowerCase();
- const queryPrev = searchElement.lastValue || '';
- if (query === queryPrev && !immediately && !container) {
- return;
- }
- if (!immediately) {
- debounce(searchStyles, 150, {immediately: true});
- return;
- }
- searchElement.lastValue = query;
- const searchInVisible = !urlMode && queryPrev && query.includes(queryPrev);
- const entries = container && container.children || container ||
- (searchInVisible ? $$('.entry:not(.hidden)') : installed.children);
- const siteStyleIds = urlMode &&
- new Set(BG.filterStyles({matchUrl: query}).map(style => style.id));
- let needsRefilter = false;
- for (const entry of entries) {
- let isMatching = !query;
- if (!isMatching) {
- const style = urlMode ? siteStyleIds.has(entry.styleId) :
- BG.cachedStyles.byId.get(entry.styleId) || {};
- isMatching = Boolean(style && (
- urlMode ||
- isMatchingText(style.name) ||
- style.url && isMatchingText(style.url) ||
- isMatchingStyle(style)));
- }
- if (entry.classList.contains('not-matching') !== !isMatching) {
- entry.classList.toggle('not-matching', !isMatching);
- needsRefilter = true;
- }
- }
- if (needsRefilter && !container) {
- filterOnChange({forceRefilter: true});
- }
- return;
- function isMatchingStyle(style) {
- for (const section of style.sections) {
- for (const prop in section) {
- const value = section[prop];
- switch (typeof value) {
- case 'string':
- if (isMatchingText(value)) {
- return true;
- }
- break;
- case 'object':
- for (const str of value) {
- if (isMatchingText(str)) {
- return true;
- }
- }
- break;
- }
- }
- }
- }
- function isMatchingText(text) {
- return text.toLocaleLowerCase().indexOf(query) >= 0;
- }
- }
|