Fix even-odd rules on entries
* Now filtering is done in js * Visible entries are always at the beginning of #installed * Hidden entries are always at the end of #installed * The code tries to minimize DOM reordering operations: * First pass only moves one hidden entry in hidden groups with odd number of items. * Second [full] pass runs after repaint.
This commit is contained in:
parent
1649a262cd
commit
eccabb8f27
11
manage.css
11
manage.css
|
@ -488,7 +488,7 @@ input[id^="manage.newUI"] {
|
|||
}
|
||||
|
||||
.hidden {
|
||||
display: none
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
@ -498,13 +498,8 @@ fieldset {
|
|||
}
|
||||
|
||||
fieldset > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enabled-only > .disabled,
|
||||
.edited-only > .updatable,
|
||||
.updates-only > .entry:not(.can-update) {
|
||||
display: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#search {
|
||||
|
|
17
manage.html
17
manage.html
|
@ -128,10 +128,19 @@
|
|||
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
|
||||
<fieldset>
|
||||
<legend id="filters" i18n-text="manageFilters"></legend>
|
||||
<label><input id="manage.onlyEnabled" type="checkbox"><span i18n-text="manageOnlyEnabled"></span></label>
|
||||
<label><input id="manage.onlyEdited" type="checkbox"><span i18n-text="manageOnlyEdited"></span></label>
|
||||
<label id="onlyUpdates" class="hidden"><input type="checkbox"><span i18n-text="manageOnlyUpdates"></span></label>
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles">
|
||||
<label>
|
||||
<input id="manage.onlyEnabled" type="checkbox" data-filter=".disabled">
|
||||
<span i18n-text="manageOnlyEnabled"></span>
|
||||
</label>
|
||||
<label>
|
||||
<input id="manage.onlyEdited" type="checkbox" data-filter=".updatable">
|
||||
<span i18n-text="manageOnlyEdited"></span>
|
||||
</label>
|
||||
<label id="onlyUpdates" class="hidden">
|
||||
<input type="checkbox" data-filter=":not(.can-update)">
|
||||
<span i18n-text="manageOnlyUpdates"></span>
|
||||
</label>
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles" data-filter=".not-matching">
|
||||
</fieldset>
|
||||
<p>
|
||||
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
|
||||
|
|
308
manage.js
308
manage.js
|
@ -2,6 +2,10 @@
|
|||
'use strict';
|
||||
|
||||
let installed;
|
||||
const filtersSelector = {
|
||||
hide: '',
|
||||
unhide: '',
|
||||
};
|
||||
|
||||
const newUI = {
|
||||
enabled: prefs.get('manage.newUI'),
|
||||
|
@ -65,15 +69,6 @@ function initGlobalEvents() {
|
|||
// remember scroll position on normal history navigation
|
||||
window.onbeforeunload = rememberScrollPosition;
|
||||
|
||||
for (const [className, checkbox] of [
|
||||
['enabled-only', $('#manage.onlyEnabled')],
|
||||
['edited-only', $('#manage.onlyEdited')],
|
||||
['updates-only', $('#onlyUpdates input')],
|
||||
]) {
|
||||
// will be triggered by setupLivePrefs immediately
|
||||
checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked);
|
||||
}
|
||||
|
||||
$$('[data-toggle-on-click]').forEach(el => {
|
||||
el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden');
|
||||
});
|
||||
|
@ -88,6 +83,11 @@ function initGlobalEvents() {
|
|||
'manage.newUI.targets',
|
||||
]);
|
||||
|
||||
$$('[data-filter]').forEach(el => {
|
||||
el.onchange = handleEvent.filterOnChange;
|
||||
});
|
||||
handleEvent.filterOnChange({forceRefilter: true});
|
||||
|
||||
$$('[id^="manage.newUI"]')
|
||||
.forEach(el => (el.oninput = (el.onchange = switchUI)));
|
||||
|
||||
|
@ -111,11 +111,7 @@ function showStyles(styles = []) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if ($('#search').value.trim()) {
|
||||
// re-apply filtering on history Back
|
||||
searchStyles({immediately: true, container: renderBin});
|
||||
}
|
||||
installed.appendChild(renderBin);
|
||||
filterAndAppend({container: renderBin});
|
||||
if (index < sorted.length) {
|
||||
setTimeout(renderStyles, 0, index);
|
||||
} else if (shouldRenderAll && 'scrollY' in (history.state || {})) {
|
||||
|
@ -337,50 +333,67 @@ Object.assign(handleEvent, {
|
|||
this.closest('.applies-to').classList.toggle('expanded');
|
||||
},
|
||||
|
||||
loadFavicons(container = installed) {
|
||||
loadFavicons(container = document.body) {
|
||||
for (const img of container.getElementsByTagName('img')) {
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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));
|
||||
Object.assign(filtersSelector, {
|
||||
hide: enabledFilters.map(el => '.entry:not(.hidden)' + el.dataset.filter).join(','),
|
||||
unhide: '.entry.hidden' + enabledFilters.map(el =>
|
||||
(':not(' + el.dataset.filter + ')').replace(/^:not\(:not\((.+?)\)\)$/, '$1')).join(''),
|
||||
});
|
||||
reapplyFilter();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function handleUpdate(style, {reason} = {}) {
|
||||
const element = createStyleElement({style});
|
||||
if ($('#search').value.trim()) {
|
||||
const renderBin = document.createDocumentFragment();
|
||||
renderBin.appendChild(element);
|
||||
searchStyles({immediately: true, container: renderBin});
|
||||
}
|
||||
const oldElement = $('#style-' + style.id, installed);
|
||||
if (oldElement) {
|
||||
if (oldElement.styleNameLowerCase == element.styleNameLowerCase) {
|
||||
installed.replaceChild(element, oldElement);
|
||||
const entry = createStyleElement({style});
|
||||
const oldEntry = $('#style-' + style.id);
|
||||
if (oldEntry) {
|
||||
if (oldEntry.styleNameLowerCase == entry.styleNameLowerCase) {
|
||||
installed.replaceChild(entry, oldEntry);
|
||||
} else {
|
||||
oldElement.remove();
|
||||
oldEntry.remove();
|
||||
}
|
||||
if (reason == 'update') {
|
||||
element.classList.add('update-done');
|
||||
element.classList.remove('can-update', 'updatable');
|
||||
$('.update-note', element).textContent = t('updateCompleted');
|
||||
entry.classList.add('update-done');
|
||||
entry.classList.remove('can-update', 'updatable');
|
||||
$('.update-note', entry).textContent = t('updateCompleted');
|
||||
renderUpdatesOnlyFilter();
|
||||
}
|
||||
}
|
||||
installed.insertBefore(element, findNextElement(style));
|
||||
if (reason != 'import') {
|
||||
animateElement(element, {className: 'highlight'});
|
||||
scrollElementIntoView(element);
|
||||
filterAndAppend({entry});
|
||||
if (!entry.classList.contains('hidden') && reason != 'import') {
|
||||
animateElement(entry, {className: 'highlight'});
|
||||
scrollElementIntoView(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDelete(id) {
|
||||
const node = $('#style-' + id, installed);
|
||||
const node = $('#style-' + id);
|
||||
if (node) {
|
||||
node.remove();
|
||||
if (node.matches('.can-update')) {
|
||||
const btnApply = $('#apply-all-updates');
|
||||
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,6 +603,9 @@ class Updater {
|
|||
$('#onlyUpdates').classList.toggle('hidden', !$('.can-update'));
|
||||
}
|
||||
}
|
||||
if (filtersSelector.hide) {
|
||||
filterAndAppend({entry: this.element});
|
||||
}
|
||||
}
|
||||
|
||||
static download(url) {
|
||||
|
@ -636,27 +652,41 @@ function renderUpdatesOnlyFilter({show, check} = {}) {
|
|||
|
||||
|
||||
function searchStyles({immediately, container}) {
|
||||
const query = $('#search').value.toLocaleLowerCase();
|
||||
if (query == (searchStyles.lastQuery || '') && !immediately && !container) {
|
||||
const searchElement = $('#search');
|
||||
const query = searchElement.value.toLocaleLowerCase();
|
||||
const queryPrev = searchElement.lastValue || '';
|
||||
if (query == queryPrev && !immediately && !container) {
|
||||
return;
|
||||
}
|
||||
searchStyles.lastQuery = query;
|
||||
if (!immediately) {
|
||||
clearTimeout(searchStyles.timeout);
|
||||
searchStyles.timeout = setTimeout(searchStyles, 150, {immediately: true});
|
||||
return;
|
||||
}
|
||||
searchElement.lastValue = query;
|
||||
|
||||
for (const element of (container || installed).children) {
|
||||
const style = BG.cachedStyles.byId.get(element.styleId) || {};
|
||||
if (style) {
|
||||
const isMatching = !query
|
||||
|| isMatchingText(style.name)
|
||||
|| style.url && isMatchingText(style.url)
|
||||
|| isMatchingStyle(style);
|
||||
element.style.display = isMatching ? '' : 'none';
|
||||
const searchInVisible = queryPrev && query.includes(queryPrev);
|
||||
const entries = container && container.children || container ||
|
||||
(searchInVisible ? $$('.entry:not(.hidden)') : installed.children);
|
||||
let needsRefilter = false;
|
||||
for (const entry of entries) {
|
||||
let isMatching = !query;
|
||||
if (!isMatching) {
|
||||
const style = BG.cachedStyles.byId.get(entry.styleId) || {};
|
||||
isMatching = Boolean(style && (
|
||||
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) {
|
||||
handleEvent.filterOnChange({forceRefilter: true});
|
||||
}
|
||||
return;
|
||||
|
||||
function isMatchingStyle(style) {
|
||||
for (const section of style.sections) {
|
||||
|
@ -686,39 +716,159 @@ function searchStyles({immediately, container}) {
|
|||
}
|
||||
|
||||
|
||||
function filterAndAppend({entry, container}) {
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.appendChild(entry);
|
||||
// reverse the visibility, otherwise reapplyFilter will see no need to work
|
||||
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if ($('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container});
|
||||
}
|
||||
reapplyFilter(container);
|
||||
}
|
||||
|
||||
|
||||
function reapplyFilter(container = installed) {
|
||||
$('#check-all-updates').disabled = !$('.updatable:not(.can-update)');
|
||||
// A: show
|
||||
const toUnhide = filtersSelector.hide ? $$(filtersSelector.unhide, container) : container;
|
||||
// showStyles() is building the page and no filters are active
|
||||
if (toUnhide instanceof DocumentFragment) {
|
||||
installed.appendChild(toUnhide);
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
const toHide = filtersSelector.hide ? $$(filtersSelector.hide, container) : [];
|
||||
if (!toHide.length) {
|
||||
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'));
|
||||
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]);
|
||||
}
|
||||
return;
|
||||
|
||||
function shuffle(fullPass) {
|
||||
// 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 rememberScrollPosition() {
|
||||
history.replaceState({scrollY: window.scrollY}, document.title);
|
||||
}
|
||||
|
||||
|
||||
function findNextElement(style) {
|
||||
const nameLLC = style.name.toLocaleLowerCase();
|
||||
const elements = installed.children;
|
||||
let a = 0;
|
||||
let b = elements.length - 1;
|
||||
if (b < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (elements[0].styleNameLowerCase > nameLLC) {
|
||||
return elements[0];
|
||||
}
|
||||
if (elements[b].styleNameLowerCase <= nameLLC) {
|
||||
return undefined;
|
||||
}
|
||||
// bisect
|
||||
while (a < b - 1) {
|
||||
const c = (a + b) / 2 | 0;
|
||||
if (nameLLC < elements[c].styleNameLowerCase) {
|
||||
b = c;
|
||||
} else {
|
||||
a = c;
|
||||
}
|
||||
}
|
||||
if (elements[a].styleNameLowerCase > nameLLC) {
|
||||
return elements[a];
|
||||
}
|
||||
while (a <= b && elements[a].name < nameLLC) {
|
||||
a++;
|
||||
}
|
||||
return elements[elements[a].styleNameLowerCase <= nameLLC ? a + 1 : a];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user