Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 86x 86x 86x 86x 1x 1x 85x 86x 86x 86x 86x 86x 86x 86x 86x 505x 505x 505x 505x 505x 505x 505x 86x 86x 258x 79x 79x 179x 179x 7x 86x 505x 505x 505x 505x 505x 505x 505x 505x 87x 1x 1x 86x 86x 86x 86x 87x 87x 86x 86x 86x 86x 86x 86x 86x 86x 86x 87x 86x 86x 6x 5x 5x 5x 6x 86x 87x 86x 86x 86x 87x 79x 79x 79x 86x 87x 505x 505x 505x 505x 505x 505x 505x 505x 31x 1x 1x 31x 31x 30x 30x 31x 31x 31x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 505x 505x 505x 505x 505x 505x 505x 78x 78x 78x 78x 505x 505x 505x 505x 505x 505x 505x 23x 1x 1x 23x 23x 23x | /**
* @file dom.js
* @description Stable, replayable descriptions of DOM nodes.
*
* A trace refers to elements in two very different situations: when a human
* reads `avenx trace view` and wants to recognise the button they clicked, and
* when replay has to find that same button again in a freshly mounted
* application. The second requirement is the strict one — a description that
* reads well but cannot be resolved makes a trace unreplayable.
*
* So every reference carries both a selector and the node's index among that
* selector's matches. Replay can then resolve `button.qty-inc` unambiguously
* even when the page has twelve of them, without depending on ids the
* application does not have.
* @module lib/core/trace/dom
*/
import { tracer } from './tracer.js';
import { TraceNodeType } from './schema.js';
/**
* How many class names are folded into a selector.
*
* Scoped-CSS class hashes make Avenx elements class-heavy; taking every class
* would produce selectors that are long to read and brittle against an
* unrelated style change.
* @type {number}
*/
const MAX_SELECTOR_CLASSES = 2;
/**
* Attributes Avenx adds for its own bookkeeping, which must never end up in a
* selector: they are re-derived on every render and would not survive replay.
* @type {RegExp}
*/
const INTERNAL_ATTR = /^(data-ax-|data-avenx-)/;
/**
* How much of a changed attribute or text value a trace keeps.
* @type {number}
*/
const MAX_DOM_VALUE = 120;
/**
* Builds a CSS selector for an element.
* @param {Element} el - The element.
* @returns {string} A selector, or the tag name when nothing else is available.
*/
function selectorFor(el) {
const tag = (el.tagName || 'unknown').toLowerCase();
if (el.id && !INTERNAL_ATTR.test(el.id)) {
return `#${el.id}`;
}
const className = typeof el.className === 'string' ? el.className : '';
const classes = className
.split(/\s+/)
.filter((name) => name && !INTERNAL_ATTR.test(name))
.slice(0, MAX_SELECTOR_CLASSES);
return classes.length > 0 ? `${tag}.${classes.join('.')}` : tag;
}
/**
* Finds the Avenx component instance an element belongs to.
* @param {Node|null} node - The starting node.
* @returns {object|null} The owning component instance, if any.
*/
export function ownerComponent(node) {
let current = node;
while (current) {
if (current.__avenx_comp_instance) {
return current.__avenx_comp_instance;
}
current = current.parentNode;
}
return null;
}
/**
* Describes a DOM node well enough to display it and to find it again.
* @param {Node|null} node - The node to describe.
* @returns {{selector: string, nth: number, component: string, uid: number}|null}
* A reference, or null when there is no element to describe.
*/
export function describeNode(node) {
if (!node) {
return null;
}
// Text nodes are addressed through the element that contains them: a text
// node has no selector of its own, and the containing element is what a
// reader recognises anyway.
const el = node.nodeType === 3 ? node.parentElement : node;
if (!el || el.nodeType !== 1) {
return null;
}
const selector = selectorFor(el);
let nth = 0;
// The index disambiguates the selector. A missing document (a detached
// subtree, a headless mount) simply yields index 0, which resolve() below
// treats as "the first match".
try {
const root = el.ownerDocument;
if (root && typeof root.querySelectorAll === 'function') {
const matches = root.querySelectorAll(selector);
for (let i = 0; i < matches.length; i++) {
if (matches[i] === el) {
nth = i;
break;
}
}
}
} catch {
// An exotic selector (an id starting with a digit, for instance) can make
// querySelectorAll throw. Falling back to index 0 keeps the description
// usable for display even when it is not precise enough to replay.
}
const ref = { selector, nth };
const owner = ownerComponent(el);
if (owner) {
ref.component = owner.constructor && owner.constructor.name;
ref.uid = owner.uid;
}
return ref;
}
/**
* Resolves a reference produced by {@link describeNode} back to an element.
* @param {object|null} ref - The reference.
* @param {Document|Element} [root] - Where to search. Defaults to the document.
* @returns {Element|null} The element, or null when it cannot be found.
*/
export function resolveNode(ref, root) {
if (!ref || !ref.selector) {
return null;
}
const scope = root || (typeof document !== 'undefined' ? document : null);
if (!scope || typeof scope.querySelectorAll !== 'function') {
return null;
}
try {
const matches = scope.querySelectorAll(ref.selector);
return matches[ref.nth || 0] || null;
} catch {
return null;
}
}
/**
* Records a DOM mutation the patcher applied, attributed to whatever caused it.
*
* Called from the existing patch operations rather than from a second diffing
* pass or a MutationObserver: the point of the trace is to say which *state
* change* produced a DOM change, and only the patcher knows both halves.
*
* Values are truncated. A trace records that `.total` went from `"$24.00"` to
* `"$36.00"`, not the innerHTML of the subtree around it.
* @param {string} op - The operation: `text`, `attr`, `remove-attr`, `insert`, `remove`, `replace`.
* @param {Node} node - The node that changed, or its parent for structural ops.
* @param {object} [fields] - Operation-specific detail (`name`, `from`, `to`).
*/
export function traceDomOp(op, node, fields = {}) {
if (!tracer.sink) {
return;
}
const ref = describeNode(node);
if (!ref) {
return;
}
tracer.record(TraceNodeType.DOM, {
op,
target: { selector: ref.selector, nth: ref.nth },
component: ref.component,
...fields,
});
}
/**
* Clamps a DOM value so a trace records the change rather than the document.
* @param {any} value - The raw attribute or text value.
* @returns {string|null} A bounded string, or null for an absent value.
*/
export function clampDomValue(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value);
return text.length > MAX_DOM_VALUE ? `${text.slice(0, MAX_DOM_VALUE)}…` : text;
}
/**
* Renders a node reference for display in `avenx trace view`.
* @param {object|null} ref - The reference.
* @returns {string} A short human-readable form.
*/
export function formatNodeRef(ref) {
if (!ref || !ref.selector) {
return '<unknown>';
}
const suffix = ref.nth > 0 ? `[${ref.nth}]` : '';
return `<${ref.selector}${suffix}>`;
}
|