rework waitForSelector and collapsible <details>
This commit is contained in:
parent
246d0a3bad
commit
ffdfc648f0
125
js/dom.js
125
js/dom.js
|
@ -307,43 +307,85 @@ define(require => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
/**
|
||||||
// and establishes a two-way connection between the document elements and the actual prefs
|
* @param {string} selector - beware of $ quirks with `#dotted.id` that won't work with $$
|
||||||
waitForSelector(selector, {stopOnDomReady = true} = {}) {
|
* @param {Object} [opt]
|
||||||
// TODO: if used concurrently see if it's worth reworking to use just one observer internally
|
* @param {function(HTMLElement, HTMLElement[]):boolean} [opt.recur] - called on each match
|
||||||
return Promise.resolve($(selector) || new Promise(resolve => {
|
with (firstMatchingElement, allMatchingElements) parameters until stopOnDomReady,
|
||||||
|
you can also return `false` to disconnect the observer
|
||||||
|
* @param {boolean} [opt.stopOnDomReady] - stop observing on DOM ready
|
||||||
|
* @returns {Promise<HTMLElement>} - resolves on first match
|
||||||
|
*/
|
||||||
|
waitForSelector(selector, {recur, stopOnDomReady = true} = {}) {
|
||||||
|
let el = $(selector);
|
||||||
|
let elems, isResolved;
|
||||||
|
return el && (!recur || recur(el, (elems = $$(selector))) === false)
|
||||||
|
? Promise.resolve(el)
|
||||||
|
: new Promise(resolve => {
|
||||||
const mo = new MutationObserver(() => {
|
const mo = new MutationObserver(() => {
|
||||||
const el = $(selector);
|
if (!el) el = $(selector);
|
||||||
if (el) {
|
if (!el) return;
|
||||||
|
if (!recur ||
|
||||||
|
callRecur() === false ||
|
||||||
|
stopOnDomReady && document.readyState === 'complete') {
|
||||||
mo.disconnect();
|
mo.disconnect();
|
||||||
|
}
|
||||||
|
if (!isResolved) {
|
||||||
|
isResolved = true;
|
||||||
resolve(el);
|
resolve(el);
|
||||||
} else if (stopOnDomReady && document.readyState === 'complete') {
|
|
||||||
mo.disconnect();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mo.observe(document, {childList: true, subtree: true});
|
mo.observe(document, {childList: true, subtree: true});
|
||||||
}));
|
});
|
||||||
|
function callRecur() {
|
||||||
|
const all = $$(selector); // simpler and faster than analyzing each node in `mutations`
|
||||||
|
const added = !elems ? all : all.filter(el => !elems.includes(el));
|
||||||
|
if (added.length) {
|
||||||
|
elems = all;
|
||||||
|
return recur(added[0], added);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Init
|
//#region Init
|
||||||
|
|
||||||
|
const Collapsible = {
|
||||||
|
bindEvents(_, elems) {
|
||||||
|
const prefKeys = [];
|
||||||
|
for (const el of elems) {
|
||||||
|
prefKeys.push(el.dataset.pref);
|
||||||
|
($('h2', el) || el).on('click', Collapsible.saveOnClick);
|
||||||
|
}
|
||||||
|
prefs.subscribe(prefKeys, Collapsible.updateOnPrefChange, {runNow: true});
|
||||||
|
},
|
||||||
|
canSave(el) {
|
||||||
|
return !el.matches('.compact-layout .ignore-pref-if-compact');
|
||||||
|
},
|
||||||
|
async saveOnClick(event) {
|
||||||
|
if (event.target.closest('.intercepts-click')) {
|
||||||
|
event.preventDefault();
|
||||||
|
} else {
|
||||||
|
const el = event.target.closest('details');
|
||||||
|
await new Promise(setTimeout);
|
||||||
|
if (Collapsible.canSave(el)) {
|
||||||
|
prefs.set(el.dataset.pref, el.open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateOnPrefChange(key, value) {
|
||||||
|
const el = $(`details[data-pref="${key}"]`);
|
||||||
|
if (el.open !== value && Collapsible.canSave(el)) {
|
||||||
|
el.open = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
require(['/js/prefs'], p => {
|
require(['/js/prefs'], p => {
|
||||||
prefs = p;
|
prefs = p;
|
||||||
dom.waitForSelector('details[data-pref]')
|
dom.waitForSelector('details[data-pref]', {recur: Collapsible.bindEvents});
|
||||||
.then(() => requestAnimationFrame(initCollapsibles));
|
if (!chrome.app) addFaviconFF();
|
||||||
if (!chrome.app) {
|
|
||||||
// add favicon in Firefox
|
|
||||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
|
||||||
for (const size of [38, 32, 19, 16]) {
|
|
||||||
document.head.appendChild($create('link', {
|
|
||||||
rel: 'icon',
|
|
||||||
href: `/images/icon/${iconset}${size}.png`,
|
|
||||||
sizes: size + 'x' + size,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
require(['/js/toolbox'], m => {
|
require(['/js/toolbox'], m => {
|
||||||
|
@ -368,6 +410,17 @@ define(require => {
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Internals
|
//#region Internals
|
||||||
|
|
||||||
|
function addFaviconFF() {
|
||||||
|
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
||||||
|
for (const size of [38, 32, 19, 16]) {
|
||||||
|
document.head.appendChild($create('link', {
|
||||||
|
rel: 'icon',
|
||||||
|
href: `/images/icon/${iconset}${size}.png`,
|
||||||
|
sizes: size + 'x' + size,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function changeFocusedInputOnWheel(event) {
|
function changeFocusedInputOnWheel(event) {
|
||||||
const el = document.activeElement;
|
const el = document.activeElement;
|
||||||
if (!el || el !== event.target && !el.contains(event.target)) {
|
if (!el || el !== event.target && !el.contains(event.target)) {
|
||||||
|
@ -408,32 +461,6 @@ define(require => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makes <details> with [data-pref] save/restore their state
|
|
||||||
function initCollapsibles() {
|
|
||||||
const onClick = async event => {
|
|
||||||
if (event.target.closest('.intercepts-click')) {
|
|
||||||
event.preventDefault();
|
|
||||||
} else {
|
|
||||||
const el = event.target.closest('details');
|
|
||||||
await new Promise(setTimeout);
|
|
||||||
if (!el.matches('.compact-layout .ignore-pref-if-compact')) {
|
|
||||||
prefs.set(el.dataset.pref, el.open);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const prefMap = {};
|
|
||||||
for (const el of $$('details[data-pref]')) {
|
|
||||||
prefMap[el.dataset.pref] = el;
|
|
||||||
($('h2', el) || el).on('click', onClick);
|
|
||||||
}
|
|
||||||
prefs.subscribe(Object.keys(prefMap), (key, value) => {
|
|
||||||
const el = prefMap[key];
|
|
||||||
if (el.open !== value && !el.matches('.compact-layout .ignore-pref-if-compact')) {
|
|
||||||
el.open = value;
|
|
||||||
}
|
|
||||||
}, {runNow: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
function keepAddressOnDummyClick(e) {
|
function keepAddressOnDummyClick(e) {
|
||||||
// avoid adding # to the page URL when clicking dummy links
|
// avoid adding # to the page URL when clicking dummy links
|
||||||
if (e.target.closest('a[href="#"]')) {
|
if (e.target.closest('a[href="#"]')) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user