/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/core/context/node.js
531 строка
14 KB
renovate[bot]
📦 Update linting devDependencies (#40105)
12 авг 2024, 22:25
Не верифицирован
12 авг 2024, 22:25
abbcd59
Код
Авторство
О чём код?
import {devAssert} from '#core/assert'; import * as mode from '#core/mode'; import {isElement} from '#core/types'; import {pushIfNotExist, removeItem} from '#core/types/array'; import {throttleTail} from './scheduler'; import {Values} from './values'; /** * @template T, DEP * @typedef {import('./types.d').IContextProp<T, DEP>} IContextProp */ // Properties set on the DOM nodes to track the context state. const NODE_PROP = '__AMP_NODE'; const ASSIGNED_SLOT_PROP = '__AMP_ASSIGNED_SLOT'; const AMP_PREFIX = 'AMP-'; // Relevant node types. // See https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType. const DOCUMENT_NODE = 9; // Includes shadow root, template, etc. const FRAGMENT_NODE = 11; /** * The structure for a group of nodes. * * @typedef {{ * cn: ContextNode<?>, * match: function(Node, Node):boolean, * weight: number, * }} GroupDef */ /** * The context node is a sparse tree over the DOM tree. Any node that needs * to manage and compute state can be attached to the context node tree. The * tree (mostly) automatically self-manages: the new nodes and DOM mutations * are auto-discovered or prompted. * * @package * @template SID subscriber ID type(s) */ export class ContextNode { /** * Returns the existing context node or creates a new one. * * @param {Node} node * @return {ContextNode<?>} */ static get(node) { let contextNode = /** @type {ContextNode<?>|undefined} */ (node[NODE_PROP]); if (!contextNode) { contextNode = new ContextNode(node, null); if (mode.isLocalDev() || mode.isTest()) { // The `Object.defineProperty({enumerable: false})` helps tests, but // hurts performance. So this is only done in a dev/test modes. Object.defineProperty(node, NODE_PROP, { value: contextNode, writable: false, enumerable: false, configurable: false, }); } else { node[NODE_PROP] = contextNode; } } return contextNode; } /** * Returns the closest available context node to the one specified. If the * `node` has the context node, it's returned unless `includeSelf` is set as * `false`. * * The DOM traversal goes at most as far as the root node (document, * shadow root, document fragment) or as far as the DOM tree allows. The * traversal follows the assigned shadow slots. * * Root nodes (document or shadow root) and AMP elements are auto-created * during the traversal. * * @param {Node} node The node from which to perform the search. * @param {boolean=} includeSelf Whether the specified node itself should * be included in the search. Defaults to `true`. * @return {?ContextNode<?>} */ static closest(node, includeSelf = true) { /** @type {?Node} */ let n = node; while (n) { // Check if a node is a candidate to be returned. if (n != node || includeSelf) { if (n[NODE_PROP]) { // Already a discovered node. return /** @type {ContextNode<?>} */ (n[NODE_PROP]); } const {nodeType} = n; if ( // A context node is always created for a root. Due to this, a // non-root element is always at least attached to a root. This // allows for quick discovery and reattachment when new information // becomes available. nodeType == DOCUMENT_NODE || nodeType == FRAGMENT_NODE || // An AMP node will always have a context node backing it at some // point. (isElement(n) && n.tagName.startsWith(AMP_PREFIX)) ) { return ContextNode.get(n); } } // Navigate up the DOM tree. Notice that we do not automatically go over // a root node boundary. /** @type {Node|Element|undefined|null} */ const assignedSlot = /** @type {?Node|undefined} */ (n[ASSIGNED_SLOT_PROP]) || /** @type {Element} */ (n).assignedSlot; if (assignedSlot) { n = assignedSlot; } else { n = /** @type {?Node} */ (n.parentNode); } } // Only disconnected nodes will return `null` here. return null; } /** * Direct slot assignment. Works the same way as shadow slots, but does not * require a shadow root. Automatically starts the discovery phase for the * affected nodes. * * See `Element.assignedSlot` API. * * @param {Node} node The target node. * @param {Node} slot The slot to which the target node is assigned. */ static assignSlot(node, slot) { if (node[ASSIGNED_SLOT_PROP] == slot) { return; } node[ASSIGNED_SLOT_PROP] = slot; discoverContained(node); } /** * Unassigns the direct slot previously done by the `assignSlot` call. * Automatically starts the discovery phase for the affected nodes. * * @param {Node} node The target node. * @param {Node} slot The slot from which the target node is assigned. */ static unassignSlot(node, slot) { if (node[ASSIGNED_SLOT_PROP] != slot) { return; } node[ASSIGNED_SLOT_PROP] = undefined; discoverContained(node); } /** * Reruns discovery on the children of the specified node, if any. * * @param {Node} node */ static rediscoverChildren(node) { const contextNode = /** @type {ContextNode<?>|undefined} */ ( node[NODE_PROP] ); contextNode?.children?.forEach(discoverContextNode); } /** * Creates the context node and automatically starts the discovery process. * * @param {Node} node * @param {?string} name */ constructor(node, name) { /** * @const * @type {Node} */ this.node = node; /** * @const * @package * @type {?string} */ this.name = name; /** * Whether this node is a root. The Document DOM nodes are automatically * considered as roots. But other nodes can become roots as well * (e.g. shadow roots) via `setIsRoot()` API. * * @package * @type {boolean} */ this.isRoot = node.nodeType == DOCUMENT_NODE; /** * The root context node. Always available for a DOM node connected to a * root node after the discovery phase. * * @package * @type {?ContextNode<?>} */ this.root = this.isRoot ? this : null; /** * Parent should be mostly meaningless to most API clients, because * it's an async concept: a parent context node can can be instantiated at * any time and it doesn't mean that this node has to change. This is * why the API is declared as package-private. However, it needs to be * unobfuscated to avoid cross-binary issues. * * @package * @type {?ContextNode<?>} */ this.parent = null; /** * See `parent` description. * * @package * @type {?ContextNode<?>[]} */ this.children = null; /** * @package * @type {?Map<string, GroupDef>} */ this.groups = null; /** * @package * @type {Values} */ this.values = new Values(this); /** * @private * @type {?Map<SID, import('./subscriber').Subscriber<?>>} */ this.subscribers_ = null; /** * @private * @type {boolean} */ this.parentOverridden_ = false; /** * @const * @private * @type {function():void} */ this.scheduleDiscover_ = throttleTail( this.discover_.bind(this), setTimeout ); // Shadow root: track slot changes. if (node.nodeType == FRAGMENT_NODE) { node.addEventListener('slotchange', (e) => { const slot = /** @type {HTMLSlotElement} */ (e.target); // Rediscover newly assigned nodes. slot.assignedNodes().forEach(discoverContained); // Rediscover unassigned nodes. ContextNode.closest(slot)?.children?.forEach(discoverContextNode); }); } this.discover(); } /** * Requests the discovery phase. Asynchronously finds the nearest parent for * this node and its root. Roots and parents set directly via `setParent()` * API are not discoverable. */ discover() { if (this.isDiscoverable()) { this.scheduleDiscover_(); } else if (this.name && this.children) { // Recursively discover the group's children. this.children.forEach(discoverContextNode); } } /** * @return {boolean} * @protected Used cross-binary. */ isDiscoverable() { return !this.isRoot && !this.parentOverridden_; } /** * Sets (or unsets) the direct parent. If the parent is set, the node will no * longer try to discover itself. * * @param {?(ContextNode<?>|Node)} parent */ setParent(parent) { const parentContext = /** @type {*} */ (parent)?.nodeType ? ContextNode.get(/** @type {Node} */ (parent)) : /** @type {?ContextNode<?>} */ (parent); this.updateTree_(parentContext, /* parentOverridden */ parent != null); } /** * Designates (or undesignates) the node as a root node. If the node is * designated as a root, it will no longer discover itself. * * @param {boolean} isRoot */ setIsRoot(isRoot) { this.isRoot = isRoot; const newRoot = isRoot ? this : (this.parent?.root ?? null); this.updateRoot(newRoot); } /** * @param {?ContextNode<?>} root * @protected Used cross-binary. */ updateRoot(root) { devAssert(!root || root.isRoot); const oldRoot = this.root; if (root != oldRoot) { // The root has changed. this.root = root; // Make sure the tree changes have been reflected for values. this.values.rootUpdated(); // Make sure the tree changes have been reflected for subscribers. this.subscribers_?.forEach((comp) => comp.rootUpdated()); // Propagate the root to the subtree. this.children?.forEach((child) => child.updateRoot(root)); } } /** * @param {string} name * @param {function(Node):boolean} match * @param {number} weight * @return {ContextNode<?>} */ addGroup(name, match, weight) { const groups = this.groups || (this.groups = new Map()); const {children, node} = this; const cn = new ContextNode(node, name); groups.set(name, {cn, match, weight}); cn.setParent(this); children?.forEach(discoverContextNode); return cn; } /** * @param {string} name * @return {?ContextNode<?>} */ group(name) { return this.groups?.get(name)?.cn || null; } /** * @param {Node} node * @return {?ContextNode<?>} * @protected */ findGroup(node) { const {groups} = this; if (!groups) { return null; } let found = null; let maxWeight = Number.NEGATIVE_INFINITY; groups.forEach(({cn, match, weight}) => { if (match(node, this.node) && weight > maxWeight) { found = cn; maxWeight = weight; } }); return found; } /** * Add or update a subscriber with a specified ID. If subscriber doesn't * yet exist, it will be created using the specified factory. The use * of factory is important to reduce bundling costs for context node. * If `func` returns a function, that function is called on cleanup. * * @param {SID} id * @param {typeof import('./subscriber').Subscriber} Ctor * @param {import('./subscriber').SubscribeCallback<DEP>} func * @param {IContextProp<DEP, ?>[]} deps * @template DEP */ subscribe(id, Ctor, func, deps) { const subscribers = this.subscribers_ || (this.subscribers_ = new Map()); let subscriber = subscribers.get(id); if (!subscriber) { subscriber = new Ctor( /** @type {ContextNode<function(...DEP):void>} */ ( /** @type {?} */ (this) ), func, deps ); subscribers.set(id, subscriber); } } /** * Removes the subscriber previously set with `subscribe`. * * @param {SID} id */ unsubscribe(id) { const subscribers = this.subscribers_; const subscriber = subscribers?.get(id); if (subscriber) { subscriber.dispose(); devAssert(subscribers); subscribers.delete(id); } } /** * Discovers the parent and the root. Runs asynchronously via scheduler. * @private */ discover_() { if (!this.isDiscoverable()) { // The discoverability might have changed while this task was in the // queue. return; } const closestNode = ContextNode.closest(this.node, /* includeSelf */ false); const parent = closestNode?.findGroup(this.node) || closestNode; this.updateTree_(parent, /* parentOverridden */ false); } /** * @param {?ContextNode<?>} parent * @param {boolean} parentOverridden * @private */ updateTree_(parent, parentOverridden) { this.parentOverridden_ = parentOverridden; const oldParent = this.parent; if (parent != oldParent) { // The parent has changed. this.parent = parent; // Remove from the old parent. if (oldParent?.children) { devAssert(oldParent.children); removeItem(oldParent.children, this); } // Add to the new parent. if (parent) { const parentChildren = parent.children || (parent.children = []); pushIfNotExist(parentChildren, this); // Check if this node has been inserted in between the parent and // it's other children. // Since the new parent (`this`) is already known, this is a very // fast operation. for (const child of parentChildren) { if (child != this && child.isDiscoverable()) { child.discover(); } } } this.values.parentUpdated(); } // Check the root. this.updateRoot(parent?.root ?? null); } } /** * Iterates over all context nodes that are contained within the specified * `node`. Only iterates over known context nodes. * * @param {Node} node * @param {function(ContextNode<?>):void} callback * @param {boolean=} includeSelf */ function forEachContained(node, callback, includeSelf = true) { const closest = ContextNode.closest(node, includeSelf); if (!closest) { return; } if (closest.node == node) { callback(closest); } else if (closest.children) { for (const child of closest.children) { if (node.contains(child.node)) { callback(child); } } } } /** * @param {Node} node */ function discoverContained(node) { forEachContained(node, discoverContextNode); } /** * @param {ContextNode<?>} cn */ function discoverContextNode(cn) { cn.discover(); }