Socket
Socket
Sign inDemoInstall

@thi.ng/hdom

Package Overview
Dependencies
Maintainers
1
Versions
273
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@thi.ng/hdom - npm Package Compare versions

Comparing version 9.3.31 to 9.4.0

8

CHANGELOG.md
# Change Log
- **Last updated**: 2023-12-09T19:12:03Z
- **Last updated**: 2023-12-11T10:07:09Z
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)

@@ -12,2 +12,8 @@

## [9.4.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/hdom@9.4.0) (2023-12-11)
#### 🚀 Features
- update setAttrib(), more alignment w/ rdom logic ([639ca71](https://github.com/thi-ng/umbrella/commit/639ca71))
### [9.3.27](https://github.com/thi-ng/umbrella/tree/@thi.ng/hdom@9.3.27) (2023-11-09)

@@ -14,0 +20,0 @@

67

default.js
import { diffTree } from "./diff.js";
import { createElement, createTextElement, createTree, getChild, hydrateTree, removeAttribs, removeChild, replaceChild, setAttrib, setContent, } from "./dom.js";
import {
createElement,
createTextElement,
createTree,
getChild,
hydrateTree,
removeAttribs,
removeChild,
replaceChild,
setAttrib,
setContent
} from "./dom.js";
import { normalizeTree } from "./normalize.js";
/**
* Default target implementation to manipulate browser DOM.
*/
export const DEFAULT_IMPL = {
createTree(opts, parent, tree, child, init) {
return createTree(opts, this, parent, tree, child, init);
},
hydrateTree(opts, parent, tree, child) {
return hydrateTree(opts, this, parent, tree, child);
},
diffTree(opts, parent, prev, curr, child) {
diffTree(opts, this, parent, prev, curr, child);
},
normalizeTree,
getElementById(id) {
return document.getElementById(id);
},
getChild,
createElement,
createTextElement,
replaceChild(opts, parent, child, tree, init) {
replaceChild(opts, this, parent, child, tree, init);
},
removeChild,
setContent,
removeAttribs,
setAttrib,
const DEFAULT_IMPL = {
createTree(opts, parent, tree, child, init) {
return createTree(opts, this, parent, tree, child, init);
},
hydrateTree(opts, parent, tree, child) {
return hydrateTree(opts, this, parent, tree, child);
},
diffTree(opts, parent, prev, curr, child) {
diffTree(opts, this, parent, prev, curr, child);
},
normalizeTree,
getElementById(id) {
return document.getElementById(id);
},
getChild,
createElement,
createTextElement,
replaceChild(opts, parent, child, tree, init) {
replaceChild(opts, this, parent, child, tree, init);
},
removeChild,
setContent,
removeAttribs,
setAttrib
};
export {
DEFAULT_IMPL
};
import { SEMAPHORE } from "@thi.ng/api/api";
import { diffArray } from "@thi.ng/diff/array";
import { diffObject } from "@thi.ng/diff/object";
import { equiv as _equiv, equivArrayLike, equivMap, equivObject, equivSet, } from "@thi.ng/equiv";
import {
equiv as _equiv,
equivArrayLike,
equivMap,
equivObject,
equivSet
} from "@thi.ng/equiv";
const isArray = Array.isArray;

@@ -10,277 +16,234 @@ const max = Math.max;

const STR = "string";
// child index tracking template buffer
const INDEX = (() => {
const res = new Array(2048);
for (let i = 2, n = res.length; i < n; i++) {
res[i] = i - 2;
}
return res;
const res = new Array(2048);
for (let i = 2, n = res.length; i < n; i++) {
res[i] = i - 2;
}
return res;
})();
const buildIndex = (n) => {
if (n <= INDEX.length) {
return INDEX.slice(0, n);
}
const res = new Array(n);
while (n-- > 2) {
res[n] = n - 2;
}
return res;
if (n <= INDEX.length) {
return INDEX.slice(0, n);
}
const res = new Array(n);
while (n-- > 2) {
res[n] = n - 2;
}
return res;
};
/**
* See {@link HDOMImplementation} interface for further details.
*
* @param opts - hdom config options
* @param impl - hdom implementation
* @param parent - parent element (DOM node)
* @param prev - previous tree
* @param curr - current tree
* @param child - child index
*/
export const diffTree = (opts, impl, parent, prev, curr, child = 0) => {
const attribs = curr[1];
if (attribs.__skip) {
return;
const diffTree = (opts, impl, parent, prev, curr, child = 0) => {
const attribs = curr[1];
if (attribs.__skip) {
return;
}
if (attribs.__diff === false) {
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
const pattribs = prev[1];
if (pattribs && pattribs.__skip) {
impl.replaceChild(opts, parent, child, curr, false);
return;
}
let _impl = attribs.__impl;
if (_impl && _impl !== impl) {
return _impl.diffTree(opts, _impl, parent, prev, curr, child);
}
const delta = diffArray(prev, curr, "only-distance-linear", equiv);
if (delta.distance === 0) {
return;
}
const edits = delta.linear;
const el = impl.getChild(parent, child);
let i;
let ii;
let status;
let val;
if (edits[0] !== 0 || prev[1].key !== attribs.key) {
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
if ((val = prev.__release) && val !== curr.__release) {
releaseTree(prev);
}
if (edits[3] !== 0) {
diffAttributes(impl, el, prev[1], curr[1]);
if (delta.distance === 2) {
return;
}
// always replace element if __diff = false
if (attribs.__diff === false) {
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
const numEdits = edits.length;
const prevLength = prev.length - 1;
const equivKeys = extractEquivElements(edits);
const offsets = buildIndex(prevLength + 1);
for (i = 2, ii = 6; ii < numEdits; i++, ii += 3) {
status = edits[ii];
if (!status)
continue;
if (status === -1) {
diffDeleted(
opts,
impl,
el,
prev,
curr,
edits,
ii,
equivKeys,
offsets,
prevLength
);
} else {
diffAdded(
opts,
impl,
el,
edits,
ii,
equivKeys,
offsets,
prevLength
);
}
const pattribs = prev[1];
if (pattribs && pattribs.__skip) {
impl.replaceChild(opts, parent, child, curr, false);
return;
}
// delegate to branch-local implementation
let _impl = attribs.__impl;
if (_impl && _impl !== impl) {
return _impl.diffTree(opts, _impl, parent, prev, curr, child);
}
const delta = diffArray(prev, curr, "only-distance-linear", equiv);
if (delta.distance === 0) {
return;
}
const edits = delta.linear;
const el = impl.getChild(parent, child);
let i;
let ii;
let status;
let val;
if (edits[0] !== 0 || prev[1].key !== attribs.key) {
// LOGGER.fine("replace:", prev, curr);
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
if ((val = prev.__release) && val !== curr.__release) {
releaseTree(prev);
}
if (edits[3] !== 0) {
diffAttributes(impl, el, prev[1], curr[1]);
// if attribs changed & distance == 2 then we're done here...
if (delta.distance === 2) {
return;
}
}
const numEdits = edits.length;
const prevLength = prev.length - 1;
const equivKeys = extractEquivElements(edits);
const offsets = buildIndex(prevLength + 1);
for (i = 2, ii = 6; ii < numEdits; i++, ii += 3) {
status = edits[ii];
if (!status)
continue;
if (status === -1) {
diffDeleted(opts, impl, el, prev, curr, edits, ii, equivKeys, offsets, prevLength);
}
else {
diffAdded(opts, impl, el, edits, ii, equivKeys, offsets, prevLength);
}
}
// call __init after all children have been added/updated
if ((val = curr.__init) && val != prev.__init) {
val.apply(curr, [el, ...curr.__args]);
}
}
if ((val = curr.__init) && val != prev.__init) {
val.apply(curr, [el, ...curr.__args]);
}
};
const diffDeleted = (opts, impl, el, prev, curr, edits, ii, equivKeys, offsets, prevLength) => {
const val = edits[ii + 2];
if (isArray(val)) {
let k = val[1].key;
if (k !== undefined && equivKeys[k][2] !== undefined) {
const eq = equivKeys[k];
k = eq[0];
// LOGGER.fine(`diff equiv key @ ${k}:`, prev[k], curr[eq[2]]);
diffTree(opts, impl, el, prev[k], curr[eq[2]], offsets[k]);
}
else {
const idx = edits[ii + 1];
// LOGGER.fine("remove @", offsets[idx], val);
releaseTree(val);
impl.removeChild(el, offsets[idx]);
incOffsets(offsets, prevLength, idx);
}
const val = edits[ii + 2];
if (isArray(val)) {
let k = val[1].key;
if (k !== void 0 && equivKeys[k][2] !== void 0) {
const eq = equivKeys[k];
k = eq[0];
diffTree(opts, impl, el, prev[k], curr[eq[2]], offsets[k]);
} else {
const idx = edits[ii + 1];
releaseTree(val);
impl.removeChild(el, offsets[idx]);
incOffsets(offsets, prevLength, idx);
}
else if (typeof val === STR) {
impl.setContent(el, "");
}
} else if (typeof val === STR) {
impl.setContent(el, "");
}
};
const diffAdded = (opts, impl, el, edits, ii, equivKeys, offsets, prevLength) => {
const val = edits[ii + 2];
if (typeof val === STR) {
impl.setContent(el, val);
const val = edits[ii + 2];
if (typeof val === STR) {
impl.setContent(el, val);
} else if (isArray(val)) {
const k = val[1].key;
if (k === void 0 || equivKeys[k][0] === void 0) {
const idx = edits[ii + 1];
impl.createTree(opts, el, val, offsets[idx]);
decOffsets(offsets, prevLength, idx);
}
else if (isArray(val)) {
const k = val[1].key;
if (k === undefined || equivKeys[k][0] === undefined) {
const idx = edits[ii + 1];
// LOGGER.fine("insert @", offsets[idx], val);
impl.createTree(opts, el, val, offsets[idx]);
decOffsets(offsets, prevLength, idx);
}
}
}
};
const incOffsets = (offsets, j, idx) => {
for (; j > idx; j--) {
offsets[j] = max(offsets[j] - 1, 0);
}
for (; j > idx; j--) {
offsets[j] = max(offsets[j] - 1, 0);
}
};
const decOffsets = (offsets, j, idx) => {
for (; j >= idx; j--) {
offsets[j]++;
}
for (; j >= idx; j--) {
offsets[j]++;
}
};
/**
* Helper function for {@link diffTree} to compute & apply the difference
* between a node's `prev` and `curr` attributes.
*
* @param impl - hdom implementation
* @param el - DOM element
* @param prev - previous attributes
* @param curr - current attributes
*
* @internal
*/
export const diffAttributes = (impl, el, prev, curr) => {
const delta = diffObject(prev, curr, "full", _equiv);
impl.removeAttribs(el, delta.dels, prev);
let val = SEMAPHORE;
let i, e, edits;
for (edits = delta.edits, i = edits.length; (i -= 2) >= 0;) {
e = edits[i];
e[0] === "o" && e[1] === "n" && impl.removeAttribs(el, [e], prev);
e !== "value"
? impl.setAttrib(el, e, edits[i + 1], curr)
: (val = edits[i + 1]);
const diffAttributes = (impl, el, prev, curr) => {
const delta = diffObject(prev, curr, "full", _equiv);
impl.removeAttribs(el, delta.dels, prev);
let val = SEMAPHORE;
let i, e, edits;
for (edits = delta.edits, i = edits.length; (i -= 2) >= 0; ) {
e = edits[i];
e[0] === "o" && e[1] === "n" && impl.removeAttribs(el, [e], prev);
e !== "value" ? impl.setAttrib(el, e, edits[i + 1], curr) : val = edits[i + 1];
}
for (edits = delta.adds, i = edits.length; i-- > 0; ) {
e = edits[i];
e !== "value" ? impl.setAttrib(el, e, curr[e], curr) : val = curr[e];
}
val !== SEMAPHORE && impl.setAttrib(el, "value", val, curr);
};
const releaseTree = (tree) => {
if (isArray(tree)) {
let x;
if ((x = tree[1]) && x.__release === false) {
return;
}
for (edits = delta.adds, i = edits.length; i-- > 0;) {
e = edits[i];
e !== "value" ? impl.setAttrib(el, e, curr[e], curr) : (val = curr[e]);
if (tree.__release) {
tree.__release.apply(tree.__this, tree.__args);
delete tree.__release;
}
val !== SEMAPHORE && impl.setAttrib(el, "value", val, curr);
};
/**
* Recursively attempts to call the {@link ILifecycle.release} lifecycle
* method on every element in given tree (branch), using depth-first
* descent. Each element is checked for the presence of the `__release`
* control attribute. If (and only if) it is set to `false`, further
* descent into that element's branch is skipped.
*
* @param tree - hdom sub-tree
*
* @internal
*/
export const releaseTree = (tree) => {
if (isArray(tree)) {
let x;
if ((x = tree[1]) && x.__release === false) {
return;
}
if (tree.__release) {
// LOGGER.fine("call __release", tag);
tree.__release.apply(tree.__this, tree.__args);
delete tree.__release;
}
for (x = tree.length; x-- > 2;) {
releaseTree(tree[x]);
}
for (x = tree.length; x-- > 2; ) {
releaseTree(tree[x]);
}
}
};
const extractEquivElements = (edits) => {
let k;
let val;
let ek;
const equiv = {};
for (let i = edits.length; (i -= 3) >= 0;) {
val = edits[i + 2];
if (isArray(val) && (k = val[1].key) !== undefined) {
ek = equiv[k];
!ek && (equiv[k] = ek = [, ,]);
ek[edits[i] + 1] = edits[i + 1];
}
let k;
let val;
let ek;
const equiv2 = {};
for (let i = edits.length; (i -= 3) >= 0; ) {
val = edits[i + 2];
if (isArray(val) && (k = val[1].key) !== void 0) {
ek = equiv2[k];
!ek && (equiv2[k] = ek = [, ,]);
ek[edits[i] + 1] = edits[i + 1];
}
return equiv;
}
return equiv2;
};
/**
* Customized version
* [`equiv()`](https://docs.thi.ng/umbrella/equiv/functions/equiv.html) which
* takes `__diff` attributes into account (at any nesting level). If an hdom
* element's attribute object contains `__diff: false`, the object will ALWAYS
* be considered unequal, even if all other attributes in the object are
* equivalent.
*
* @param a -
* @param b -
*
* @internal
*/
export const equiv = (a, b) => {
let proto;
if (a === b) {
return true;
const equiv = (a, b) => {
let proto;
if (a === b) {
return true;
}
if (a != null) {
if (typeof a.equiv === FN) {
return a.equiv(b);
}
if (a != null) {
if (typeof a.equiv === FN) {
return a.equiv(b);
}
} else {
return a == b;
}
if (b != null) {
if (typeof b.equiv === FN) {
return b.equiv(a);
}
else {
return a == b;
}
if (b != null) {
if (typeof b.equiv === FN) {
return b.equiv(a);
}
}
else {
return a == b;
}
if (typeof a === STR || typeof b === STR) {
return false;
}
if (((proto = Object.getPrototypeOf(a)), proto == null || proto === OBJP) &&
((proto = Object.getPrototypeOf(b)), proto == null || proto === OBJP)) {
return (!(a.__diff === false || b.__diff === false) &&
equivObject(a, b, equiv));
}
if (typeof a !== FN &&
a.length !== undefined &&
typeof b !== FN &&
b.length !== undefined) {
return equivArrayLike(a, b, equiv);
}
if (a instanceof Set && b instanceof Set) {
return equivSet(a, b, equiv);
}
if (a instanceof Map && b instanceof Map) {
return equivMap(a, b, equiv);
}
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.toString() === b.toString();
}
// NaN
return a !== a && b !== b;
} else {
return a == b;
}
if (typeof a === STR || typeof b === STR) {
return false;
}
if ((proto = Object.getPrototypeOf(a), proto == null || proto === OBJP) && (proto = Object.getPrototypeOf(b), proto == null || proto === OBJP)) {
return !(a.__diff === false || b.__diff === false) && equivObject(a, b, equiv);
}
if (typeof a !== FN && a.length !== void 0 && typeof b !== FN && b.length !== void 0) {
return equivArrayLike(a, b, equiv);
}
if (a instanceof Set && b instanceof Set) {
return equivSet(a, b, equiv);
}
if (a instanceof Map && b instanceof Map) {
return equivMap(a, b, equiv);
}
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.toString() === b.toString();
}
return a !== a && b !== b;
};
export {
diffAttributes,
diffTree,
equiv,
releaseTree
};
import { implementsFunction } from "@thi.ng/checks/implements-function";
import { isArray as isa } from "@thi.ng/checks/is-array";
import { isNotStringAndIterable as isi } from "@thi.ng/checks/is-not-string-iterable";
import { isString as iss } from "@thi.ng/checks/is-string";
import { SVG_TAGS } from "@thi.ng/hiccup/api";
import { isArray } from "@thi.ng/checks/is-array";
import { isNotStringAndIterable } from "@thi.ng/checks/is-not-string-iterable";
import { isString } from "@thi.ng/checks/is-string";
import { ATTRIB_JOIN_DELIMS, SVG_TAGS } from "@thi.ng/hiccup/api";
import { css } from "@thi.ng/hiccup/css";
import { formatPrefixes } from "@thi.ng/hiccup/prefix";
import { XML_SVG } from "@thi.ng/prefixes/xml";
const isArray = isa;
const isNotStringAndIterable = isi;
const isString = iss;
const maybeInitElement = (el, tree) => tree.__init && tree.__init.apply(tree.__this, [el, ...tree.__args]);
/**
* See {@link HDOMImplementation} interface for further details.
*
* @param opts - hdom config options
* @param parent - DOM element
* @param tree - component tree
* @param insert - child index
*/
export const createTree = (opts, impl, parent, tree, insert, init = true) => {
if (isArray(tree)) {
const tag = tree[0];
if (typeof tag === "function") {
return createTree(opts, impl, parent, tag.apply(null, [opts.ctx, ...tree.slice(1)]), insert);
}
const attribs = tree[1];
if (attribs.__impl) {
return attribs.__impl.createTree(opts, parent, tree, insert, init);
}
const el = impl.createElement(parent, tag, attribs, insert);
if (tree.length > 2) {
const n = tree.length;
for (let i = 2; i < n; i++) {
createTree(opts, impl, el, tree[i], undefined, init);
}
}
init && maybeInitElement(el, tree);
return el;
const createTree = (opts, impl, parent, tree, insert, init = true) => {
if (isArray(tree)) {
const tag = tree[0];
if (typeof tag === "function") {
return createTree(
opts,
impl,
parent,
tag.apply(null, [opts.ctx, ...tree.slice(1)]),
insert
);
}
if (isNotStringAndIterable(tree)) {
const res = [];
for (let t of tree) {
res.push(createTree(opts, impl, parent, t, insert, init));
}
return res;
const attribs = tree[1];
if (attribs.__impl) {
return attribs.__impl.createTree(
opts,
parent,
tree,
insert,
init
);
}
if (tree == null) {
return parent;
const el = impl.createElement(parent, tag, attribs, insert);
if (tree.length > 2) {
const n = tree.length;
for (let i = 2; i < n; i++) {
createTree(opts, impl, el, tree[i], void 0, init);
}
}
return impl.createTextElement(parent, tree);
init && maybeInitElement(el, tree);
return el;
}
if (isNotStringAndIterable(tree)) {
const res = [];
for (let t of tree) {
res.push(createTree(opts, impl, parent, t, insert, init));
}
return res;
}
if (tree == null) {
return parent;
}
return impl.createTextElement(parent, tree);
};
/**
* See {@link HDOMImplementation} interface for further details.
*
* @param opts - hdom config options
* @param parent - DOM element
* @param tree - component tree
* @param index - child index
*/
export const hydrateTree = (opts, impl, parent, tree, index = 0) => {
if (isArray(tree)) {
const el = impl.getChild(parent, index);
if (typeof tree[0] === "function") {
hydrateTree(opts, impl, parent, tree[0].apply(null, [opts.ctx, ...tree.slice(1)]), index);
}
const attribs = tree[1];
if (attribs.__impl) {
return attribs.__impl.hydrateTree(opts, parent, tree, index);
}
maybeInitElement(el, tree);
for (let a in attribs) {
a[0] === "o" && a[1] === "n" && impl.setAttrib(el, a, attribs[a]);
}
for (let n = tree.length, i = 2; i < n; i++) {
hydrateTree(opts, impl, el, tree[i], i - 2);
}
const hydrateTree = (opts, impl, parent, tree, index = 0) => {
if (isArray(tree)) {
const el = impl.getChild(parent, index);
if (typeof tree[0] === "function") {
hydrateTree(
opts,
impl,
parent,
tree[0].apply(null, [opts.ctx, ...tree.slice(1)]),
index
);
}
else if (isNotStringAndIterable(tree)) {
for (let t of tree) {
hydrateTree(opts, impl, parent, t, index);
index++;
}
const attribs = tree[1];
if (attribs.__impl) {
return attribs.__impl.hydrateTree(
opts,
parent,
tree,
index
);
}
maybeInitElement(el, tree);
for (let a in attribs) {
a[0] === "o" && a[1] === "n" && impl.setAttrib(el, a, attribs[a]);
}
for (let n = tree.length, i = 2; i < n; i++) {
hydrateTree(opts, impl, el, tree[i], i - 2);
}
} else if (isNotStringAndIterable(tree)) {
for (let t of tree) {
hydrateTree(opts, impl, parent, t, index);
index++;
}
}
};
/**
* Creates a new DOM element of type `tag` with optional `attribs`. If
* `parent` is not `null`, the new element will be inserted as child at
* given `insert` index. If `insert` is missing, the element will be
* appended to the `parent`'s list of children. Returns new DOM node.
*
* If `tag` is a known SVG element name, the new element will be created
* with the proper SVG XML namespace.
*
* @param parent - DOM element
* @param tag - component tree
* @param attribs - attributes
* @param insert - child index
*/
export const createElement = (parent, tag, attribs, insert) => {
const el = SVG_TAGS[tag]
? document.createElementNS(XML_SVG, tag)
: document.createElement(tag);
attribs && setAttribs(el, attribs);
return addChild(parent, el, insert);
const createElement = (parent, tag, attribs, insert) => {
const el = SVG_TAGS[tag] ? document.createElementNS(XML_SVG, tag) : document.createElement(tag);
attribs && setAttribs(el, attribs);
return addChild(parent, el, insert);
};
export const createTextElement = (parent, content, insert) => addChild(parent, document.createTextNode(content), insert);
export const addChild = (parent, child, insert) => parent
? insert === undefined
? parent.appendChild(child)
: parent.insertBefore(child, parent.children[insert])
: child;
export const getChild = (parent, child) => parent.children[child];
export const replaceChild = (opts, impl, parent, child, tree, init = true) => (impl.removeChild(parent, child),
impl.createTree(opts, parent, tree, child, init));
export const cloneWithNewAttribs = (el, attribs) => {
const res = el.cloneNode(true);
setAttribs(res, attribs);
el.parentNode.replaceChild(res, el);
return res;
const createTextElement = (parent, content, insert) => addChild(parent, document.createTextNode(content), insert);
const addChild = (parent, child, insert) => parent ? insert === void 0 ? parent.appendChild(child) : parent.insertBefore(child, parent.children[insert]) : child;
const getChild = (parent, child) => parent.children[child];
const replaceChild = (opts, impl, parent, child, tree, init = true) => (impl.removeChild(parent, child), impl.createTree(opts, parent, tree, child, init));
const cloneWithNewAttribs = (el, attribs) => {
const res = el.cloneNode(true);
setAttribs(res, attribs);
el.parentNode.replaceChild(res, el);
return res;
};
export const setContent = (el, body) => (el.textContent = body);
export const setAttribs = (el, attribs) => {
for (let k in attribs) {
setAttrib(el, k, attribs[k], attribs);
}
return el;
const setContent = (el, body) => el.textContent = body;
const setAttribs = (el, attribs) => {
for (let k in attribs) {
setAttrib(el, k, attribs[k], attribs);
}
return el;
};
/**
* Sets a single attribute on given element. If attrib name is NOT an
* event name (prefix: "on") and its value is a function, it is called
* with given `attribs` object (usually the full attrib object passed to
* {@link setAttribs}) and the function's return value is used as the actual
* attrib value.
*
* Special rules apply for certain attributes:
*
* - "style": delegated to {@link setStyle}
* - "value": delegated to {@link updateValueAttrib}
* - attrib IDs starting with "on" are treated as event listeners
*
* If the given (or computed) attrib value is `false` or `undefined` the
* attrib is removed from the element.
*
* @param el - DOM element
* @param id - attribute name
* @param val - attribute value
* @param attribs - object of all attribs
*/
export const setAttrib = (el, id, val, attribs) => {
implementsFunction(val, "deref") && (val = val.deref());
if (id.startsWith("__"))
return;
const isListener = id[0] === "o" && id[1] === "n";
if (!isListener && typeof val === "function") {
val = val(attribs);
const setAttrib = (el, id, val, attribs) => {
implementsFunction(val, "deref") && (val = val.deref());
if (id.startsWith("__"))
return;
const isListener = id[0] === "o" && id[1] === "n";
if (isListener) {
if (isString(val)) {
el.setAttribute(id, val);
} else {
id = id.substring(2);
isArray(val) ? el.addEventListener(id, val[0], val[1]) : el.addEventListener(id, val);
}
if (val !== undefined && val !== false) {
switch (id) {
case "style":
setStyle(el, val);
break;
case "value":
updateValueAttrib(el, val);
break;
case "prefix":
el.setAttribute(id, isString(val) ? val : formatPrefixes(val));
break;
case "accesskey":
el.accessKey = val;
break;
case "contenteditable":
el.contentEditable = val;
break;
case "tabindex":
el.tabIndex = val;
break;
case "align":
case "autocapitalize":
case "checked":
case "dir":
case "draggable":
case "hidden":
case "id":
case "lang":
case "namespaceURI":
case "scrollTop":
case "scrollLeft":
case "title":
// TODO add more properties / enumerated attribs?
el[id] = val;
break;
default:
isListener
? setListener(el, id.substring(2), val)
: el.setAttribute(id, val === true ? "" : val);
}
}
else {
el.hasAttribute(id)
? el.removeAttribute("title")
: el[id] && (el[id] = null);
}
return el;
}
if (typeof val === "function")
val = val(attribs);
if (isArray(val))
val = val.join(ATTRIB_JOIN_DELIMS[id] || " ");
switch (id) {
case "style":
setStyle(el, val);
break;
case "value":
updateValueAttrib(el, val);
break;
case "prefix":
el.setAttribute(id, isString(val) ? val : formatPrefixes(val));
break;
case "accesskey":
case "accessKey":
el.accessKey = val;
break;
case "contenteditable":
case "contentEditable":
el.contentEditable = val;
break;
case "tabindex":
case "tabIndex":
el.tabIndex = val;
break;
case "align":
case "autocapitalize":
case "checked":
case "dir":
case "draggable":
case "hidden":
case "id":
case "indeterminate":
case "lang":
case "namespaceURI":
case "scrollLeft":
case "scrollTop":
case "selectionEnd":
case "selectionStart":
case "slot":
case "spellcheck":
case "title":
el[id] = val;
break;
default:
val === false || val == null ? el.removeAttribute(id) : el.setAttribute(id, val === true ? id : val);
}
return el;
};
/**
* Updates an element's `value` property. For form elements it too
* ensures the edit cursor retains its position.
*
* @param el - DOM element
* @param value - value
*/
export const updateValueAttrib = (el, value) => {
let ev;
switch (el.type) {
case "text":
case "textarea":
case "password":
case "search":
case "number":
case "email":
case "url":
case "tel":
case "date":
case "datetime-local":
case "time":
case "week":
case "month":
if ((ev = el.value) !== undefined && typeof value === "string") {
const off = value.length - (ev.length - (el.selectionStart || 0));
el.value = value;
el.selectionStart = el.selectionEnd = off;
break;
}
default:
el.value = value;
}
const updateValueAttrib = (el, value) => {
let ev;
switch (el.type) {
case "text":
case "textarea":
case "password":
case "search":
case "number":
case "email":
case "url":
case "tel":
case "date":
case "datetime-local":
case "time":
case "week":
case "month":
if ((ev = el.value) !== void 0 && typeof value === "string") {
const off = value.length - (ev.length - (el.selectionStart || 0));
el.value = value;
el.selectionStart = el.selectionEnd = off;
break;
}
default:
el.value = value;
}
};
export const removeAttribs = (el, attribs, prev) => {
for (let i = attribs.length; i-- > 0;) {
const a = attribs[i];
if (a[0] === "o" && a[1] === "n") {
removeListener(el, a.substring(2), prev[a]);
}
else {
el.hasAttribute(a) ? el.removeAttribute(a) : (el[a] = null);
}
const removeAttribs = (el, attribs, prev) => {
for (let i = attribs.length; i-- > 0; ) {
const a = attribs[i];
if (a[0] === "o" && a[1] === "n") {
removeListener(el, a.substring(2), prev[a]);
} else {
el.hasAttribute(a) ? el.removeAttribute(a) : el[a] = null;
}
}
};
export const setStyle = (el, styles) => (el.setAttribute("style", css(styles)), el);
/**
* Adds event listener (possibly with options).
*
* @param el - DOM element
* @param id - event name (w/o `on` prefix)
* @param listener -
*/
export const setListener = (el, id, listener) => isString(listener)
? el.setAttribute("on" + id, listener)
: isArray(listener)
? el.addEventListener(id, ...listener)
: el.addEventListener(id, listener);
/**
* Removes event listener (possibly with options).
*
* @param el - DOM element
* @param id - event name (w/o `on` prefix)
* @param listener -
*/
export const removeListener = (el, id, listener) => isArray(listener)
? el.removeEventListener(id, ...listener)
: el.removeEventListener(id, listener);
export const clearDOM = (el) => (el.innerHTML = "");
export const removeChild = (parent, childIdx) => {
const n = parent.children[childIdx];
n !== undefined && parent.removeChild(n);
const setStyle = (el, styles) => (el.setAttribute("style", css(styles)), el);
const setListener = (el, id, listener) => isString(listener) ? el.setAttribute("on" + id, listener) : isArray(listener) ? el.addEventListener(id, ...listener) : el.addEventListener(id, listener);
const removeListener = (el, id, listener) => isArray(listener) ? el.removeEventListener(id, ...listener) : el.removeEventListener(id, listener);
const clearDOM = (el) => el.innerHTML = "";
const removeChild = (parent, childIdx) => {
const n = parent.children[childIdx];
n !== void 0 && parent.removeChild(n);
};
export {
addChild,
clearDOM,
cloneWithNewAttribs,
createElement,
createTextElement,
createTree,
getChild,
hydrateTree,
removeAttribs,
removeChild,
removeListener,
replaceChild,
setAttrib,
setAttribs,
setContent,
setListener,
setStyle,
updateValueAttrib
};
import { NULL_LOGGER } from "@thi.ng/logger/null";
export let LOGGER = NULL_LOGGER;
export const setLogger = (logger) => (LOGGER = logger);
let LOGGER = NULL_LOGGER;
const setLogger = (logger) => LOGGER = logger;
export {
LOGGER,
setLogger
};

@@ -10,141 +10,120 @@ import { isArray as isa } from "@thi.ng/checks/is-array";

const isPlainObject = iso;
/**
* Expands single hiccup element/component into its canonical form:
*
* ```
* [tagname, {attribs}, ...children]
* ```
*
* Emmet-style ID and class names in the original tagname are moved into
* the attribs object, e.g.:
*
* ```
* ["div#foo.bar.baz"] => ["div", {id: "foo", class: "bar baz"}]
* ```
*
* If both Emmet-style classes AND a `class` attrib exists, the former
* are appended to the latter:
*
* ```
* ["div.bar.baz", {class: "foo"}] => ["div", {class: "foo bar baz"}]
* ```
*
* Elements with `__skip` attrib enabled and no children, will have an
* empty text child element injected.
*
* @param spec - single hdom component
* @param keys -
*
* @internal
*/
export const normalizeElement = (spec, keys) => {
let tag = spec[0];
let hasAttribs = isPlainObject(spec[1]);
let match;
let name;
let attribs;
if (typeof tag !== "string" || !(match = RE_TAG.exec(tag))) {
illegalArgs(`${tag} is not a valid tag name`);
}
name = match[1];
// return orig if already normalized and satisfies key requirement
if (tag === name && hasAttribs && (!keys || spec[1].key)) {
return spec;
}
attribs = mergeEmmetAttribs(hasAttribs ? { ...spec[1] } : {}, match[2], match[3]);
return attribs.__skip && spec.length < 3
? [name, attribs]
: [name, attribs, ...spec.slice(hasAttribs ? 2 : 1)];
const normalizeElement = (spec, keys) => {
let tag = spec[0];
let hasAttribs = isPlainObject(spec[1]);
let match;
let name;
let attribs;
if (typeof tag !== "string" || !(match = RE_TAG.exec(tag))) {
illegalArgs(`${tag} is not a valid tag name`);
}
name = match[1];
if (tag === name && hasAttribs && (!keys || spec[1].key)) {
return spec;
}
attribs = mergeEmmetAttribs(
hasAttribs ? { ...spec[1] } : {},
match[2],
match[3]
);
return attribs.__skip && spec.length < 3 ? [name, attribs] : [name, attribs, ...spec.slice(hasAttribs ? 2 : 1)];
};
/**
* See {@link HDOMImplementation} interface for further details.
*
* @param opts - hdom config options
* @param tree - component tree
*/
export const normalizeTree = (opts, tree) => _normalizeTree(tree, opts, opts.ctx, [0], opts.keys !== false, opts.span !== false);
const normalizeTree = (opts, tree) => _normalizeTree(
tree,
opts,
opts.ctx,
[0],
opts.keys !== false,
opts.span !== false
);
const _normalizeTree = (tree, opts, ctx, path, keys, span) => {
if (tree == null) {
return;
if (tree == null) {
return;
}
if (isArray(tree)) {
if (tree.length === 0) {
return;
}
if (isArray(tree)) {
if (tree.length === 0) {
return;
}
let norm, nattribs = tree[1], impl;
// if available, use branch-local normalize implementation
if (nattribs &&
(impl = nattribs.__impl) &&
(impl = impl.normalizeTree)) {
return impl(opts, tree);
}
const tag = tree[0];
// use result of function call
// pass ctx as first arg and remaining array elements as rest args
if (typeof tag === "function") {
return _normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), opts, ctx, path, keys, span);
}
// component object w/ life cycle methods
// (render() is the only required hook)
if (typeof tag.render === "function") {
const args = [ctx, ...tree.slice(1)];
norm = _normalizeTree(tag.render.apply(tag, args), opts, ctx, path, keys, span);
if (isArray(norm)) {
norm.__this = tag;
norm.__init = tag.init;
norm.__release = tag.release;
norm.__args = args;
}
return norm;
}
norm = normalizeElement(tree, keys);
nattribs = norm[1];
if (nattribs.__normalize === false) {
return norm;
}
if (keys && nattribs.key === undefined) {
nattribs.key = path.join("-");
}
return norm.length > 2
? normalizeChildren(norm, nattribs, opts, ctx, path, keys, span)
: norm;
let norm, nattribs = tree[1], impl;
if (nattribs && (impl = nattribs.__impl) && (impl = impl.normalizeTree)) {
return impl(opts, tree);
}
return typeof tree === "function"
? _normalizeTree(tree(ctx), opts, ctx, path, keys, span)
: typeof tree.toHiccup === "function"
? _normalizeTree(tree.toHiccup(opts.ctx), opts, ctx, path, keys, span)
: typeof tree.deref === "function"
? _normalizeTree(tree.deref(), opts, ctx, path, keys, span)
: span
? ["span", keys ? { key: path.join("-") } : {}, tree.toString()]
: tree.toString();
const tag = tree[0];
if (typeof tag === "function") {
return _normalizeTree(
tag.apply(null, [ctx, ...tree.slice(1)]),
opts,
ctx,
path,
keys,
span
);
}
if (typeof tag.render === "function") {
const args = [ctx, ...tree.slice(1)];
norm = _normalizeTree(
tag.render.apply(tag, args),
opts,
ctx,
path,
keys,
span
);
if (isArray(norm)) {
norm.__this = tag;
norm.__init = tag.init;
norm.__release = tag.release;
norm.__args = args;
}
return norm;
}
norm = normalizeElement(tree, keys);
nattribs = norm[1];
if (nattribs.__normalize === false) {
return norm;
}
if (keys && nattribs.key === void 0) {
nattribs.key = path.join("-");
}
return norm.length > 2 ? normalizeChildren(norm, nattribs, opts, ctx, path, keys, span) : norm;
}
return typeof tree === "function" ? _normalizeTree(tree(ctx), opts, ctx, path, keys, span) : typeof tree.toHiccup === "function" ? _normalizeTree(tree.toHiccup(opts.ctx), opts, ctx, path, keys, span) : typeof tree.deref === "function" ? _normalizeTree(tree.deref(), opts, ctx, path, keys, span) : span ? ["span", keys ? { key: path.join("-") } : {}, tree.toString()] : tree.toString();
};
const normalizeChildren = (norm, nattribs, opts, ctx, path, keys, span) => {
const tag = norm[0];
const res = [tag, nattribs];
span = span && !NO_SPANS[tag];
for (let i = 2, j = 2, k = 0, n = norm.length; i < n; i++) {
let el = norm[i];
if (el != null) {
const isarray = isArray(el);
if ((isarray && isArray(el[0])) ||
(!isarray && isNotStringAndIterable(el))) {
for (let c of el) {
c = _normalizeTree(c, opts, ctx, path.concat(k), keys, span);
if (c !== undefined) {
res[j++] = c;
}
k++;
}
}
else {
el = _normalizeTree(el, opts, ctx, path.concat(k), keys, span);
if (el !== undefined) {
res[j++] = el;
}
k++;
}
const tag = norm[0];
const res = [tag, nattribs];
span = span && !NO_SPANS[tag];
for (let i = 2, j = 2, k = 0, n = norm.length; i < n; i++) {
let el = norm[i];
if (el != null) {
const isarray = isArray(el);
if (isarray && isArray(el[0]) || !isarray && isNotStringAndIterable(el)) {
for (let c of el) {
c = _normalizeTree(
c,
opts,
ctx,
path.concat(k),
keys,
span
);
if (c !== void 0) {
res[j++] = c;
}
k++;
}
} else {
el = _normalizeTree(el, opts, ctx, path.concat(k), keys, span);
if (el !== void 0) {
res[j++] = el;
}
k++;
}
}
return res;
}
return res;
};
export {
normalizeElement,
normalizeTree
};
{
"name": "@thi.ng/hdom",
"version": "9.3.31",
"version": "9.4.0",
"description": "Lightweight vanilla ES6 UI component trees with customizable branch-local behaviors",

@@ -30,3 +30,5 @@ "type": "module",

"scripts": {
"build": "yarn clean && tsc --declaration",
"build": "yarn build:esbuild && yarn build:decl",
"build:decl": "tsc --declaration --emitDeclarationOnly",
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",

@@ -40,14 +42,15 @@ "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",

"dependencies": {
"@thi.ng/api": "^8.9.11",
"@thi.ng/checks": "^3.4.11",
"@thi.ng/diff": "^5.1.44",
"@thi.ng/equiv": "^2.1.36",
"@thi.ng/errors": "^2.4.5",
"@thi.ng/hiccup": "^5.1.0",
"@thi.ng/logger": "^2.0.1",
"@thi.ng/prefixes": "^2.2.7"
"@thi.ng/api": "^8.9.12",
"@thi.ng/checks": "^3.4.12",
"@thi.ng/diff": "^5.1.45",
"@thi.ng/equiv": "^2.1.37",
"@thi.ng/errors": "^2.4.6",
"@thi.ng/hiccup": "^5.1.1",
"@thi.ng/logger": "^2.0.2",
"@thi.ng/prefixes": "^2.2.8"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.38.3",
"@thi.ng/atom": "^5.2.17",
"@thi.ng/atom": "^5.2.18",
"esbuild": "^0.19.8",
"rimraf": "^5.0.5",

@@ -134,3 +137,3 @@ "tools": "^0.0.1",

},
"gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n"
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n"
}

@@ -160,3 +160,3 @@ <!-- This file is generated - DO NOT EDIT! -->

Package sizes (brotli'd, pre-treeshake): ESM: 3.42 KB
Package sizes (brotli'd, pre-treeshake): ESM: 3.49 KB

@@ -163,0 +163,0 @@ ## Dependencies

import { derefContext } from "@thi.ng/hiccup/deref";
import { DEFAULT_IMPL } from "./default.js";
import { resolveRoot } from "./resolve.js";
/**
* One-off hdom tree conversion & target DOM application. Takes same
* options as {@link start}, but performs no diffing and only creates or
* hydrates target once. The given tree is first normalized and if
* result is `null` or `undefined` no further action will be taken.
*
* @param tree - component tree
* @param opts - hdom config options
* @param impl - hdom implementation
*/
export const renderOnce = (tree, opts = {}, impl = DEFAULT_IMPL) => {
opts = { root: "app", ...opts };
opts.ctx = derefContext(opts.ctx, opts.autoDerefKeys);
const root = resolveRoot(opts.root, impl);
tree = impl.normalizeTree(opts, tree);
if (!tree)
return;
opts.hydrate
? impl.hydrateTree(opts, root, tree)
: impl.createTree(opts, root, tree);
const renderOnce = (tree, opts = {}, impl = DEFAULT_IMPL) => {
opts = { root: "app", ...opts };
opts.ctx = derefContext(opts.ctx, opts.autoDerefKeys);
const root = resolveRoot(opts.root, impl);
tree = impl.normalizeTree(opts, tree);
if (!tree)
return;
opts.hydrate ? impl.hydrateTree(opts, root, tree) : impl.createTree(opts, root, tree);
};
export {
renderOnce
};
import { isString } from "@thi.ng/checks/is-string";
export const resolveRoot = (root, impl) => isString(root) ? impl.getElementById(root) : root;
const resolveRoot = (root, impl) => isString(root) ? impl.getElementById(root) : root;
export {
resolveRoot
};
import { derefContext } from "@thi.ng/hiccup/deref";
import { DEFAULT_IMPL } from "./default.js";
import { resolveRoot } from "./resolve.js";
/**
* Takes an hiccup tree (array, function or component object w/ life cycle
* methods) and an optional object of DOM update options. Starts RAF update
* loop, in each iteration first normalizing given tree, then computing diff to
* previous frame's tree and applying any changes to the real DOM. The `ctx`
* option can be used for passing arbitrary config data or state down into the
* hiccup component tree. Any embedded component function in the tree will
* receive this context object (shallow copy) as first argument, as will life
* cycle methods in component objects. If the `autoDerefKeys` option is given,
* attempts to auto-expand/deref the given keys in the user supplied context
* object (`ctx` option) prior to *each* tree normalization. All of these values
* should implement the
* [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html) interface
* (e.g. atoms, cursors, views, rstreams etc.). This feature can be used to
* define dynamic contexts linked to the main app state, e.g. using derived
* views provided by [`thi.ng/atom`](https://thi.ng/atom).
*
* **Selective updates**: No updates will be applied if the given hiccup tree is
* `undefined` or `null` or a root component function returns no value. This way
* a given root function can do some state handling of its own and implement
* fail-fast checks to determine no DOM updates are necessary, save effort
* re-creating a new hiccup tree and request skipping DOM updates via this
* function. In this case, the previous DOM tree is kept around until the root
* function returns a tree again, which then is diffed and applied against the
* previous tree kept as usual. Any number of frames may be skipped this way.
*
* **Important:** Unless the `hydrate` option is enabled, the parent element
* given is assumed to have NO children at the time when `start()` is called.
* Since hdom does NOT track the real DOM, the resulting changes will result in
* potentially undefined behavior if the parent element wasn't empty. Likewise,
* if `hydrate` is enabled, it is assumed that an equivalent DOM (minus
* listeners) already exists (i.e. generated via SSR) when `start()` is called.
* Any other discrepancies between the pre-existing DOM and the hdom trees will
* cause undefined behavior.
*
* Returns a function, which when called, immediately cancels the update loop.
*
* @param tree - hiccup DOM tree
* @param opts - options
* @param impl - hdom target implementation
*/
export const start = (tree, opts = {}, impl = DEFAULT_IMPL) => {
const _opts = { root: "app", ...opts };
let prev = [];
let isActive = true;
const root = resolveRoot(_opts.root, impl);
const update = () => {
if (isActive) {
_opts.ctx = derefContext(opts.ctx, _opts.autoDerefKeys);
const curr = impl.normalizeTree(_opts, tree);
if (curr != null) {
if (_opts.hydrate) {
impl.hydrateTree(_opts, root, curr);
_opts.hydrate = false;
}
else {
impl.diffTree(_opts, root, prev, curr);
}
prev = curr;
}
// check again in case one of the components called cancel
isActive && requestAnimationFrame(update);
const start = (tree, opts = {}, impl = DEFAULT_IMPL) => {
const _opts = { root: "app", ...opts };
let prev = [];
let isActive = true;
const root = resolveRoot(_opts.root, impl);
const update = () => {
if (isActive) {
_opts.ctx = derefContext(opts.ctx, _opts.autoDerefKeys);
const curr = impl.normalizeTree(_opts, tree);
if (curr != null) {
if (_opts.hydrate) {
impl.hydrateTree(_opts, root, curr);
_opts.hydrate = false;
} else {
impl.diffTree(_opts, root, prev, curr);
}
};
requestAnimationFrame(update);
return () => (isActive = false);
prev = curr;
}
isActive && requestAnimationFrame(update);
}
};
requestAnimationFrame(update);
return () => isActive = false;
};
export {
start
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc