UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

487 lines (452 loc) 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DOMWatcher = void 0; var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs")); var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs")); var _dom = require("../utils/dom.cjs"); var _domAlter = require("../utils/dom-alter.cjs"); var _domEvents = require("../utils/dom-events.cjs"); var _log = require("../utils/log.cjs"); var _misc = require("../utils/misc.cjs"); var _text = require("../utils/text.cjs"); var _validation = require("../utils/validation.cjs"); var _callback = require("../modules/callback.cjs"); var _xMap = require("../modules/x-map.cjs"); var _debug = _interopRequireDefault(require("../debug/debug.cjs")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * @module Watchers/DOMWatcher */ /** * {@link DOMWatcher} listens for changes do the DOM tree. It's built on top of * {@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver | MutationObserver}. * * It manages registered callbacks globally and reuses MutationObservers for * more efficient performance. * * Each instance of DOMWatcher manages up to two MutationObservers: one * for `childList` changes and one for attribute changes, and it disconnects * them when there are no active callbacks for the relevant type. * * `characterData` and changes to base * {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node}s * (non-{@link https://developer.mozilla.org/en-US/docs/Web/API/Element | Element}) * are not supported. */ class DOMWatcher { /** * Creates a new instance of DOMWatcher with the given * {@link DOMWatcherConfig}. It does not save it for future reuse. */ static create(config) { return new DOMWatcher(getConfig(config), CONSTRUCTOR_KEY); } /** * Returns an existing instance of DOMWatcher with the given * {@link DOMWatcherConfig}, or creates a new one. * * **NOTE:** It saves it for future reuse, so don't use this for temporary * short-lived watchers. */ static reuse(config) { var _instances$get; const myConfig = getConfig(config); const configStrKey = (0, _text.objToStrKey)((0, _misc.omitKeys)(myConfig, { _root: null })); const root = myConfig._root === MH.getBody() ? null : myConfig._root; let instance = (_instances$get = instances.get(root)) === null || _instances$get === void 0 ? void 0 : _instances$get.get(configStrKey); if (!instance) { instance = new DOMWatcher(myConfig, CONSTRUCTOR_KEY); instances.sGet(root).set(configStrKey, instance); } return instance; } constructor(config, key) { /** * Call the given handler whenever there's a matching mutation within this * DOMWatcher's {@link DOMWatcherConfig.root | root}. * * If {@link OnMutationOptions.skipInitial | options.skipInitial} is `false` * (default), _and_ {@link OnMutationOptions.selector | options.selector} is * given, _and_ {@link OnMutationOptions.categories | options.categories} * includes "added", the handler is also called (almost) immediately with all * existing elements matching the selector under this DOMWatcher's * {@link DOMWatcherConfig.root | root}. * * **IMPORTANT:** The same handler can _not_ be added multiple times, even if * the options differ. If the handler has already been added, it is removed * and re-added with the current options. * * @throws {@link Errors.LisnUsageError | LisnUsageError} * If the options are not valid. */ _defineProperty(this, "onMutation", void 0); /** * Removes a previously added handler. */ _defineProperty(this, "offMutation", void 0); /** * Ignore an upcoming moving/adding/removing of an element. * * The operation must complete within the next cycle, by the time * MutationObserver calls us. * * Use this to prevent this instance of DOMWatcher from calling any callbacks * that listen for relevant changes as a result of this operation, to prevent * loops for example. * * **IMPORTANT:** * * Ignoring moving of an element from a parent _inside_ this DOMWatcher's * root to another parent that's _outside_ the root, will work as expected, * even though the "adding to the new parent" mutation will not be observed. * This is because the element's current parent at the time of the mutation * callback can be examined. * * However if you want to ignore moving of an element _from a parent outside * this DOMWatcher's root_ you need to specify from: null since the "removal * from the old parent" mutation would not be observed and there's no way to * examine it's previous parent at the time the "adding to the new parent" * mutation is observed. * * For this reason, setting `options.from` to be an element that's not under * the root is internally treated the same as `options.from: null`. */ _defineProperty(this, "ignoreMove", void 0); if (key !== CONSTRUCTOR_KEY) { throw MH.illegalConstructorError("DOMWatcher.create"); } const logger = _debug.default ? new _debug.default.Logger({ name: "DOMWatcher", logAtCreation: config }) : null; const buffer = (0, _xMap.newXMap)(t => ({ _target: t, _categoryBitmask: 0, _attributes: MH.newSet(), _addedTo: null, _removedFrom: null })); const allCallbacks = MH.newMap(); // ---------- let timer = null; const mutationHandler = records => { debug: logger === null || logger === void 0 || logger.debug9(`Got ${records.length} new records`, records); for (const record of records) { const target = MH.targetOf(record); const recType = record.type; /* istanbul ignore next */ if (!MH.isElement(target)) { continue; } if (recType === MC.S_CHILD_LIST) { for (const child of record.addedNodes) { if (MH.isElement(child)) { const operation = buffer.sGet(child); operation._addedTo = target; operation._categoryBitmask |= ADDED_BIT; } } for (const child of record.removedNodes) { if (MH.isElement(child)) { const operation = buffer.sGet(child); operation._removedFrom = target; operation._categoryBitmask |= REMOVED_BIT; } } // } else if (recType === MC.S_ATTRIBUTES && record.attributeName) { const operation = buffer.sGet(target); operation._attributes.add(record.attributeName); operation._categoryBitmask |= ATTRIBUTE_BIT; } } // Schedule flushing of the buffer asynchronously so that we can combine // the records from the two MutationObservers. if (!timer && MH.sizeOf(buffer)) { timer = MH.setTimer(() => { debug: logger === null || logger === void 0 || logger.debug9(`Processing ${buffer.size} operations`); for (const operation of buffer.values()) { if (shouldSkipOperation(operation)) { debug: logger === null || logger === void 0 || logger.debug10("Skipping operation", operation); } else { processOperation(operation); } } buffer.clear(); timer = null; }, 0); } }; const observers = { [MC.S_CHILD_LIST]: { _observer: MH.newMutationObserver(mutationHandler), _isActive: false }, [MC.S_ATTRIBUTES]: { _observer: MH.newMutationObserver(mutationHandler), _isActive: false } }; // ---------- const createCallback = (handler, options) => { var _allCallbacks$get; MH.remove((_allCallbacks$get = allCallbacks.get(handler)) === null || _allCallbacks$get === void 0 ? void 0 : _allCallbacks$get._callback); debug: logger === null || logger === void 0 || logger.debug5("Adding/updating handler", options); const callback = (0, _callback.wrapCallback)(handler); callback.onRemove(() => deleteHandler(handler)); allCallbacks.set(handler, { _callback: callback, _options: options }); return callback; }; // ---------- const setupOnMutation = async (handler, userOptions) => { var _config$_root; const options = getOptions(userOptions !== null && userOptions !== void 0 ? userOptions : {}); const callback = createCallback(handler, options); let root = (_config$_root = config._root) !== null && _config$_root !== void 0 ? _config$_root : MH.getBody(); if (!root) { root = await (0, _domEvents.waitForElement)(MH.getBody); } else { // So that the call is always async await null; } if (callback.isRemoved()) { return; } if (options._categoryBitmask & (ADDED_BIT | REMOVED_BIT)) { activateObserver(root, MC.S_CHILD_LIST); } if (options._categoryBitmask & ATTRIBUTE_BIT) { activateObserver(root, MC.S_ATTRIBUTES); } if (userOptions !== null && userOptions !== void 0 && userOptions.skipInitial || !options._selector || !(options._categoryBitmask & ADDED_BIT)) { return; } // As some of the matching elements that currently exist in the root may // have just been added and therefore in the MutationObserver's queue, to // avoid calling the handler with those entries twice, we empty its queue // now and process it (which would also invoke the newly added callback). // Then we skip any elements returned in querySelectorAll that were in // the queue. const childQueue = observers[MC.S_CHILD_LIST]._observer.takeRecords(); mutationHandler(childQueue); for (const element of [...MH.querySelectorAll(root, options._selector), ...(root.matches(options._selector) ? [root] : [])]) { const initOperation = { _target: element, _categoryBitmask: ADDED_BIT, _attributes: MH.newSet(), _addedTo: MH.parentOf(element), _removedFrom: null }; const bufferedOperation = buffer.get(element); const diffOperation = getDiffOperation(initOperation, bufferedOperation); if (diffOperation) { if (shouldSkipOperation(diffOperation)) { debug: logger === null || logger === void 0 || logger.debug10("Skipping operation", diffOperation); } else { debug: logger === null || logger === void 0 || logger.debug5("Calling initially with", diffOperation); await invokeCallback(callback, diffOperation); } } } }; // ---------- const deleteHandler = handler => { MH.deleteKey(allCallbacks, handler); let activeCategories = 0; for (const entry of allCallbacks.values()) { activeCategories |= entry._options._categoryBitmask; } if (!(activeCategories & (ADDED_BIT | REMOVED_BIT))) { deactivateObserver(MC.S_CHILD_LIST); } if (!(activeCategories & ATTRIBUTE_BIT)) { deactivateObserver(MC.S_ATTRIBUTES); } }; // ---------- const processOperation = operation => { debug: logger === null || logger === void 0 || logger.debug10("Processing operation", operation); for (const entry of allCallbacks.values()) { const categoryBitmask = entry._options._categoryBitmask; const target = entry._options._target; const selector = entry._options._selector; if (!(operation._categoryBitmask & categoryBitmask)) { debug: logger === null || logger === void 0 || logger.debug10(`Category does not match: ${categoryBitmask}`); continue; } const currentTargets = []; if (target) { if (!operation._target.contains(target)) { debug: logger === null || logger === void 0 || logger.debug10("Target does not match", target); continue; } currentTargets.push(target); } if (selector) { const matches = [...MH.querySelectorAll(operation._target, selector)]; if (operation._target.matches(selector)) { matches.push(operation._target); } if (!MH.lengthOf(matches)) { debug: logger === null || logger === void 0 || logger.debug10(`Selector does not match: ${selector}`); continue; } currentTargets.push(...matches); } invokeCallback(entry._callback, operation, currentTargets); } }; // ---------- const activateObserver = (root, mutationType) => { if (!observers[mutationType]._isActive) { debug: logger === null || logger === void 0 || logger.debug3(`Activating mutation observer for '${mutationType}'`); observers[mutationType]._observer.observe(root, { [mutationType]: true, subtree: config._subtree }); observers[mutationType]._isActive = true; } }; // ---------- const deactivateObserver = mutationType => { if (observers[mutationType]._isActive) { debug: logger === null || logger === void 0 || logger.debug3(`Disconnecting mutation observer for '${mutationType}'`); observers[mutationType]._observer.disconnect(); observers[mutationType]._isActive = false; } }; // ---------- const shouldSkipOperation = operation => { var _config$_root2; const target = operation._target; const requestToSkip = (0, _domAlter.getIgnoreMove)(target); if (!requestToSkip) { return false; } const removedFrom = operation._removedFrom; const addedTo = MH.parentOf(target); const requestFrom = requestToSkip.from; const requestTo = requestToSkip.to; const root = (_config$_root2 = config._root) !== null && _config$_root2 !== void 0 ? _config$_root2 : MH.getBody(); // If "from" is currently outside our root, we may not have seen a // removal operation. if ((removedFrom === requestFrom || !root.contains(requestFrom)) && addedTo === requestTo) { (0, _domAlter.clearIgnoreMove)(target); return true; } return false; }; // ---------- this.ignoreMove = _domAlter.ignoreMove; // ---------- this.onMutation = setupOnMutation; // ---------- this.offMutation = handler => { var _allCallbacks$get2; debug: logger === null || logger === void 0 || logger.debug5("Removing handler"); MH.remove((_allCallbacks$get2 = allCallbacks.get(handler)) === null || _allCallbacks$get2 === void 0 ? void 0 : _allCallbacks$get2._callback); }; } } /** * @interface */ /** * @interface */ /** * The handler is invoked with one argument: * * - a {@link MutationOperation} for a set of mutations related to a particular * element * * The handler could be invoked multiple times in each "round" (cycle of event * loop) if there are mutation operations for more than one element that match * the supplied {@link OnMutationOptions}. */ // ---------------------------------------- exports.DOMWatcher = DOMWatcher; const CONSTRUCTOR_KEY = MC.SYMBOL(); const instances = (0, _xMap.newXMap)(() => MH.newMap()); const getConfig = config => { var _config$root, _config$subtree; return { _root: (_config$root = config === null || config === void 0 ? void 0 : config.root) !== null && _config$root !== void 0 ? _config$root : null, _subtree: (_config$subtree = config === null || config === void 0 ? void 0 : config.subtree) !== null && _config$subtree !== void 0 ? _config$subtree : true }; }; const CATEGORIES_BITS = _dom.DOM_CATEGORIES_SPACE.bit; const ADDED_BIT = CATEGORIES_BITS[MC.S_ADDED]; const REMOVED_BIT = CATEGORIES_BITS[MC.S_REMOVED]; const ATTRIBUTE_BIT = CATEGORIES_BITS[MC.S_ATTRIBUTE]; // ---------------------------------------- const getOptions = options => { var _options$selector, _options$target; let categoryBitmask = 0; const categories = (0, _validation.validateStrList)("categories", options.categories, _dom.DOM_CATEGORIES_SPACE.has); if (categories) { for (const cat of categories) { categoryBitmask |= CATEGORIES_BITS[cat]; } } else { categoryBitmask = _dom.DOM_CATEGORIES_SPACE.bitmask; // default: all } const selector = (_options$selector = options.selector) !== null && _options$selector !== void 0 ? _options$selector : ""; if (!MH.isString(selector)) { throw MH.usageError("'selector' must be a string"); } return { _categoryBitmask: categoryBitmask, _target: (_options$target = options.target) !== null && _options$target !== void 0 ? _options$target : null, _selector: selector }; }; const getDiffOperation = (operationA, operationB) => { if (!operationB || operationA._target !== operationB._target) { return operationA; } const attributes = MH.newSet(); for (const attr of operationA._attributes) { if (!operationB._attributes.has(attr)) { attributes.add(attr); } } const categoryBitmask = operationA._categoryBitmask ^ operationB._categoryBitmask; const addedTo = operationA._addedTo === operationB._addedTo ? null : operationA._addedTo; const removedFrom = operationA._removedFrom === operationB._removedFrom ? null : operationA._removedFrom; if (!MH.sizeOf(attributes) && !categoryBitmask && !addedTo && !removedFrom) { return null; } return { _target: operationA._target, _categoryBitmask: categoryBitmask, _attributes: attributes, _addedTo: addedTo, _removedFrom: removedFrom }; }; const invokeCallback = (callback, operation, currentTargets = []) => { if (!MH.lengthOf(currentTargets)) { currentTargets = [operation._target]; } for (const currentTarget of currentTargets) { callback.invoke({ target: operation._target, currentTarget, attributes: operation._attributes, addedTo: operation._addedTo, removedFrom: operation._removedFrom }).catch(_log.logError); } }; //# sourceMappingURL=dom-watcher.cjs.map