Created
October 25, 2024 06:21
-
-
Save StephanHoyer/9962c5730b0c6892b95d327984defb12 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
var hasOwn = {}.hasOwnProperty; | |
/* eslint-disable no-bitwise */ | |
/* | |
Caution: be sure to check the minified output. I've noticed an issue with Terser trying to inline | |
single-use functions as IIFEs, and this predictably causes perf issues since engines don't seem to | |
reliably lower this in either their bytecode generation *or* their optimized code. | |
Rather than painfully trying to reduce that to an MVC and filing a bug against it, I'm just | |
inlining and commenting everything. It also gives me a better idea of the true cost of various | |
functions. | |
In `m`, I do use a no-inline hints (the `__NOINLINE__` in an inline block comment there) to | |
prevent Terser from inlining a cold function in a very hot code path, to try to squeeze a little | |
more performance out of the framework. Likewise, to try to preserve this through build scripts, | |
Terser annotations are preserved in the ESM production bundle (but not the UMD bundle). | |
Also, be aware: I use some bit operations here. Nothing super fancy like find-first-set, just | |
mainly ANDs, ORs, and a one-off XOR for inequality. | |
*/ | |
/* | |
State note: | |
If remove on throw is `true` and an error occurs: | |
- All visited vnodes' new versions are removed. | |
- All unvisited vnodes' old versions are removed. | |
If remove on throw is `false` and an error occurs: | |
- Attribute modification errors are logged. | |
- Views that throw retain the previous version and log their error. | |
- Errors other than the above cause the tree to be torn down as if remove on throw was `true`. | |
*/ | |
/* | |
This same structure is used for several nodes. Here's an explainer for each type. | |
Retain: | |
- `m`: `-1` | |
- All other properties are unused | |
- On ingest, the vnode itself is converted into the type of the element it's retaining. This | |
includes changing its type. | |
Fragments: | |
- `m` bits 0-2: `0` | |
- `t`: unused | |
- `s`: unused | |
- `a`: unused | |
- `c`: virtual DOM children | |
- `d`: unused | |
Keys: | |
- `m` bits 0-2: `1` | |
- `t`: `KEY` | |
- `s`: identity key (may be any arbitrary object) | |
- `a`: unused | |
- `c`: virtual DOM children | |
- `d`: unused | |
Text: | |
- `m` bits 0-2: `2` | |
- `t`: unused | |
- `s`: text string | |
- `a`: unused | |
- `c`: unused | |
- `d`: abort controller reference | |
Components: | |
- `m` bits 0-2: `3` | |
- `t`: component reference | |
- `s`: view function, may be same as component reference | |
- `a`: most recently received attributes | |
- `c`: instance vnode | |
- `d`: unused | |
DOM elements: | |
- `m` bits 0-2: `4` | |
- `t`: tag name string | |
- `s`: event listener dictionary, if any events were ever registered | |
- `a`: most recently received attributes | |
- `c`: virtual DOM children | |
- `d`: element reference | |
Layout: | |
- `m` bits 0-2: `5` | |
- `t`: unused | |
- `s`: callback to schedule | |
- `a`: unused | |
- `c`: unused | |
- `d`: parent DOM reference, for easier queueing | |
Remove: | |
- `m` bits 0-2: `6` | |
- `t`: unused | |
- `s`: callback to schedule | |
- `a`: unused | |
- `c`: unused | |
- `d`: parent DOM reference, for easier queueing | |
The `m` field is also used for various assertions, that aren't described here. | |
*/ | |
var TYPE_MASK = 7; | |
var TYPE_RETAIN = -1; | |
var TYPE_FRAGMENT = 0; | |
var TYPE_KEY = 1; | |
var TYPE_TEXT = 2; | |
var TYPE_ELEMENT = 3; | |
var TYPE_COMPONENT = 4; | |
var TYPE_LAYOUT = 5; | |
var TYPE_REMOVE = 6; | |
var TYPE_SET_CONTEXT = 7; | |
var FLAG_KEYED = 1 << 3; | |
var FLAG_USED = 1 << 4; | |
var FLAG_IS_REMOVE = 1 << 5; | |
var FLAG_HTML_ELEMENT = 1 << 6; | |
var FLAG_CUSTOM_ELEMENT = 1 << 7; | |
var FLAG_INPUT_ELEMENT = 1 << 8; | |
var FLAG_SELECT_ELEMENT = 1 << 9; | |
var FLAG_OPTION_ELEMENT = 1 << 10; | |
var FLAG_TEXTAREA_ELEMENT = 1 << 11; | |
var FLAG_IS_FILE_INPUT = 1 << 12; | |
var Vnode = (mask, tag, state, attrs, children) => ({ | |
m: mask, | |
t: tag, | |
s: state, | |
a: attrs, | |
c: children, | |
// Think of this as either "data" or "DOM" - it's used for both. | |
d: null, | |
}); | |
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g; | |
var selectorUnescape = /\\(["'\\])/g; | |
var selectorCache = /*@__PURE__*/ new Map(); | |
var compileSelector = (selector) => { | |
var match, tag = "div", classes = [], attrs = {}, className, hasAttrs = false; | |
while (match = selectorParser.exec(selector)) { | |
var type = match[1], value = match[2]; | |
if (type === "" && value !== "") { | |
tag = value; | |
} else { | |
hasAttrs = true; | |
if (type === "#") { | |
attrs.id = value; | |
} else if (type === ".") { | |
classes.push(value); | |
} else if (match[3][0] === "[") { | |
var attrValue = match[6]; | |
if (attrValue) attrValue = attrValue.replace(selectorUnescape, "$1"); | |
if (match[4] === "class" || match[4] === "className") classes.push(attrValue); | |
else attrs[match[4]] = attrValue == null || attrValue; | |
} | |
} | |
} | |
if (classes.length > 0) { | |
className = classes.join(" "); | |
} | |
var state = {t: tag, a: hasAttrs ? attrs : null, c: className}; | |
selectorCache.set(selector, state); | |
return state | |
}; | |
/* | |
Edit this with caution and profile every change you make. This comprises about 4% of the total | |
runtime overhead in benchmarks, and any reduction in performance here will immediately be felt. | |
Also, it's specially designed to only allocate the bare minimum it needs to build vnodes, as part | |
of this optimization process. It doesn't allocate arguments except as needed to build children, it | |
doesn't allocate attributes except to replace them for modifications, among other things. | |
*/ | |
var m = function (selector, attrs) { | |
if (typeof selector !== "string" && typeof selector !== "function") { | |
throw new Error("The selector must be either a string or a component."); | |
} | |
var start = 1; | |
var children; | |
if (attrs == null || typeof attrs === "object" && typeof attrs.m !== "number" && !Array.isArray(attrs)) { | |
start = 2; | |
if (arguments.length < 3 && attrs && Array.isArray(attrs.children)) { | |
children = attrs.children.slice(); | |
} | |
} else { | |
attrs = null; | |
} | |
if (children == null) { | |
if (arguments.length === start + 1 && Array.isArray(arguments[start])) { | |
children = arguments[start].slice(); | |
} else { | |
children = []; | |
while (start < arguments.length) children.push(arguments[start++]); | |
} | |
} | |
// It may seem expensive to inline elements handling, but it's less expensive than you'd think. | |
// DOM nodes are about as commonly constructed as vnodes, but fragments are only constructed | |
// from JSX code (and even then, they aren't common). | |
if (typeof selector !== "string") { | |
if (selector === m.Fragment) { | |
return createParentVnode(TYPE_FRAGMENT, null, null, null, children) | |
} else { | |
return Vnode(TYPE_COMPONENT, selector, null, Object.assign({children}, attrs), null) | |
} | |
} | |
attrs = attrs || {}; | |
var hasClassName = hasOwn.call(attrs, "className"); | |
var dynamicClass = hasClassName ? attrs.className : attrs.class; | |
var state = selectorCache.get(selector); | |
var original = attrs; | |
if (state == null) { | |
state = /*@__NOINLINE__*/compileSelector(selector); | |
} | |
if (state.a != null) { | |
attrs = Object.assign({}, state.a, attrs); | |
} | |
if (dynamicClass != null || state.c != null) { | |
if (attrs !== original) attrs = Object.assign({}, attrs); | |
attrs.class = dynamicClass != null | |
? state.c != null ? `${state.c} ${dynamicClass}` : dynamicClass | |
: state.c; | |
if (hasClassName) attrs.className = null; | |
} | |
return createParentVnode(TYPE_ELEMENT, selector, null, attrs, children) | |
}; | |
m.TYPE_MASK = TYPE_MASK; | |
m.TYPE_RETAIN = TYPE_RETAIN; | |
m.TYPE_FRAGMENT = TYPE_FRAGMENT; | |
m.TYPE_KEY = TYPE_KEY; | |
m.TYPE_TEXT = TYPE_TEXT; | |
m.TYPE_ELEMENT = TYPE_ELEMENT; | |
m.TYPE_COMPONENT = TYPE_COMPONENT; | |
m.TYPE_LAYOUT = TYPE_LAYOUT; | |
m.TYPE_REMOVE = TYPE_REMOVE; | |
m.TYPE_SET_CONTEXT = TYPE_SET_CONTEXT; | |
// Simple and sweet. Also useful for idioms like `onfoo: m.capture` to drop events without | |
// redrawing. | |
m.capture = (ev) => { | |
ev.preventDefault(); | |
ev.stopPropagation(); | |
return false | |
}; | |
m.retain = () => Vnode(TYPE_RETAIN, null, null, null, null); | |
m.layout = (callback) => { | |
if (typeof callback !== "function") { | |
throw new TypeError("Callback must be a function if provided") | |
} | |
return Vnode(TYPE_LAYOUT, null, callback, null, null) | |
}; | |
m.remove = (callback) => { | |
if (typeof callback !== "function") { | |
throw new TypeError("Callback must be a function if provided") | |
} | |
return Vnode(TYPE_REMOVE, null, callback, null, null) | |
}; | |
m.Fragment = (attrs) => attrs.children; | |
m.key = (key, ...children) => | |
createParentVnode(TYPE_KEY, key, null, null, | |
children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] | |
); | |
m.set = (entries, ...children) => | |
createParentVnode(TYPE_SET_CONTEXT, null, null, entries, | |
children.length === 1 && Array.isArray(children[0]) ? children[0].slice() : [...children] | |
); | |
m.normalize = (node) => { | |
if (node == null || typeof node === "boolean") return null | |
if (typeof node !== "object") return Vnode(TYPE_TEXT, null, String(node), null, null) | |
if (Array.isArray(node)) return createParentVnode(TYPE_FRAGMENT, null, null, null, node.slice()) | |
return node | |
}; | |
var createParentVnode = (mask, tag, state, attrs, input) => { | |
if (input.length) { | |
input[0] = m.normalize(input[0]); | |
var isKeyed = input[0] != null && (input[0].m & TYPE_MASK) === TYPE_KEY; | |
var keys = new Set(); | |
mask |= -isKeyed & FLAG_KEYED; | |
// Note: this is a *very* perf-sensitive check. | |
// Fun fact: merging the loop like this is somehow faster than splitting | |
// it, noticeably so. | |
for (var i = 1; i < input.length; i++) { | |
input[i] = m.normalize(input[i]); | |
if ((input[i] != null && (input[i].m & TYPE_MASK) === TYPE_KEY) !== isKeyed) { | |
throw new TypeError( | |
isKeyed | |
? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit empty key vnode, `m.key()`, instead of a hole." | |
: "In fragments, vnodes must either all have keys or none have keys." | |
) | |
} | |
if (isKeyed) { | |
if (keys.has(input[i].t)) { | |
throw new TypeError(`Duplicate key detected: ${input[i].t}`) | |
} | |
keys.add(input[i].t); | |
} | |
} | |
} | |
return Vnode(mask, tag, state, attrs, input) | |
}; | |
var xlinkNs = "http://www.w3.org/1999/xlink"; | |
var htmlNs = "http://www.w3.org/1999/xhtml"; | |
var nameSpace = { | |
svg: "http://www.w3.org/2000/svg", | |
math: "http://www.w3.org/1998/Math/MathML" | |
}; | |
var currentHooks; | |
var currentRedraw; | |
var currentParent; | |
var currentRefNode; | |
var currentNamespace; | |
var currentDocument; | |
var currentContext; | |
var currentRemoveOnThrow; | |
var insertAfterCurrentRefNode = (child) => { | |
if (currentRefNode) { | |
currentRefNode.after(currentRefNode = child); | |
} else { | |
currentParent.prepend(currentRefNode = child); | |
} | |
}; | |
//update | |
var moveToPosition = (vnode) => { | |
var type; | |
while ((type = vnode.m & TYPE_MASK) === TYPE_COMPONENT) { | |
if (!(vnode = vnode.c)) return | |
} | |
if ((1 << TYPE_FRAGMENT | 1 << TYPE_KEY | 1 << TYPE_SET_CONTEXT) & 1 << type) { | |
vnode.c.forEach(moveToPosition); | |
} else if ((1 << TYPE_TEXT | 1 << TYPE_ELEMENT) & 1 << type) { | |
insertAfterCurrentRefNode(vnode.d); | |
} | |
}; | |
var updateFragment = (old, vnode) => { | |
// Here's the logic: | |
// - If `old` or `vnode` is `null`, common length is 0 by default, and it falls back to an | |
// unkeyed empty fragment. | |
// - If `old` and `vnode` differ in their keyedness, their children must be wholly replaced. | |
// - If `old` and `vnode` are both non-keyed, patch their children linearly. | |
// - If `old` and `vnode` are both keyed, patch their children using a map. | |
var mask = vnode != null ? vnode.m : 0; | |
var newLength = vnode != null ? vnode.c.length : 0; | |
var oldMask = old != null ? old.m : 0; | |
var oldLength = old != null ? old.c.length : 0; | |
var commonLength = oldLength < newLength ? oldLength : newLength; | |
if ((oldMask ^ mask) & FLAG_KEYED) { // XOR is equivalent to bit inequality | |
// Key state changed. Replace the subtree | |
commonLength = 0; | |
mask &= ~FLAG_KEYED; | |
} | |
if (!(mask & FLAG_KEYED)) { | |
// Not keyed. Patch the common prefix, remove the extra in the old, and create the | |
// extra in the new. | |
// | |
// Can't just take the max of both, because out-of-bounds accesses both disrupts | |
// optimizations and is just generally slower. | |
// | |
// Note: if either `vnode` or `old` is `null`, the common length and its own length are | |
// both zero, so it can't actually throw. | |
try { | |
for (var i = 0; i < commonLength; i++) updateNode(old.c[i], vnode.c[i]); | |
for (var i = commonLength; i < newLength; i++) updateNode(null, vnode.c[i]); | |
} catch (e) { | |
commonLength = i; | |
for (var i = 0; i < commonLength; i++) updateNode(vnode.c[i], null); | |
for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null); | |
throw e | |
} | |
for (var i = commonLength; i < oldLength; i++) updateNode(old.c[i], null); | |
} else { | |
// Keyed. I take a pretty straightforward approach here to keep it simple: | |
// 1. Build a map from old map to old vnode. | |
// 2. Walk the new vnodes, adding what's missing and patching what's in the old. | |
// 3. Remove from the old map the keys in the new vnodes, leaving only the keys that | |
// were removed this run. | |
// 4. Remove the remaining nodes in the old map that aren't in the new map. Since the | |
// new keys were already deleted, this is just a simple map iteration. | |
// Note: if either `vnode` or `old` is `null`, they won't get here. The default mask is | |
// zero, and that causes keyed state to differ and thus a forced linear diff per above. | |
var oldMap = new Map(); | |
for (var p of old.c) oldMap.set(p.t, p); | |
try { | |
for (var i = 0; i < newLength; i++) { | |
var n = vnode.c[i]; | |
var p = oldMap.get(n.t); | |
if (p == null) { | |
updateFragment(null, n); | |
} else { | |
oldMap.delete(n.t); | |
var prev = currentRefNode; | |
moveToPosition(p); | |
currentRefNode = prev; | |
updateFragment(p, n); | |
} | |
} | |
} catch (e) { | |
for (var j = 0; j < i; j++) updateNode(vnode.c[j], null); | |
updateNode(old.c[j], null); | |
oldMap.forEach((p) => updateNode(p, null)); | |
throw e | |
} | |
oldMap.forEach((p) => updateNode(p, null)); | |
} | |
}; | |
var updateNode = (old, vnode) => { | |
// This is important. Declarative state bindings that rely on dependency tracking, like | |
// https://github.com/tc39/proposal-signals and related, memoize their results, but that's the | |
// absolute extent of what they necessarily reuse. They don't pool anything. That means all I | |
// need to do to support components based on them is just add this neat single line of code | |
// here. | |
// | |
// Code based on streams (see this repo here) will also potentially need this depending on how | |
// they do their combinators. | |
if (old === vnode) return | |
var type; | |
if (old == null) { | |
if (vnode == null) return | |
if (vnode.m < 0) { | |
throw new Error("No node present to retain with `m.retain()`") | |
} | |
if (vnode.m & FLAG_USED) { | |
throw new TypeError("Vnodes must not be reused") | |
} | |
type = vnode.m & TYPE_MASK; | |
vnode.m |= FLAG_USED; | |
} else { | |
type = old.m & TYPE_MASK; | |
if (vnode == null) { | |
try { | |
removeNodeDispatch[type](old); | |
} catch (e) { | |
console.error(e); | |
} | |
return | |
} | |
if (vnode.m < 0) { | |
// If it's a retain node, transmute it into the node it's retaining. Makes it much easier | |
// to implement and work with. | |
// | |
// Note: this key list *must* be complete. | |
vnode.m = old.m; | |
vnode.t = old.t; | |
vnode.s = old.s; | |
vnode.a = old.a; | |
vnode.c = old.c; | |
vnode.d = old.d; | |
return | |
} | |
if (vnode.m & FLAG_USED) { | |
throw new TypeError("Vnodes must not be reused") | |
} | |
var newType = vnode.m & TYPE_MASK; | |
if (type === newType && vnode.t === old.t) { | |
vnode.m = old.m & ~FLAG_KEYED | vnode.m & FLAG_KEYED; | |
} else { | |
updateNode(old, null); | |
type = newType; | |
old = null; | |
} | |
} | |
try { | |
updateNodeDispatch[type](old, vnode); | |
} catch (e) { | |
updateNode(old, null); | |
throw e | |
} | |
}; | |
var updateLayout = (_, vnode) => { | |
vnode.d = currentParent; | |
currentHooks.push(vnode); | |
}; | |
var updateRemove = (_, vnode) => { | |
vnode.d = currentParent; | |
}; | |
var emptyObject = {}; | |
var updateSet = (old, vnode) => { | |
var descs = Object.getOwnPropertyDescriptors(vnode.a); | |
for (var key of Reflect.ownKeys(descs)) { | |
// Drop the descriptor entirely if it's not enumerable. Setting it to an empty object | |
// avoids changing its shape, which is useful. | |
if (!descs[key].enumerable) descs[key] = emptyObject; | |
// Drop the setter if one is present, to keep it read-only. | |
else if ("set" in descs[key]) descs[key].set = undefined; | |
} | |
var prevContext = currentContext; | |
currentContext = Object.freeze(Object.create(prevContext, descs)); | |
updateFragment(old, vnode); | |
currentContext = prevContext; | |
}; | |
var updateText = (old, vnode) => { | |
if (old == null) { | |
insertAfterCurrentRefNode(vnode.d = currentDocument.createTextNode(vnode.s)); | |
} else { | |
if (`${old.s}` !== `${vnode.s}`) old.d.nodeValue = vnode.s; | |
vnode.d = currentRefNode = old.d; | |
} | |
}; | |
var handleAttributeError = (old, e, force) => { | |
if (currentRemoveOnThrow || force) { | |
removeNode(old); | |
updateFragment(old, null); | |
throw e | |
} | |
console.error(e); | |
}; | |
var updateElement = (old, vnode) => { | |
var prevParent = currentParent; | |
var prevNamespace = currentNamespace; | |
var mask = vnode.m; | |
var attrs = vnode.a; | |
var element , oldAttrs; | |
if (old == null) { | |
var entry = selectorCache.get(vnode.t); | |
var tag = entry ? entry.t : vnode.t; | |
var customTag = tag.includes("-"); | |
var is = !customTag && attrs && attrs.is; | |
var ns = attrs && attrs.xmlns || nameSpace[tag] || prevNamespace; | |
var opts = is ? {is} : null; | |
insertAfterCurrentRefNode(element = vnode.d = ( | |
ns | |
? currentDocument.createElementNS(ns, tag, opts) | |
: currentDocument.createElement(tag, opts) | |
)); | |
if (ns == null) { | |
// Doing it this way since it doesn't seem Terser is smart enough to optimize the `if` with | |
// every branch doing `a |= value` for differing `value`s to a ternary. It *is* smart | |
// enough to inline the constants, and the following pass optimizes the rest to just | |
// integers. | |
// | |
// Doing a simple constant-returning ternary also makes it easier for engines to emit the | |
// right code. | |
/* eslint-disable indent */ | |
vnode.m = mask |= ( | |
is || customTag | |
? FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT | |
: (tag = tag.toUpperCase(), ( | |
tag === "INPUT" ? FLAG_HTML_ELEMENT | FLAG_INPUT_ELEMENT | |
: tag === "SELECT" ? FLAG_HTML_ELEMENT | FLAG_SELECT_ELEMENT | |
: tag === "OPTION" ? FLAG_HTML_ELEMENT | FLAG_OPTION_ELEMENT | |
: tag === "TEXTAREA" ? FLAG_HTML_ELEMENT | FLAG_TEXTAREA_ELEMENT | |
: FLAG_HTML_ELEMENT | |
)) | |
); | |
/* eslint-enable indent */ | |
if (is) element.setAttribute("is", is); | |
} | |
currentParent = element; | |
currentNamespace = ns; | |
} else { | |
vnode.s = old.s; | |
oldAttrs = old.a; | |
currentNamespace = (currentParent = element = vnode.d = old.d).namespaceURI; | |
if (currentNamespace === htmlNs) currentNamespace = null; | |
} | |
currentRefNode = null; | |
try { | |
if (oldAttrs != null && oldAttrs === attrs) { | |
throw new Error("Attributes object cannot be reused.") | |
} | |
if (attrs != null) { | |
// The DOM does things to inputs based on the value, so it needs set first. | |
// See: https://github.com/MithrilJS/mithril.js/issues/2622 | |
if (mask & FLAG_INPUT_ELEMENT && attrs.type != null) { | |
if (attrs.type === "file") mask |= FLAG_IS_FILE_INPUT; | |
element.type = attrs.type; | |
} | |
for (var key in attrs) { | |
setAttr(vnode, element, mask, key, oldAttrs, attrs); | |
} | |
} | |
for (var key in oldAttrs) { | |
mask |= FLAG_IS_REMOVE; | |
setAttr(vnode, element, mask, key, oldAttrs, attrs); | |
} | |
} catch (e) { | |
return handleAttributeError(old, e, true) | |
} | |
updateFragment(old, vnode); | |
if (mask & FLAG_SELECT_ELEMENT && old == null) { | |
try { | |
// This does exactly what I want, so I'm reusing it to save some code | |
var normalized = getStyleKey(attrs, "value"); | |
if ("value" in attrs) { | |
if (normalized === null) { | |
if (element.selectedIndex >= 0) { | |
element.value = null; | |
} | |
} else { | |
if (element.selectedIndex < 0 || element.value !== normalized) { | |
element.value = normalized; | |
} | |
} | |
} | |
} catch (e) { | |
handleAttributeError(old, e, false); | |
} | |
try { | |
// This does exactly what I want, so I'm reusing it to save some code | |
var normalized = getPropKey(attrs, "selectedIndex"); | |
if (normalized !== null) { | |
element.selectedIndex = normalized; | |
} | |
} catch (e) { | |
handleAttributeError(old, e, false); | |
} | |
} | |
currentParent = prevParent; | |
currentRefNode = element; | |
currentNamespace = prevNamespace; | |
}; | |
var updateComponent = (old, vnode) => { | |
try { | |
var attrs = vnode.a; | |
var tree, oldInstance, oldAttrs; | |
rendered: { | |
if (old != null) { | |
tree = old.s; | |
oldInstance = old.c; | |
oldAttrs = old.a; | |
} else if (typeof (tree = (vnode.s = vnode.t)(attrs, oldAttrs, currentContext)) !== "function") { | |
break rendered | |
} | |
tree = (vnode.s = tree)(attrs, oldAttrs, currentContext); | |
} | |
if (tree === vnode) { | |
throw new Error("A view cannot return the vnode it received as argument") | |
} | |
tree = m.normalize(tree); | |
} catch (e) { | |
if (currentRemoveOnThrow) throw e | |
console.error(e); | |
return | |
} | |
updateNode(oldInstance, vnode.c = tree); | |
}; | |
var removeFragment = (old) => updateFragment(old, null); | |
var removeNode = (old) => { | |
try { | |
if (!old.d) return | |
old.d.remove(); | |
old.d = null; | |
} catch (e) { | |
console.error(e); | |
} | |
}; | |
// Replaces an otherwise necessary `switch`. | |
var updateNodeDispatch = [ | |
updateFragment, | |
updateFragment, | |
updateText, | |
updateElement, | |
updateComponent, | |
updateLayout, | |
updateRemove, | |
updateSet, | |
]; | |
var removeNodeDispatch = [ | |
removeFragment, | |
removeFragment, | |
removeNode, | |
(old) => { | |
removeNode(old); | |
updateFragment(old, null); | |
}, | |
(old) => updateNode(old.c, null), | |
() => {}, | |
(old) => currentHooks.push(old), | |
removeFragment, | |
]; | |
//attrs | |
/* eslint-disable no-unused-vars */ | |
var ASCII_COLON = 0x3A; | |
var ASCII_LOWER_E = 0x65; | |
var ASCII_LOWER_F = 0x66; | |
var ASCII_LOWER_H = 0x68; | |
var ASCII_LOWER_I = 0x69; | |
var ASCII_LOWER_K = 0x6B; | |
var ASCII_LOWER_L = 0x6C; | |
var ASCII_LOWER_M = 0x6D; | |
var ASCII_LOWER_N = 0x6E; | |
var ASCII_LOWER_O = 0x6F; | |
var ASCII_LOWER_P = 0x70; | |
var ASCII_LOWER_R = 0x72; | |
var ASCII_LOWER_S = 0x73; | |
var ASCII_LOWER_T = 0x74; | |
var ASCII_LOWER_X = 0x78; | |
var ASCII_LOWER_Y = 0x79; | |
/* eslint-enable no-unused-vars */ | |
var getPropKey = (host, key) => { | |
if (host != null && hasOwn.call(host, key)) { | |
var value = host[key]; | |
if (value !== false && value != null) return value | |
} | |
return null | |
}; | |
var getStyleKey = (host, key) => { | |
if (host != null && hasOwn.call(host, key)) { | |
var value = host[key]; | |
if (value !== false && value != null) return `${value}` | |
} | |
return null | |
}; | |
var uppercaseRegex = /[A-Z]/g; | |
var toLowerCase = (capital) => "-" + capital.toLowerCase(); | |
var normalizeKey = (key) => ( | |
key.startsWith("--") ? key : | |
key === "cssFloat" ? "float" : | |
key.replace(uppercaseRegex, toLowerCase) | |
); | |
var setStyle = (style, old, value, add) => { | |
for (var propName of Object.keys(value)) { | |
var propValue = getStyleKey(value, propName); | |
if (propValue !== null) { | |
var oldValue = getStyleKey(old, propName); | |
if (add) { | |
if (propValue !== oldValue) style.setProperty(normalizeKey(propName), propValue); | |
} else { | |
if (oldValue === null) style.removeProperty(normalizeKey(propName)); | |
} | |
} | |
} | |
}; | |
/* | |
Edit this with extreme caution, and profile any change you make. | |
Not only is this itself a hot spot (it comprises about 3-5% of runtime overhead), but the way it's | |
compiled can even sometimes have knock-on performance impacts elsewhere. Per some Turbolizer | |
experiments, this will generate around 10-15 KiB of assembly in its final optimized form. | |
Some of the optimizations it does: | |
- For pairs of attributes, I pack them into two integers so I can compare them in | |
parallel. | |
- I reuse the same character loads for `xlink:*` and `on*` to check for other nodes. I do not reuse | |
the last load, as the first 2 characters is usually enough just on its own to know if a special | |
attribute name is matchable. | |
- For small attribute names (4 characters or less), the code handles them in full, with no full | |
string comparison. | |
- I fuse all the conditions, `hasOwn` and existence checks, and all the add/remove logic into just | |
this, to reduce startup overhead and keep outer loop code size down. | |
- I use a lot of labels to reuse as much code as possible, and thus more ICs, to make optimization | |
easier and better-informed. | |
- Bit flags are used extensively here to merge as many comparisons as possible. This function is | |
actually the real reason why I'm using bit flags for stuff like `<input type="file">` in the | |
first place - it moves the check to just the create flow where it's only done once. | |
*/ | |
var setAttr = (vnode, element, mask, key, old, attrs) => { | |
try { | |
var newValue = getPropKey(attrs, key); | |
var oldValue = getPropKey(old, key); | |
if (mask & FLAG_IS_REMOVE && newValue !== null) return | |
forceSetAttribute: { | |
forceTryProperty: { | |
skipValueDiff: { | |
if (key.length > 1) { | |
var pair1 = key.charCodeAt(0) | key.charCodeAt(1) << 16; | |
if (key.length === 2 && pair1 === (ASCII_LOWER_I | ASCII_LOWER_S << 16)) { | |
return | |
} else if (pair1 === (ASCII_LOWER_O | ASCII_LOWER_N << 16)) { | |
if (newValue === oldValue) return | |
// Update the event | |
if (typeof newValue === "function") { | |
if (typeof oldValue !== "function") { | |
if (vnode.s == null) vnode.s = new EventDict(); | |
element.addEventListener(key.slice(2), vnode.s); | |
} | |
// Save this, so the current redraw is correctly tracked. | |
vnode.s._ = currentRedraw; | |
vnode.s.set(key, newValue); | |
} else if (typeof oldValue === "function") { | |
element.removeEventListener(key.slice(2), vnode.s); | |
vnode.s.delete(key); | |
} | |
return | |
} else if (key.length > 3) { | |
var pair2 = key.charCodeAt(2) | key.charCodeAt(3) << 16; | |
if ( | |
key.length > 6 && | |
pair1 === (ASCII_LOWER_X | ASCII_LOWER_L << 16) && | |
pair2 === (ASCII_LOWER_I | ASCII_LOWER_N << 16) && | |
(key.charCodeAt(4) | key.charCodeAt(5) << 16) === (ASCII_LOWER_K | ASCII_COLON << 16) | |
) { | |
key = key.slice(6); | |
if (newValue !== null) { | |
element.setAttributeNS(xlinkNs, key, newValue); | |
} else { | |
element.removeAttributeNS(xlinkNs, key); | |
} | |
return | |
} else if (key.length === 4) { | |
if ( | |
pair1 === (ASCII_LOWER_T | ASCII_LOWER_Y << 16) && | |
pair2 === (ASCII_LOWER_P | ASCII_LOWER_E << 16) | |
) { | |
if (!(mask & FLAG_INPUT_ELEMENT)) break skipValueDiff | |
if (newValue === null) break forceSetAttribute | |
break forceTryProperty | |
} else if ( | |
// Try to avoid a few browser bugs on normal elements. | |
pair1 === (ASCII_LOWER_H | ASCII_LOWER_R << 16) && pair2 === (ASCII_LOWER_E | ASCII_LOWER_F << 16) || | |
pair1 === (ASCII_LOWER_L | ASCII_LOWER_I << 16) && pair2 === (ASCII_LOWER_S | ASCII_LOWER_T << 16) || | |
pair1 === (ASCII_LOWER_F | ASCII_LOWER_O << 16) && pair2 === (ASCII_LOWER_R | ASCII_LOWER_M << 16) | |
) { | |
// If it's a custom element, just keep it. Otherwise, force the attribute | |
// to be set. | |
if (!(mask & FLAG_CUSTOM_ELEMENT)) { | |
break forceSetAttribute | |
} | |
} | |
} else if (key.length > 4) { | |
switch (key) { | |
case "children": | |
return | |
case "class": | |
case "className": | |
case "title": | |
if (newValue === null) break forceSetAttribute | |
break forceTryProperty | |
case "value": | |
if ( | |
// Filter out non-HTML keys and custom elements | |
(mask & (FLAG_HTML_ELEMENT | FLAG_CUSTOM_ELEMENT)) !== FLAG_HTML_ELEMENT || | |
!(key in element) | |
) { | |
break | |
} | |
if (newValue === null) { | |
if (mask & (FLAG_OPTION_ELEMENT | FLAG_SELECT_ELEMENT)) { | |
break forceSetAttribute | |
} else { | |
break forceTryProperty | |
} | |
} | |
if (!(mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT | FLAG_SELECT_ELEMENT | FLAG_OPTION_ELEMENT))) { | |
break | |
} | |
// It's always stringified, so it's okay to always coerce | |
if (element.value === (newValue = `${newValue}`)) { | |
// Setting `<input type="file" value="...">` to the same value causes an | |
// error to be generated if it's non-empty | |
if (mask & FLAG_IS_FILE_INPUT) return | |
// Setting `<input value="...">` to the same value by typing on focused | |
// element moves cursor to end in Chrome | |
if (mask & (FLAG_INPUT_ELEMENT | FLAG_TEXTAREA_ELEMENT)) { | |
if (element === currentDocument.activeElement) return | |
} else { | |
if (oldValue != null && oldValue !== false) return | |
} | |
} | |
if (mask & FLAG_IS_FILE_INPUT) { | |
//setting input[type=file][value] to different value is an error if it's non-empty | |
// Not ideal, but it at least works around the most common source of uncaught exceptions for now. | |
if (newValue !== "") { | |
console.error("File input `value` attributes must either mirror the current value or be set to the empty string (to reset)."); | |
return | |
} | |
} | |
break forceTryProperty | |
case "style": | |
if (oldValue === newValue) { | |
// Styles are equivalent, do nothing. | |
} else if (newValue === null) { | |
// New style is missing, just clear it. | |
element.style = ""; | |
} else if (typeof newValue !== "object") { | |
// New style is a string, let engine deal with patching. | |
element.style = newValue; | |
} else if (oldValue === null || typeof oldValue !== "object") { | |
// `old` is missing or a string, `style` is an object. | |
element.style = ""; | |
// Add new style properties | |
setStyle(element.style, null, newValue, true); | |
} else { | |
// Both old & new are (different) objects, or `old` is missing. | |
// Update style properties that have changed, or add new style properties | |
setStyle(element.style, oldValue, newValue, true); | |
// Remove style properties that no longer exist | |
setStyle(element.style, newValue, oldValue, false); | |
} | |
return | |
case "selected": | |
var active = currentDocument.activeElement; | |
if ( | |
element === active || | |
mask & FLAG_OPTION_ELEMENT && element.parentNode === active | |
) { | |
break | |
} | |
// falls through | |
case "checked": | |
case "selectedIndex": | |
break skipValueDiff | |
// Try to avoid a few browser bugs on normal elements. | |
case "width": | |
case "height": | |
// If it's a custom element, just keep it. Otherwise, force the attribute | |
// to be set. | |
if (!(mask & FLAG_CUSTOM_ELEMENT)) { | |
break forceSetAttribute | |
} | |
} | |
} | |
} | |
} | |
if (newValue !== null && typeof newValue !== "object" && oldValue === newValue) return | |
} | |
// Filter out namespaced keys | |
if (!(mask & FLAG_HTML_ELEMENT)) { | |
break forceSetAttribute | |
} | |
} | |
// Filter out namespaced keys | |
// Defer the property check until *after* we check everything. | |
if (key in element) { | |
element[key] = newValue; | |
return | |
} | |
} | |
if (newValue === null) { | |
if (oldValue !== null) element.removeAttribute(key); | |
} else { | |
element.setAttribute(key, newValue === true ? "" : newValue); | |
} | |
} catch (e) { | |
handleAttributeError(old, e, false); | |
} | |
}; | |
// Here's an explanation of how this works: | |
// 1. The event names are always (by design) prefixed by `on`. | |
// 2. The EventListener interface accepts either a function or an object with a `handleEvent` method. | |
// 3. The object inherits from `Map`, to avoid hitting global setters. | |
// 4. The event name is remapped to the handler before calling it. | |
// 5. In function-based event handlers, `ev.currentTarget === this`. We replicate that below. | |
// 6. In function-based event handlers, `return false` prevents the default action and stops event | |
// propagation. Instead of that, we hijack it to control implicit redrawing, and let users | |
// return a promise that resolves to it. | |
class EventDict extends Map { | |
async handleEvent(ev) { | |
var handler = this.get(`on${ev.type}`); | |
if (typeof handler === "function") { | |
var result = handler.call(ev.currentTarget, ev); | |
if (result === false) return | |
if (result && typeof result.then === "function" && (await result) === false) return | |
(0, this._)(); | |
} | |
} | |
} | |
//event | |
var currentlyRendering = []; | |
m.render = (dom, vnode, {redraw, removeOnThrow} = {}) => { | |
if (!dom) throw new TypeError("DOM element being rendered to does not exist.") | |
if (currentlyRendering.some((d) => d === dom || d.contains(dom))) { | |
throw new TypeError("Node is currently being rendered to and thus is locked.") | |
} | |
if (redraw != null && typeof redraw !== "function") { | |
throw new TypeError("Redraw must be a function if given.") | |
} | |
var active = dom.ownerDocument.activeElement; | |
var namespace = dom.namespaceURI; | |
var prevHooks = currentHooks; | |
var prevRedraw = currentRedraw; | |
var prevParent = currentParent; | |
var prevRefNode = currentRefNode; | |
var prevNamespace = currentNamespace; | |
var prevDocument = currentDocument; | |
var prevContext = currentContext; | |
var prevRemoveOnThrow = currentRemoveOnThrow; | |
var hooks = currentHooks = []; | |
try { | |
currentlyRendering.push(currentParent = dom); | |
currentRedraw = typeof redraw === "function" ? redraw : null; | |
currentRefNode = null; | |
currentNamespace = namespace === htmlNs ? null : namespace; | |
currentDocument = dom.ownerDocument; | |
currentContext = {redraw}; | |
// eslint-disable-next-line no-implicit-coercion | |
currentRemoveOnThrow = !!removeOnThrow; | |
// First time rendering into a node clears it out | |
if (dom.vnodes == null) dom.textContent = ""; | |
updateNode(dom.vnodes, vnode = m.normalize(vnode)); | |
dom.vnodes = vnode; | |
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement | |
if (active != null && currentDocument.activeElement !== active && typeof active.focus === "function") { | |
active.focus(); | |
} | |
for (var {s, d} of hooks) { | |
try { | |
s(d); | |
} catch (e) { | |
console.error(e); | |
} | |
} | |
} finally { | |
currentRedraw = prevRedraw; | |
currentHooks = prevHooks; | |
currentParent = prevParent; | |
currentRefNode = prevRefNode; | |
currentNamespace = prevNamespace; | |
currentDocument = prevDocument; | |
currentContext = prevContext; | |
currentRemoveOnThrow = prevRemoveOnThrow; | |
currentlyRendering.pop(); | |
} | |
}; | |
m.mount = (root, view) => { | |
if (!root) throw new TypeError("Root must be an element") | |
if (typeof view !== "function") { | |
throw new TypeError("View must be a function") | |
} | |
var window = root.ownerDocument.defaultView; | |
var id = 0; | |
var unschedule = () => { | |
if (id) { | |
window.cancelAnimationFrame(id); | |
id = 0; | |
} | |
}; | |
var redraw = () => { if (!id) id = window.requestAnimationFrame(redraw.sync); }; | |
var Mount = (_, old) => [ | |
m.remove(unschedule), | |
view(!old, redraw) | |
]; | |
redraw.sync = () => { | |
unschedule(); | |
m.render(root, m(Mount), {redraw}); | |
}; | |
m.render(root, null); | |
redraw.sync(); | |
return redraw | |
}; | |
/* global window: false */ | |
var WithRouter = ({prefix, initial: href}) => { | |
if (prefix == null) prefix = ""; | |
if (typeof prefix !== "string") { | |
throw new TypeError("The route prefix must be a string if given") | |
} | |
var mustReplace, redraw, currentUrl, currentPath; | |
var updateRouteWithHref = () => { | |
var url = new URL(href); | |
var urlPath = url.pathname + url.search + url.hash; | |
var decodedPrefix = prefix; | |
var index = urlPath.indexOf(decodedPrefix); | |
if (index < 0) index = urlPath.indexOf(decodedPrefix = encodeURI(decodedPrefix)); | |
if (index >= 0) urlPath = urlPath.slice(index + decodedPrefix.length); | |
if (urlPath[0] !== "/") urlPath = `/${urlPath}`; | |
currentUrl = new URL(urlPath, href); | |
currentPath = decodeURI(currentUrl.pathname); | |
mustReplace = false; | |
}; | |
var updateRoute = () => { | |
if (href === window.location.href) return | |
href = window.location.href; | |
var prevUrl = currentUrl; | |
updateRouteWithHref(); | |
if (currentUrl.href !== prevUrl.href) redraw(); | |
}; | |
var set = (path, {replace, state} = {}) => { | |
if (mustReplace) replace = true; | |
mustReplace = true; | |
void (async () => { | |
await 0; // wait for next microtask | |
updateRoute(); | |
})(); | |
redraw(); | |
if (typeof window === "object") { | |
window.history[replace ? "replaceState" : "pushState"](state, "", prefix + path); | |
} | |
}; | |
if (!href) { | |
if (typeof window !== "object") { | |
throw new TypeError("Outside the DOM, `href` must be set") | |
} | |
href = window.location.href; | |
window.addEventListener("popstate", updateRoute); | |
} | |
updateRouteWithHref(); | |
return ({children}, _, context) => { | |
redraw = context.redraw; | |
return [ | |
m.remove(() => window.removeEventListener("popstate", updateRoute)), | |
m.set({ | |
route: { | |
prefix, | |
path: currentPath, | |
params: currentUrl.searchParams, | |
current: currentPath + currentUrl.search + currentUrl.hash, | |
set, | |
}, | |
}, children), | |
] | |
} | |
}; | |
// Let's provide a *right* way to manage a route link, rather than letting people screw up | |
// accessibility on accident. | |
// | |
// Note: this does *not* support disabling. Instead, consider more accessible alternatives like not | |
// showing the link in the first place. If you absolutely have to disable the link, disable it by | |
// removing this component (like via `m("div", {disabled}, !disabled && m(Link))`). There's | |
// friction here for a reason. | |
var Link = () => { | |
var opts, setRoute; | |
var listener = (ev) => { | |
// Adapted from React Router's implementation: | |
// https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js | |
// | |
// Try to be flexible and intuitive in how we handle links. | |
// Fun fact: links aren't as obvious to get right as you | |
// would expect. There's a lot more valid ways to click a | |
// link than this, and one might want to not simply click a | |
// link, but right click or command-click it to copy the | |
// link target, etc. Nope, this isn't just for blind people. | |
if ( | |
// Skip if `onclick` prevented default | |
!ev.defaultPrevented && | |
// Ignore everything but left clicks | |
(ev.button === 0 || ev.which === 0 || ev.which === 1) && | |
// Let the browser handle `target=_blank`, etc. | |
(!ev.currentTarget.target || ev.currentTarget.target === "_self") && | |
// No modifier keys | |
!ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey | |
) { | |
setRoute(opts.href, opts); | |
// Capture the event, and don't double-call `redraw`. | |
return m.capture(ev) | |
} | |
}; | |
return (attrs, old, {route: {prefix, set}}) => { | |
setRoute = set; | |
opts = attrs; | |
return [ | |
m.layout((dom) => { | |
dom.href = prefix + opts.href; | |
if (!old) dom.addEventListener("click", listener); | |
}), | |
m.remove((dom) => { | |
dom.removeEventListener("click", listener); | |
}), | |
] | |
} | |
}; | |
/* global performance, setTimeout, clearTimeout */ | |
var validateDelay = (delay) => { | |
if (!Number.isFinite(delay) || delay <= 0) { | |
throw new RangeError("Timer delay must be finite and positive") | |
} | |
}; | |
var rateLimiterImpl = (delay = 500, isThrottler) => { | |
validateDelay(delay); | |
var closed = false; | |
var start = 0; | |
var timer = 0; | |
var resolveNext; | |
var callback = () => { | |
timer = undefined; | |
if (typeof resolveNext === "function") { | |
resolveNext(false); | |
resolveNext = undefined; | |
} | |
}; | |
var rateLimiter = async (ignoreLeading) => { | |
if (closed) { | |
return true | |
} | |
if (typeof resolveNext === "function") { | |
resolveNext(true); | |
resolveNext = null; | |
} | |
if (timer) { | |
if (isThrottler) { | |
return new Promise((resolve) => resolveNext = resolve) | |
} | |
clearTimeout(timer); | |
ignoreLeading = true; | |
} | |
start = performance.now(); | |
timer = setTimeout(callback, delay); | |
if (!ignoreLeading) { | |
return | |
} | |
return new Promise((resolve) => resolveNext = resolve) | |
}; | |
rateLimiter.update = (newDelay) => { | |
validateDelay(newDelay); | |
delay = newDelay; | |
if (closed) return | |
if (timer) { | |
clearTimeout(timer); | |
timer = setTimeout(callback, (start - performance.now()) + delay); | |
} | |
}; | |
rateLimiter.dispose = () => { | |
if (closed) return | |
closed = true; | |
clearTimeout(timer); | |
if (typeof resolveNext === "function") { | |
resolveNext(true); | |
resolveNext = null; | |
} | |
}; | |
return rateLimiter | |
}; | |
/** | |
* A general-purpose bi-edge throttler, with a dynamically configurable limit. It's much better | |
* than your typical `throttle(f, ms)` because it lets you easily separate the trigger and reaction | |
* using a single shared, encapsulated state object. That same separation is also used to make the | |
* rate limit dynamically reconfigurable on hit. | |
* | |
* Create as `throttled = m.throttler(ms)` and do `if (await throttled()) return` to rate-limit | |
* the code that follows. The result is one of three values, to allow you to identify edges: | |
* | |
* - Leading edge: `undefined` | |
* - Trailing edge: `false`, returned only if a second call was made | |
* - No edge: `true` | |
* | |
* Call `throttled.update(ms)` to update the interval. This not only impacts future delays, but also any current one. | |
* | |
* To dispose, like on component removal, call `throttled.dispose()`. | |
* | |
* If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for | |
* most needs. There is no default for `throttled.update(...)` - you must specify one explicitly. | |
* | |
* Example usage: | |
* | |
* ```js | |
* const throttled = m.throttler() | |
* let results, error | |
* return (_attrs, _old, {redraw}) => [ | |
* m.remove(throttled.dispose), | |
* m("input[type=search]", { | |
* async oninput(ev) { | |
* if (await throttled()) return false // Skip redraw if rate limited - it's pointless | |
* error = results = null | |
* redraw() | |
* try { | |
* const response = await fetch(m.p("/search", {q: ev.target.value})) | |
* if (response.ok) { | |
* results = await response.json() | |
* } else { | |
* error = await response.text() | |
* } | |
* } catch (e) { | |
* error = e.message | |
* } | |
* }, | |
* }), | |
* results.map((result) => m(SearchResult, {result})), | |
* !error || m(ErrorDisplay, {error})), | |
* ] | |
* ``` | |
* | |
* Important note: due to the way this is implemented in basically all runtimes, the throttler's | |
* clock might not tick during sleep, so if you do `await throttled()` and immediately sleep in a | |
* low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a | |
* high-power state. | |
*/ | |
var throttler = (delay) => rateLimiterImpl(delay, 1); | |
/** | |
* A general-purpose bi-edge debouncer, with a dynamically configurable limit. It's much better | |
* than your typical `debounce(f, ms)` because it lets you easily separate the trigger and reaction | |
* using a single shared, encapsulated state object. That same separation is also used to make the | |
* rate limit dynamically reconfigurable on hit. | |
* | |
* Create as `debounced = m.debouncer(ms)` and do `if (await debounced()) return` to rate-limit | |
* the code that follows. The result is one of three values, to allow you to identify edges: | |
* | |
* - Leading edge: `undefined` | |
* - Trailing edge: `false`, returned only if a second call was made | |
* - No edge: `true` | |
* | |
* Call `debounced.update(ms)` to update the interval. This not only impacts future delays, but also any current one. | |
* | |
* To dispose, like on component removal, call `debounced.dispose()`. | |
* | |
* If you don't sepecify a delay, it defaults to 500ms on creation, which works well enough for | |
* most needs. There is no default for `debounced.update(...)` - you must specify one explicitly. | |
* | |
* Example usage: | |
* | |
* ```js | |
* const debounced = m.debouncer() | |
* let results, error | |
* return (attrs, _, {redraw}) => [ | |
* m.remove(debounced.dispose), | |
* m("input[type=text].value", { | |
* async oninput(ev) { | |
* if ((await debounced()) !== false) return | |
* try { | |
* const response = await fetch(m.p("/save/:id", {id: attrs.id}), { | |
* body: JSON.stringify({value: ev.target.value}), | |
* }) | |
* if (!response.ok) { | |
* error = await response.text() | |
* } | |
* } catch (e) { | |
* error = e.message | |
* } | |
* }, | |
* }), | |
* results.map((result) => m(SearchResult, {result})), | |
* !error || m(ErrorDisplay, {error})), | |
* ] | |
* ``` | |
* | |
* Important note: due to the way this is implemented in basically all runtimes, the debouncer's | |
* clock might not tick during sleep, so if you do `await debounced()` and immediately sleep in a | |
* low-power state for 5 minutes, you might have to wait another 10 minutes after resuming to a | |
* high-power state. | |
*/ | |
var debouncer = (delay) => rateLimiterImpl(delay, 0); | |
var toString = {}.toString; | |
var serializeQueryValue = (key, value) => { | |
if (value == null || value === false) { | |
return "" | |
} else if (Array.isArray(value)) { | |
return value.map((i) => serializeQueryValue(`${key}[]`, i)).join("&") | |
} else if (toString.call(value) !== "[object Object]") { | |
return `${encodeURIComponent(key)}${value === true ? "" : `=${encodeURIComponent(value)}`}` | |
} else { | |
return Object.entries(value).map(([k, v]) => serializeQueryValue(`${key}[${k}]`, v)).join("&") | |
} | |
}; | |
var q = (params) => Object.entries(params).map(([k, v]) => serializeQueryValue(k, v)).join("&"); | |
var invalidTemplateChars = /:([^\/\.-]+)(\.{3})?:/; | |
// Returns `path` from `template` + `params` | |
var p = (template, params) => { | |
if (invalidTemplateChars.test(template)) { | |
throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.") | |
} | |
if (params == null) return template | |
var queryIndex = template.indexOf("?"); | |
var hashIndex = template.indexOf("#"); | |
var queryEnd = hashIndex < 0 ? template.length : hashIndex; | |
var pathEnd = queryIndex < 0 ? queryEnd : queryIndex; | |
var path = template.slice(0, pathEnd); | |
var query = Object.assign({}, params); | |
var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, (m, key, variadic) => { | |
delete query[key]; | |
// If no such parameter exists, don't interpolate it. | |
if (params[key] == null) return m | |
// Escape normal parameters, but not variadic ones. | |
return variadic ? params[key] : encodeURIComponent(String(params[key])) | |
}); | |
// In case the template substitution adds new query/hash parameters. | |
var newQueryIndex = resolved.indexOf("?"); | |
var newHashIndex = resolved.indexOf("#"); | |
var newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex; | |
var newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex; | |
var result = resolved.slice(0, newPathEnd); | |
if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd); | |
if (newQueryIndex >= 0) result += (queryIndex < 0 ? "?" : "&") + resolved.slice(newQueryIndex, newQueryEnd); | |
var querystring = q(query); | |
if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? "?" : "&") + querystring; | |
if (hashIndex >= 0) result += template.slice(hashIndex); | |
if (newHashIndex >= 0) result += (hashIndex < 0 ? "" : "&") + resolved.slice(newHashIndex); | |
return result | |
}; | |
var Init = ({f}, old, {redraw}) => { | |
if (old) return m.retain() | |
var ctrl = new AbortController(); | |
void (async () => { | |
await 0; // wait for next microtask | |
if ((await f(ctrl.signal)) !== false) redraw(); | |
})(); | |
return m.remove(() => ctrl.abort()) | |
}; | |
var init = (f) => m(Init, {f}); | |
var lazy = (opts) => { | |
// Capture the error here so stack traces make more sense | |
var error = new ReferenceError("Component not found"); | |
var redraws = new Set(); | |
var Comp = (_, __, context) => { | |
redraws.add(context.redraw); | |
return opts.pending && opts.pending() | |
}; | |
var init = async () => { | |
try { | |
Comp = await opts.fetch(); | |
if (typeof Comp !== "function") { | |
Comp = Comp.default; | |
if (typeof Comp !== "function") throw error | |
} | |
} catch (e) { | |
console.error(e); | |
Comp = () => opts.error && opts.error(e); | |
} | |
var r = redraws; | |
redraws = null; | |
for (var f of r) f(); | |
}; | |
return (attrs) => { | |
var f = init; | |
init = null; | |
if (typeof f === "function") f(); | |
return m(Comp, attrs) | |
} | |
}; | |
/* | |
Here's the intent. | |
- Usage in model: | |
- List | |
- Get | |
- Track | |
- Delete | |
- Replace (equivalent to delete + track) | |
- Usage in view: | |
- Iterate live handles | |
- Release aborted live handles that no longer needed | |
Models can do basic CRUD operations on the collection. | |
- They can list what's currently there. | |
- They can get a current value. | |
- They can set the current value. | |
- They can delete the current value. | |
- They can replace the current value, deleting a value that's already there. | |
In the view, they use handles to abstract over the concept of a key. Duplicates are theoretically | |
possible, so they should use the handle itself as the key for `m.key(...)`. It might look something | |
like this: | |
```js | |
return t.live().map((handle) => ( | |
m.key(handle, m(Entry, { | |
name: handle.key, | |
value: handle.value, | |
removed: handle.signal.aborted, | |
onremovaltransitionended: () => handle.release(), | |
})) | |
)) | |
``` | |
There used to be an in-renderer way to manage this transparently, but there's a couple big reasons | |
why that was removed in favor of this: | |
1. It's very complicated to get right. Like, the majority of the removal code was related to it. In | |
fact, this module is considerably smaller than the code that'd have to go into the renderer to | |
support it, as this isn't nearly as perf-sensitive as that. | |
2. When you need to remove something asynchronously, there's multiple ways you may want to manage | |
transitions. You might want to stagger them. You might want to do them all at once. You might | |
want to clear some state and not other state. You might want to preserve some elements of a | |
sibling's state. Embedding it in the renderer would force an opinion on you, and in order to | |
work around it, you'd have to do something like this anyways. | |
*/ | |
/** | |
* @template K, V | |
* @typedef TrackedHandle | |
* | |
* @property {K} key | |
* @property {V} value | |
* @property {AbortSignal} signal | |
* @property {() => void} release | |
*/ | |
/** | |
* @template K, V | |
* @typedef Tracked | |
* | |
* @property {() => Array<TrackedHandle<K, V>>} live | |
* @property {() => Array<[K, V]>} list | |
* @property {(key: K) => boolean} has | |
* @property {(key: K) => undefined | V} get | |
* @property {(key: K, value: V) => void} track | |
* @property {(key: K, value: V) => void} replace | |
* @property {(key: K) => boolean} delete | |
*/ | |
/** | |
* @template K, V | |
* @param {Iterable<[K, V]>} [initial] | |
* @param {() => void} redraw | |
* @returns {Tracked<K, V>} | |
*/ | |
var tracked = (redraw, initial) => { | |
/** @type {Map<K, TrackedHandle<K, V> & {_: AbortController}>} */ var state = new Map(); | |
/** @type {Set<TrackedHandle<K, V>>} */ var live = new Set(); | |
var abort = (prev) => { | |
try { | |
if (prev) { | |
if (prev._) prev._.abort(); | |
else live.delete(prev); | |
} | |
} catch (e) { | |
console.error(e); | |
} | |
}; | |
// Bit 1 forcibly releases the old handle, and bit 2 causes an update notification to be sent | |
// (something that's unwanted during initialization). | |
var setHandle = (k, v, bits) => { | |
var prev = state.get(k); | |
var ctrl = new AbortController(); | |
/** @type {TrackedHandle<K, V>} */ | |
var handle = { | |
_: ctrl, | |
key: k, | |
value: v, | |
signal: ctrl.signal, | |
release() { | |
if (state.get(handle.key) === handle) { | |
handle._ = null; | |
} else if (live.delete(handle)) { | |
redraw(); | |
} | |
}, | |
}; | |
state.set(k, handle); | |
live.add(handle); | |
// eslint-disable-next-line no-bitwise | |
if (bits & 1) live.delete(prev); | |
abort(prev); | |
// eslint-disable-next-line no-bitwise | |
if (bits & 2) redraw(); | |
}; | |
for (var [k, v] of initial || []) setHandle(k, v, 1); | |
return { | |
live: () => [...live], | |
list: () => Array.from(state.values(), (h) => [h.key, h.value]), | |
has: (k) => state.has(k), | |
get: (k) => (k = state.get(k)) && k.value, | |
set: (k, v) => setHandle(k, v, 3), | |
replace: (k, v) => setHandle(k, v, 2), | |
delete(k) { | |
var prev = state.get(k); | |
var result = state.delete(k); | |
abort(prev); | |
redraw(); | |
return result | |
}, | |
} | |
}; | |
var Use = () => { | |
var key = 0; | |
return (n, o) => { | |
if (o && !( | |
n.d.length === o.d.length && | |
n.d.every((b, i) => Object.is(b, o.d[i])) | |
)) { | |
key++; | |
} | |
return m.key(key, n.children) | |
} | |
}; | |
var use = (deps, ...children) => m(Use, {d: [...deps]}, ...children); | |
/** | |
* @param {ReadableStream<Uint8Array> | null} source | |
* @param {(current: number) => void} notify | |
*/ | |
var withProgress = (source, notify) => { | |
var reader = source && source.getReader(); | |
var current = 0; | |
return new ReadableStream({ | |
type: "bytes", | |
start: (ctrl) => reader || ctrl.close(), | |
cancel: (reason) => reader.cancel(reason), | |
async pull(ctrl) { | |
var result = await reader.read(); | |
if (result.done) { | |
ctrl.close(); | |
} else { | |
current += result.value.length; | |
ctrl.enqueue(result.value); | |
notify(current); | |
} | |
}, | |
}) | |
}; | |
m.WithRouter = WithRouter; | |
m.Link = Link; | |
m.p = p; | |
m.q = q; | |
m.withProgress = withProgress; | |
m.lazy = lazy; | |
m.init = init; | |
m.use = use; | |
m.tracked = tracked; | |
m.throttler = throttler; | |
m.debouncer = debouncer; | |
/* global module: false, window: false */ | |
if (typeof module !== "undefined") module.exports = m; | |
else window.m = m; | |
})(); | |
//# sourceMappingURL=mithril.umd.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment