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 {
 | 
					.hidden {
 | 
				
			||||||
  display: none
 | 
					  display: none !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fieldset {
 | 
					fieldset {
 | 
				
			||||||
| 
						 | 
					@ -498,13 +498,8 @@ fieldset {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fieldset > * {
 | 
					fieldset > * {
 | 
				
			||||||
  display: block;
 | 
					  display: flex;
 | 
				
			||||||
}
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					 | 
				
			||||||
.enabled-only > .disabled,
 | 
					 | 
				
			||||||
.edited-only > .updatable,
 | 
					 | 
				
			||||||
.updates-only > .entry:not(.can-update) {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#search {
 | 
					#search {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								manage.html
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								manage.html
									
									
									
									
									
								
							| 
						 | 
					@ -128,10 +128,19 @@
 | 
				
			||||||
  <h1 id="manage-heading" i18n-text="manageHeading"></h1>
 | 
					  <h1 id="manage-heading" i18n-text="manageHeading"></h1>
 | 
				
			||||||
  <fieldset>
 | 
					  <fieldset>
 | 
				
			||||||
    <legend id="filters" i18n-text="manageFilters"></legend>
 | 
					    <legend id="filters" i18n-text="manageFilters"></legend>
 | 
				
			||||||
    <label><input id="manage.onlyEnabled" type="checkbox"><span i18n-text="manageOnlyEnabled"></span></label>
 | 
					    <label>
 | 
				
			||||||
    <label><input id="manage.onlyEdited" type="checkbox"><span i18n-text="manageOnlyEdited"></span></label>
 | 
					      <input id="manage.onlyEnabled" type="checkbox" data-filter=".disabled">
 | 
				
			||||||
    <label id="onlyUpdates" class="hidden"><input type="checkbox"><span i18n-text="manageOnlyUpdates"></span></label>
 | 
					      <span i18n-text="manageOnlyEnabled"></span>
 | 
				
			||||||
    <input id="search" type="search" i18n-placeholder="searchStyles">
 | 
					    </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>
 | 
					  </fieldset>
 | 
				
			||||||
  <p>
 | 
					  <p>
 | 
				
			||||||
    <button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
 | 
					    <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';
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let installed;
 | 
					let installed;
 | 
				
			||||||
 | 
					const filtersSelector = {
 | 
				
			||||||
 | 
					  hide: '',
 | 
				
			||||||
 | 
					  unhide: '',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const newUI = {
 | 
					const newUI = {
 | 
				
			||||||
  enabled: prefs.get('manage.newUI'),
 | 
					  enabled: prefs.get('manage.newUI'),
 | 
				
			||||||
| 
						 | 
					@ -65,15 +69,6 @@ function initGlobalEvents() {
 | 
				
			||||||
  // remember scroll position on normal history navigation
 | 
					  // remember scroll position on normal history navigation
 | 
				
			||||||
  window.onbeforeunload = rememberScrollPosition;
 | 
					  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 => {
 | 
					  $$('[data-toggle-on-click]').forEach(el => {
 | 
				
			||||||
    el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden');
 | 
					    el.onclick = () => $(el.dataset.toggleOnClick).classList.toggle('hidden');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -88,6 +83,11 @@ function initGlobalEvents() {
 | 
				
			||||||
    'manage.newUI.targets',
 | 
					    'manage.newUI.targets',
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $$('[data-filter]').forEach(el => {
 | 
				
			||||||
 | 
					    el.onchange = handleEvent.filterOnChange;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  handleEvent.filterOnChange({forceRefilter: true});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $$('[id^="manage.newUI"]')
 | 
					  $$('[id^="manage.newUI"]')
 | 
				
			||||||
    .forEach(el => (el.oninput = (el.onchange = switchUI)));
 | 
					    .forEach(el => (el.oninput = (el.onchange = switchUI)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -111,11 +111,7 @@ function showStyles(styles = []) {
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if ($('#search').value.trim()) {
 | 
					    filterAndAppend({container: renderBin});
 | 
				
			||||||
      // re-apply filtering on history Back
 | 
					 | 
				
			||||||
      searchStyles({immediately: true, container: renderBin});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    installed.appendChild(renderBin);
 | 
					 | 
				
			||||||
    if (index < sorted.length) {
 | 
					    if (index < sorted.length) {
 | 
				
			||||||
      setTimeout(renderStyles, 0, index);
 | 
					      setTimeout(renderStyles, 0, index);
 | 
				
			||||||
    } else if (shouldRenderAll && 'scrollY' in (history.state || {})) {
 | 
					    } else if (shouldRenderAll && 'scrollY' in (history.state || {})) {
 | 
				
			||||||
| 
						 | 
					@ -337,50 +333,67 @@ Object.assign(handleEvent, {
 | 
				
			||||||
    this.closest('.applies-to').classList.toggle('expanded');
 | 
					    this.closest('.applies-to').classList.toggle('expanded');
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  loadFavicons(container = installed) {
 | 
					  loadFavicons(container = document.body) {
 | 
				
			||||||
    for (const img of container.getElementsByTagName('img')) {
 | 
					    for (const img of container.getElementsByTagName('img')) {
 | 
				
			||||||
      if (img.dataset.src) {
 | 
					      if (img.dataset.src) {
 | 
				
			||||||
        img.src = img.dataset.src;
 | 
					        img.src = img.dataset.src;
 | 
				
			||||||
        delete 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} = {}) {
 | 
					function handleUpdate(style, {reason} = {}) {
 | 
				
			||||||
  const element = createStyleElement({style});
 | 
					  const entry = createStyleElement({style});
 | 
				
			||||||
  if ($('#search').value.trim()) {
 | 
					  const oldEntry = $('#style-' + style.id);
 | 
				
			||||||
    const renderBin = document.createDocumentFragment();
 | 
					  if (oldEntry) {
 | 
				
			||||||
    renderBin.appendChild(element);
 | 
					    if (oldEntry.styleNameLowerCase == entry.styleNameLowerCase) {
 | 
				
			||||||
    searchStyles({immediately: true, container: renderBin});
 | 
					      installed.replaceChild(entry, oldEntry);
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const oldElement = $('#style-' + style.id, installed);
 | 
					 | 
				
			||||||
  if (oldElement) {
 | 
					 | 
				
			||||||
    if (oldElement.styleNameLowerCase == element.styleNameLowerCase) {
 | 
					 | 
				
			||||||
      installed.replaceChild(element, oldElement);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      oldElement.remove();
 | 
					      oldEntry.remove();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (reason == 'update') {
 | 
					    if (reason == 'update') {
 | 
				
			||||||
      element.classList.add('update-done');
 | 
					      entry.classList.add('update-done');
 | 
				
			||||||
      element.classList.remove('can-update', 'updatable');
 | 
					      entry.classList.remove('can-update', 'updatable');
 | 
				
			||||||
      $('.update-note', element).textContent = t('updateCompleted');
 | 
					      $('.update-note', entry).textContent = t('updateCompleted');
 | 
				
			||||||
      renderUpdatesOnlyFilter();
 | 
					      renderUpdatesOnlyFilter();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  installed.insertBefore(element, findNextElement(style));
 | 
					  filterAndAppend({entry});
 | 
				
			||||||
  if (reason != 'import') {
 | 
					  if (!entry.classList.contains('hidden') && reason != 'import') {
 | 
				
			||||||
    animateElement(element, {className: 'highlight'});
 | 
					    animateElement(entry, {className: 'highlight'});
 | 
				
			||||||
    scrollElementIntoView(element);
 | 
					    scrollElementIntoView(entry);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleDelete(id) {
 | 
					function handleDelete(id) {
 | 
				
			||||||
  const node = $('#style-' + id, installed);
 | 
					  const node = $('#style-' + id);
 | 
				
			||||||
  if (node) {
 | 
					  if (node) {
 | 
				
			||||||
    node.remove();
 | 
					    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'));
 | 
					        $('#onlyUpdates').classList.toggle('hidden', !$('.can-update'));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (filtersSelector.hide) {
 | 
				
			||||||
 | 
					      filterAndAppend({entry: this.element});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static download(url) {
 | 
					  static download(url) {
 | 
				
			||||||
| 
						 | 
					@ -636,27 +652,41 @@ function renderUpdatesOnlyFilter({show, check} = {}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function searchStyles({immediately, container}) {
 | 
					function searchStyles({immediately, container}) {
 | 
				
			||||||
  const query = $('#search').value.toLocaleLowerCase();
 | 
					  const searchElement = $('#search');
 | 
				
			||||||
  if (query == (searchStyles.lastQuery || '') && !immediately && !container) {
 | 
					  const query = searchElement.value.toLocaleLowerCase();
 | 
				
			||||||
 | 
					  const queryPrev = searchElement.lastValue || '';
 | 
				
			||||||
 | 
					  if (query == queryPrev && !immediately && !container) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  searchStyles.lastQuery = query;
 | 
					 | 
				
			||||||
  if (!immediately) {
 | 
					  if (!immediately) {
 | 
				
			||||||
    clearTimeout(searchStyles.timeout);
 | 
					    clearTimeout(searchStyles.timeout);
 | 
				
			||||||
    searchStyles.timeout = setTimeout(searchStyles, 150, {immediately: true});
 | 
					    searchStyles.timeout = setTimeout(searchStyles, 150, {immediately: true});
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  searchElement.lastValue = query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (const element of (container || installed).children) {
 | 
					  const searchInVisible = queryPrev && query.includes(queryPrev);
 | 
				
			||||||
    const style = BG.cachedStyles.byId.get(element.styleId) || {};
 | 
					  const entries = container && container.children || container ||
 | 
				
			||||||
    if (style) {
 | 
					    (searchInVisible ? $$('.entry:not(.hidden)') : installed.children);
 | 
				
			||||||
      const isMatching = !query
 | 
					  let needsRefilter = false;
 | 
				
			||||||
        || isMatchingText(style.name)
 | 
					  for (const entry of entries) {
 | 
				
			||||||
        || style.url && isMatchingText(style.url)
 | 
					    let isMatching = !query;
 | 
				
			||||||
        || isMatchingStyle(style);
 | 
					    if (!isMatching) {
 | 
				
			||||||
      element.style.display = isMatching ? '' : 'none';
 | 
					      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) {
 | 
					  function isMatchingStyle(style) {
 | 
				
			||||||
    for (const section of style.sections) {
 | 
					    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() {
 | 
					function rememberScrollPosition() {
 | 
				
			||||||
  history.replaceState({scrollY: window.scrollY}, document.title);
 | 
					  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