@thi.ng/hdom
Advanced tools
Comparing version 9.3.31 to 9.4.0
# 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 @@ |
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 | ||
}; |
463
diff.js
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 | ||
}; |
471
dom.js
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 | ||
}; |
241
normalize.js
@@ -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 | ||
}; |
90
start.js
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 | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
150954
7
1380
Updated@thi.ng/api@^8.9.12
Updated@thi.ng/checks@^3.4.12
Updated@thi.ng/diff@^5.1.45
Updated@thi.ng/equiv@^2.1.37
Updated@thi.ng/errors@^2.4.6
Updated@thi.ng/hiccup@^5.1.1
Updated@thi.ng/logger@^2.0.2
Updated@thi.ng/prefixes@^2.2.8