tachyon-dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/dist/adapters/lambda.d.ts +65 -0
- package/dist/adapters/lambda.js +211 -0
- package/dist/adapters/node.d.ts +25 -0
- package/dist/adapters/node.js +360 -0
- package/dist/adapters/workers.d.ts +149 -0
- package/dist/adapters/workers.js +272 -0
- package/dist/adapters.d.ts +3 -0
- package/dist/adapters.js +3 -0
- package/dist/app.d.ts +98 -0
- package/dist/app.js +306 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +711 -0
- package/dist/compiler/expression.d.ts +55 -0
- package/dist/compiler/expression.js +858 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +50 -0
- package/dist/compiler/ir.d.ts +4 -0
- package/dist/compiler/ir.js +255 -0
- package/dist/compiler/parser.d.ts +3 -0
- package/dist/compiler/parser.js +236 -0
- package/dist/compiler/sfc.d.ts +33 -0
- package/dist/compiler/sfc.js +439 -0
- package/dist/compiler/targets/client.d.ts +3 -0
- package/dist/compiler/targets/client.js +568 -0
- package/dist/compiler/targets/server.d.ts +7 -0
- package/dist/compiler/targets/server.js +396 -0
- package/dist/compiler/targets/stream.d.ts +2 -0
- package/dist/compiler/targets/stream.js +204 -0
- package/dist/compiler/types.d.ts +185 -0
- package/dist/compiler/types.js +1 -0
- package/dist/compiler/utils.d.ts +40 -0
- package/dist/compiler/utils.js +185 -0
- package/dist/compiler/whitespace.d.ts +2 -0
- package/dist/compiler/whitespace.js +66 -0
- package/dist/compiler.d.ts +1 -0
- package/dist/compiler.js +1 -0
- package/dist/constant-time.d.ts +1 -0
- package/dist/constant-time.js +26 -0
- package/dist/cookies.d.ts +44 -0
- package/dist/cookies.js +199 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +45 -0
- package/dist/env.d.ts +27 -0
- package/dist/env.js +37 -0
- package/dist/html-escape.d.ts +2 -0
- package/dist/html-escape.js +20 -0
- package/dist/html-whitespace.d.ts +6 -0
- package/dist/html-whitespace.js +83 -0
- package/dist/i18n.d.ts +19 -0
- package/dist/i18n.js +93 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/language-server.d.ts +17 -0
- package/dist/language-server.js +79 -0
- package/dist/package-integrity.d.ts +9 -0
- package/dist/package-integrity.js +64 -0
- package/dist/result.d.ts +13 -0
- package/dist/result.js +4 -0
- package/dist/router-node.d.ts +2 -0
- package/dist/router-node.js +12 -0
- package/dist/router.d.ts +324 -0
- package/dist/router.js +1024 -0
- package/dist/runtime/attr.d.ts +3 -0
- package/dist/runtime/attr.js +66 -0
- package/dist/runtime/class.d.ts +2 -0
- package/dist/runtime/class.js +10 -0
- package/dist/runtime/conditional.d.ts +78 -0
- package/dist/runtime/conditional.js +140 -0
- package/dist/runtime/enhancement.d.ts +11 -0
- package/dist/runtime/enhancement.js +70 -0
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +27 -0
- package/dist/runtime/event.d.ts +1 -0
- package/dist/runtime/event.js +11 -0
- package/dist/runtime/form.d.ts +41 -0
- package/dist/runtime/form.js +167 -0
- package/dist/runtime/fragment.d.ts +6 -0
- package/dist/runtime/fragment.js +18 -0
- package/dist/runtime/html.d.ts +8 -0
- package/dist/runtime/html.js +6 -0
- package/dist/runtime/hydrate.d.ts +68 -0
- package/dist/runtime/hydrate.js +205 -0
- package/dist/runtime/keyed-rows.d.ts +61 -0
- package/dist/runtime/keyed-rows.js +192 -0
- package/dist/runtime/list.d.ts +84 -0
- package/dist/runtime/list.js +424 -0
- package/dist/runtime/portal.d.ts +6 -0
- package/dist/runtime/portal.js +12 -0
- package/dist/runtime/router.d.ts +102 -0
- package/dist/runtime/router.js +649 -0
- package/dist/runtime/signal.d.ts +30 -0
- package/dist/runtime/signal.js +275 -0
- package/dist/runtime/store.d.ts +1 -0
- package/dist/runtime/store.js +1 -0
- package/dist/runtime/stream-client.d.ts +8 -0
- package/dist/runtime/stream-client.js +33 -0
- package/dist/runtime/text.d.ts +2 -0
- package/dist/runtime/text.js +10 -0
- package/dist/runtime/virtual-list.d.ts +15 -0
- package/dist/runtime/virtual-list.js +89 -0
- package/dist/security.d.ts +27 -0
- package/dist/security.js +173 -0
- package/dist/server/form-action.d.ts +52 -0
- package/dist/server/form-action.js +102 -0
- package/dist/server/html.d.ts +19 -0
- package/dist/server/html.js +101 -0
- package/dist/server/stream.d.ts +4 -0
- package/dist/server/stream.js +72 -0
- package/dist/source-map.d.ts +17 -0
- package/dist/source-map.js +18 -0
- package/dist/tachyon-html.d.ts +110 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +36 -0
- package/dist/typed.d.ts +9 -0
- package/dist/typed.js +4 -0
- package/dist/vite.d.ts +84 -0
- package/dist/vite.js +426 -0
- package/package.json +604 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const setAttributeValue: (element: Element, name: string, value: unknown) => void;
|
|
2
|
+
export declare const setStyleValue: (element: Element, name: string, value: unknown) => void;
|
|
3
|
+
export declare const setRef: (scope: Record<string, unknown>, expression: string, element: Element) => void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const dangerousPropertyNames = new Set(["innerhtml", "outerhtml", "srcdoc"]);
|
|
2
|
+
const shouldReflectProperty = (name) => {
|
|
3
|
+
const normalized = name.toLowerCase();
|
|
4
|
+
return !normalized.startsWith("on") && !dangerousPropertyNames.has(normalized);
|
|
5
|
+
};
|
|
6
|
+
export const setAttributeValue = (element, name, value) => {
|
|
7
|
+
const reflectProperty = shouldReflectProperty(name);
|
|
8
|
+
if (value == null || value === false) {
|
|
9
|
+
element.removeAttribute(name);
|
|
10
|
+
if (reflectProperty && name in element) {
|
|
11
|
+
try {
|
|
12
|
+
const properties = element;
|
|
13
|
+
const current = properties[name];
|
|
14
|
+
if (typeof current === "boolean") {
|
|
15
|
+
properties[name] = false;
|
|
16
|
+
}
|
|
17
|
+
else if (name === "value" && typeof current === "string") {
|
|
18
|
+
properties[name] = "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Some readonly DOM properties throw on assignment.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!reflectProperty) {
|
|
28
|
+
element.removeAttribute(name);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (value === true) {
|
|
32
|
+
element.setAttribute(name, "");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
element.setAttribute(name, String(value));
|
|
36
|
+
}
|
|
37
|
+
if (name in element) {
|
|
38
|
+
try {
|
|
39
|
+
element[name] = value;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Some readonly DOM properties throw on assignment.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const setStyleValue = (element, name, value) => {
|
|
47
|
+
if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
element.style.setProperty(name, value == null || value === false ? "" : String(value));
|
|
51
|
+
};
|
|
52
|
+
export const setRef = (scope, expression, element) => {
|
|
53
|
+
const parts = expression.split(".");
|
|
54
|
+
let current = scope;
|
|
55
|
+
for (const part of parts.slice(0, -1)) {
|
|
56
|
+
const next = current[part];
|
|
57
|
+
if (next == null || typeof next !== "object") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
current = next;
|
|
61
|
+
}
|
|
62
|
+
const last = parts.at(-1);
|
|
63
|
+
if (last) {
|
|
64
|
+
current[last] = element;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const elementAt = (root, path) => {
|
|
2
|
+
let current = root;
|
|
3
|
+
for (const index of path) {
|
|
4
|
+
current = current.childNodes[index];
|
|
5
|
+
}
|
|
6
|
+
return current;
|
|
7
|
+
};
|
|
8
|
+
export const setClassPresence = (element, className, value) => {
|
|
9
|
+
element.classList.toggle(className, Boolean(value));
|
|
10
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
type TextBinding = {
|
|
2
|
+
kind: "text";
|
|
3
|
+
path: number[];
|
|
4
|
+
expression: string;
|
|
5
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
6
|
+
};
|
|
7
|
+
type ClassBinding = {
|
|
8
|
+
kind: "class";
|
|
9
|
+
path: number[];
|
|
10
|
+
className: string;
|
|
11
|
+
expression: string;
|
|
12
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
13
|
+
};
|
|
14
|
+
type EventBinding = {
|
|
15
|
+
kind: "event";
|
|
16
|
+
path: number[];
|
|
17
|
+
eventName: string;
|
|
18
|
+
handler: string;
|
|
19
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
20
|
+
};
|
|
21
|
+
type AttributeBinding = {
|
|
22
|
+
kind: "attr";
|
|
23
|
+
path: number[];
|
|
24
|
+
name: string;
|
|
25
|
+
expression: string;
|
|
26
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
27
|
+
};
|
|
28
|
+
type StyleBinding = {
|
|
29
|
+
kind: "style";
|
|
30
|
+
path: number[];
|
|
31
|
+
name: string;
|
|
32
|
+
expression: string;
|
|
33
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
34
|
+
};
|
|
35
|
+
type RefBinding = {
|
|
36
|
+
kind: "ref";
|
|
37
|
+
path: number[];
|
|
38
|
+
expression: string;
|
|
39
|
+
};
|
|
40
|
+
type ModelBinding = {
|
|
41
|
+
kind: "model";
|
|
42
|
+
path: number[];
|
|
43
|
+
property: "value" | "checked";
|
|
44
|
+
expression: string;
|
|
45
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
46
|
+
write?: (scope: Record<string, unknown>, value: unknown) => void;
|
|
47
|
+
};
|
|
48
|
+
type NestedListBinding = {
|
|
49
|
+
kind: "list";
|
|
50
|
+
signature?: string;
|
|
51
|
+
path: number[];
|
|
52
|
+
each: string;
|
|
53
|
+
key: string;
|
|
54
|
+
keyRead?: (scope: Record<string, unknown>) => unknown;
|
|
55
|
+
keyReadItem?: (item: unknown) => unknown;
|
|
56
|
+
itemName: string;
|
|
57
|
+
templateHtml: string;
|
|
58
|
+
bindings: ConditionalBinding[];
|
|
59
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
60
|
+
};
|
|
61
|
+
type NestedConditionalBinding = {
|
|
62
|
+
kind: "if";
|
|
63
|
+
signature?: string;
|
|
64
|
+
path: number[];
|
|
65
|
+
test: string;
|
|
66
|
+
templateHtml: string;
|
|
67
|
+
bindings: ConditionalBinding[];
|
|
68
|
+
read?: (scope: Record<string, unknown>) => unknown;
|
|
69
|
+
};
|
|
70
|
+
type ConditionalBinding = TextBinding | ClassBinding | EventBinding | AttributeBinding | StyleBinding | RefBinding | ModelBinding | NestedListBinding | NestedConditionalBinding;
|
|
71
|
+
export type ConditionalOptions = {
|
|
72
|
+
signature?: string;
|
|
73
|
+
templateHtml: string;
|
|
74
|
+
bindings: ConditionalBinding[];
|
|
75
|
+
};
|
|
76
|
+
export declare const nodeAt: (root: Node, path: readonly number[]) => Node;
|
|
77
|
+
export declare const mountConditional: (root: Node, path: readonly number[], visible: unknown, scope: Record<string, unknown>, options: ConditionalOptions) => void;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { elementAt, setClassPresence } from "./class.js";
|
|
2
|
+
import { setAttributeValue, setRef, setStyleValue } from "./attr.js";
|
|
3
|
+
import { delegate } from "./event.js";
|
|
4
|
+
import { bindControl, setControlValue } from "./form.js";
|
|
5
|
+
import { mountKeyedList } from "./list.js";
|
|
6
|
+
import { setText, textAt } from "./text.js";
|
|
7
|
+
const states = new WeakMap();
|
|
8
|
+
export const nodeAt = (root, path) => {
|
|
9
|
+
let current = root;
|
|
10
|
+
for (const index of path) {
|
|
11
|
+
current = current.childNodes[index];
|
|
12
|
+
}
|
|
13
|
+
return current;
|
|
14
|
+
};
|
|
15
|
+
const readPath = (scope, expression) => {
|
|
16
|
+
const parts = expression.split(".");
|
|
17
|
+
let current = scope;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (current == null || typeof current !== "object") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
current = current[part];
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
};
|
|
26
|
+
const writePath = (scope, expression, value) => {
|
|
27
|
+
const parts = expression.split(".");
|
|
28
|
+
const property = parts.pop();
|
|
29
|
+
let current = scope;
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
current = current && typeof current === "object" ? current[part] : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (property && current && typeof current === "object") {
|
|
34
|
+
current[property] = value;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const signatureFor = (options) => options.signature ?? JSON.stringify(options);
|
|
38
|
+
const readBinding = (scope, binding) => (binding.read ? binding.read(scope) : readPath(scope, binding.expression));
|
|
39
|
+
const readExpression = (scope, expression, read) => (read ? read(scope) : readPath(scope, expression));
|
|
40
|
+
const readEvent = (scope, binding) => binding.read ? binding.read(scope) : readPath(scope, binding.handler);
|
|
41
|
+
const writeBinding = (scope, binding, value) => {
|
|
42
|
+
if (binding.write) {
|
|
43
|
+
binding.write(scope, value);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
writePath(scope, binding.expression, value);
|
|
47
|
+
};
|
|
48
|
+
const cleanup = (state) => {
|
|
49
|
+
for (const cleanupFn of state.cleanups) {
|
|
50
|
+
cleanupFn();
|
|
51
|
+
}
|
|
52
|
+
state.cleanups.length = 0;
|
|
53
|
+
for (const node of state.nodes) {
|
|
54
|
+
node.parentNode?.removeChild(node);
|
|
55
|
+
}
|
|
56
|
+
state.nodes.length = 0;
|
|
57
|
+
};
|
|
58
|
+
const createNodes = (templateHtml) => {
|
|
59
|
+
const template = document.createElement("template");
|
|
60
|
+
template.innerHTML = templateHtml;
|
|
61
|
+
return Array.from(template.content.childNodes).map((node) => node.cloneNode(true));
|
|
62
|
+
};
|
|
63
|
+
const bindNodes = (anchor, state, scope, options) => {
|
|
64
|
+
state.scope = scope;
|
|
65
|
+
const firstElement = state.nodes.find((node) => node instanceof Element);
|
|
66
|
+
if (!firstElement) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const binding of options.bindings) {
|
|
70
|
+
if (binding.kind === "text") {
|
|
71
|
+
setText(textAt(firstElement, binding.path), readBinding(scope, binding));
|
|
72
|
+
}
|
|
73
|
+
else if (binding.kind === "class") {
|
|
74
|
+
setClassPresence(elementAt(firstElement, binding.path), binding.className, readBinding(scope, binding));
|
|
75
|
+
}
|
|
76
|
+
else if (binding.kind === "attr") {
|
|
77
|
+
setAttributeValue(elementAt(firstElement, binding.path), binding.name, readBinding(scope, binding));
|
|
78
|
+
}
|
|
79
|
+
else if (binding.kind === "style") {
|
|
80
|
+
setStyleValue(elementAt(firstElement, binding.path), binding.name, readBinding(scope, binding));
|
|
81
|
+
}
|
|
82
|
+
else if (binding.kind === "ref") {
|
|
83
|
+
setRef(scope, binding.expression, elementAt(firstElement, binding.path));
|
|
84
|
+
}
|
|
85
|
+
else if (binding.kind === "model") {
|
|
86
|
+
setControlValue(elementAt(firstElement, binding.path), binding.property, readBinding(scope, binding));
|
|
87
|
+
}
|
|
88
|
+
else if (binding.kind === "list") {
|
|
89
|
+
mountKeyedList(firstElement, binding.path, readExpression(scope, binding.each, binding.read), { ...binding, scope });
|
|
90
|
+
}
|
|
91
|
+
else if (binding.kind === "if") {
|
|
92
|
+
mountConditional(firstElement, binding.path, readExpression(scope, binding.test, binding.read), scope, binding);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (state.cleanups.length === 0) {
|
|
96
|
+
for (const binding of options.bindings) {
|
|
97
|
+
if (binding.kind === "event") {
|
|
98
|
+
const listener = (event) => {
|
|
99
|
+
const handler = readEvent(state.scope, binding);
|
|
100
|
+
if (typeof handler === "function") {
|
|
101
|
+
handler(event);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
state.cleanups.push(delegate(firstElement, binding.eventName, binding.path, listener));
|
|
105
|
+
}
|
|
106
|
+
else if (binding.kind === "model") {
|
|
107
|
+
const element = elementAt(firstElement, binding.path);
|
|
108
|
+
state.cleanups.push(bindControl(element, binding.property, () => readBinding(scope, binding), (value) => writeBinding(scope, binding, value)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!firstElement.isConnected) {
|
|
113
|
+
anchor.after(...state.nodes);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
export const mountConditional = (root, path, visible, scope, options) => {
|
|
117
|
+
const anchor = nodeAt(root, path);
|
|
118
|
+
if (!(anchor instanceof Comment)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const signature = signatureFor(options);
|
|
122
|
+
const current = states.get(anchor);
|
|
123
|
+
if (!visible) {
|
|
124
|
+
if (current) {
|
|
125
|
+
cleanup(current);
|
|
126
|
+
states.delete(anchor);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const state = current && current.signature === signature
|
|
131
|
+
? current
|
|
132
|
+
: {
|
|
133
|
+
signature,
|
|
134
|
+
nodes: createNodes(options.templateHtml),
|
|
135
|
+
cleanups: [],
|
|
136
|
+
scope,
|
|
137
|
+
};
|
|
138
|
+
states.set(anchor, state);
|
|
139
|
+
bindNodes(anchor, state, scope, options);
|
|
140
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type EnhancementCleanup = () => void;
|
|
2
|
+
export type EnhancementInitializer = (root: Element) => void | EnhancementCleanup;
|
|
3
|
+
export type EnhancementRegistry = {
|
|
4
|
+
register: (name: string, initializer: EnhancementInitializer) => () => void;
|
|
5
|
+
enhance: (root?: ParentNode) => number;
|
|
6
|
+
cleanup: (root?: ParentNode) => number;
|
|
7
|
+
};
|
|
8
|
+
export declare const createEnhancementRegistry: () => EnhancementRegistry;
|
|
9
|
+
export declare const registerEnhancement: (name: string, initializer: EnhancementInitializer) => () => void;
|
|
10
|
+
export declare const enhance: (root?: ParentNode) => number;
|
|
11
|
+
export declare const cleanupEnhancements: (root?: ParentNode) => number;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const enhancementAttribute = "data-td-enhance";
|
|
2
|
+
const rootsFor = (root) => {
|
|
3
|
+
const elements = [];
|
|
4
|
+
if (root instanceof Element && root.hasAttribute(enhancementAttribute)) {
|
|
5
|
+
elements.push(root);
|
|
6
|
+
}
|
|
7
|
+
elements.push(...Array.from(root.querySelectorAll(`[${enhancementAttribute}]`)));
|
|
8
|
+
return elements;
|
|
9
|
+
};
|
|
10
|
+
const enhancementNamesFor = (element) => (element.getAttribute(enhancementAttribute) ?? "")
|
|
11
|
+
.split(/\s+/)
|
|
12
|
+
.map((name) => name.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
export const createEnhancementRegistry = () => {
|
|
15
|
+
const initializers = new Map();
|
|
16
|
+
const initialized = new WeakMap();
|
|
17
|
+
return {
|
|
18
|
+
register: (name, initializer) => {
|
|
19
|
+
if (initializers.has(name)) {
|
|
20
|
+
throw new Error(`Enhancement is already registered: ${name}`);
|
|
21
|
+
}
|
|
22
|
+
initializers.set(name, initializer);
|
|
23
|
+
return () => {
|
|
24
|
+
initializers.delete(name);
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
enhance: (root = document) => {
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const element of rootsFor(root)) {
|
|
30
|
+
const active = initialized.get(element) ?? new Map();
|
|
31
|
+
for (const name of enhancementNamesFor(element)) {
|
|
32
|
+
if (active.has(name)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const initializer = initializers.get(name);
|
|
36
|
+
if (!initializer) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const cleanup = initializer(element);
|
|
40
|
+
active.set(name, cleanup || undefined);
|
|
41
|
+
count += 1;
|
|
42
|
+
}
|
|
43
|
+
if (active.size > 0) {
|
|
44
|
+
initialized.set(element, active);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return count;
|
|
48
|
+
},
|
|
49
|
+
cleanup: (root = document) => {
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (const element of rootsFor(root)) {
|
|
52
|
+
const active = initialized.get(element);
|
|
53
|
+
if (!active) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const cleanup of active.values()) {
|
|
57
|
+
cleanup?.();
|
|
58
|
+
count += 1;
|
|
59
|
+
}
|
|
60
|
+
active.clear();
|
|
61
|
+
initialized.delete(element);
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const defaultRegistry = createEnhancementRegistry();
|
|
68
|
+
export const registerEnhancement = defaultRegistry.register;
|
|
69
|
+
export const enhance = defaultRegistry.enhance;
|
|
70
|
+
export const cleanupEnhancements = defaultRegistry.cleanup;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ClientHtml } from "./html.js";
|
|
2
|
+
type ErrorBoundaryRenderValue = string | ClientHtml | Node | readonly Node[] | DocumentFragment;
|
|
3
|
+
export type ErrorBoundaryOptions = {
|
|
4
|
+
render: (root: Element) => void;
|
|
5
|
+
fallback: (error: unknown) => ErrorBoundaryRenderValue;
|
|
6
|
+
};
|
|
7
|
+
export declare const createErrorBoundary: (root: Element, options: ErrorBoundaryOptions) => (() => void);
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { catchError } from "./signal.js";
|
|
2
|
+
import { isClientHtml } from "./html.js";
|
|
3
|
+
const renderValue = (root, value) => {
|
|
4
|
+
root.replaceChildren();
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
root.textContent = value;
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (isClientHtml(value)) {
|
|
10
|
+
root.innerHTML = value.toString();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (value instanceof DocumentFragment) {
|
|
14
|
+
root.appendChild(value);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (value instanceof Node) {
|
|
18
|
+
root.appendChild(value);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
root.append(...value);
|
|
22
|
+
};
|
|
23
|
+
export const createErrorBoundary = (root, options) => catchError(() => {
|
|
24
|
+
options.render(root);
|
|
25
|
+
}, (error) => {
|
|
26
|
+
renderValue(root, options.fallback(error));
|
|
27
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const delegate: (root: Element, eventName: string, path: readonly number[], handler: EventListener) => (() => void);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const delegate = (root, eventName, path, handler) => {
|
|
2
|
+
let target = root;
|
|
3
|
+
for (const index of path) {
|
|
4
|
+
target = target.childNodes[index];
|
|
5
|
+
}
|
|
6
|
+
if (!(target instanceof Element)) {
|
|
7
|
+
return () => undefined;
|
|
8
|
+
}
|
|
9
|
+
target.addEventListener(eventName, handler);
|
|
10
|
+
return () => target.removeEventListener(eventName, handler);
|
|
11
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type BoundControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
2
|
+
export declare const setControlValue: (element: BoundControl, property: "value" | "checked", value: unknown) => void;
|
|
3
|
+
export declare const bindControl: (element: Element, property: "value" | "checked", read: () => unknown, write: (value: unknown) => void) => (() => void);
|
|
4
|
+
export type EnhancedFormContext = {
|
|
5
|
+
form: HTMLFormElement;
|
|
6
|
+
request: Request;
|
|
7
|
+
formData: FormData;
|
|
8
|
+
};
|
|
9
|
+
export type FormFieldRule = {
|
|
10
|
+
required?: boolean;
|
|
11
|
+
pattern?: RegExp;
|
|
12
|
+
minLength?: number;
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
message?: string;
|
|
15
|
+
validate?: (value: string, formData: FormData) => string | undefined | null;
|
|
16
|
+
};
|
|
17
|
+
export type FormValidationResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
values: Record<string, FormDataEntryValue | FormDataEntryValue[]>;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
errors: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
export type EnhanceFormOptions = {
|
|
25
|
+
validate?: (context: EnhancedFormContext) => FormValidationResult | Promise<FormValidationResult>;
|
|
26
|
+
submit?: (context: EnhancedFormContext) => Response | Promise<Response>;
|
|
27
|
+
onInvalid?: (context: EnhancedFormContext & {
|
|
28
|
+
errors: Record<string, string>;
|
|
29
|
+
}) => void | Promise<void>;
|
|
30
|
+
onSuccess?: (context: EnhancedFormContext & {
|
|
31
|
+
response: Response;
|
|
32
|
+
}) => void | Promise<void>;
|
|
33
|
+
onError?: (context: EnhancedFormContext & {
|
|
34
|
+
error: unknown;
|
|
35
|
+
}) => void | Promise<void>;
|
|
36
|
+
navigate?: (href: string, options?: {
|
|
37
|
+
replace?: boolean;
|
|
38
|
+
}) => void | Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
export declare const validateFormData: (formData: FormData, rules: Record<string, FormFieldRule>) => FormValidationResult;
|
|
41
|
+
export declare const enhanceForm: (form: HTMLFormElement, options?: EnhanceFormOptions) => (() => void);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const setControlValue = (element, property, value) => {
|
|
2
|
+
if (property === "checked" && element instanceof HTMLInputElement) {
|
|
3
|
+
element.checked = Boolean(value);
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
element.value = value == null ? "" : String(value);
|
|
7
|
+
};
|
|
8
|
+
export const bindControl = (element, property, read, write) => {
|
|
9
|
+
if (!(element instanceof HTMLInputElement ||
|
|
10
|
+
element instanceof HTMLTextAreaElement ||
|
|
11
|
+
element instanceof HTMLSelectElement)) {
|
|
12
|
+
return () => undefined;
|
|
13
|
+
}
|
|
14
|
+
const control = element;
|
|
15
|
+
setControlValue(control, property, read());
|
|
16
|
+
const eventName = property === "checked" ? "change" : "input";
|
|
17
|
+
let composing = false;
|
|
18
|
+
const commit = () => {
|
|
19
|
+
write(property === "checked" && control instanceof HTMLInputElement ? control.checked : control.value);
|
|
20
|
+
};
|
|
21
|
+
const listener = (event) => {
|
|
22
|
+
if (property === "value" && (event.isComposing || composing)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
commit();
|
|
26
|
+
};
|
|
27
|
+
const onCompositionStart = () => {
|
|
28
|
+
composing = true;
|
|
29
|
+
};
|
|
30
|
+
const onCompositionEnd = () => {
|
|
31
|
+
composing = false;
|
|
32
|
+
commit();
|
|
33
|
+
};
|
|
34
|
+
control.addEventListener(eventName, listener);
|
|
35
|
+
if (property === "value") {
|
|
36
|
+
control.addEventListener("compositionstart", onCompositionStart);
|
|
37
|
+
control.addEventListener("compositionend", onCompositionEnd);
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
control.removeEventListener(eventName, listener);
|
|
41
|
+
control.removeEventListener("compositionstart", onCompositionStart);
|
|
42
|
+
control.removeEventListener("compositionend", onCompositionEnd);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const formMethod = (form) => (form.getAttribute("method") || "get").toUpperCase();
|
|
46
|
+
const formAction = (form) => new URL(form.getAttribute("action") || location.href, location.href);
|
|
47
|
+
const requestForForm = (form, formData) => {
|
|
48
|
+
const method = formMethod(form);
|
|
49
|
+
const url = formAction(form);
|
|
50
|
+
if (method === "GET") {
|
|
51
|
+
for (const [name, value] of formData) {
|
|
52
|
+
url.searchParams.append(name, String(value));
|
|
53
|
+
}
|
|
54
|
+
return new Request(url, { method });
|
|
55
|
+
}
|
|
56
|
+
return new Request(url, { method, body: formData });
|
|
57
|
+
};
|
|
58
|
+
const valuesForFormData = (formData) => {
|
|
59
|
+
const values = {};
|
|
60
|
+
for (const [name, value] of formData) {
|
|
61
|
+
const current = values[name];
|
|
62
|
+
if (current === undefined) {
|
|
63
|
+
values[name] = value;
|
|
64
|
+
}
|
|
65
|
+
else if (Array.isArray(current)) {
|
|
66
|
+
current.push(value);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
values[name] = [current, value];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return values;
|
|
73
|
+
};
|
|
74
|
+
const fieldStringValue = (formData, name) => {
|
|
75
|
+
const value = formData.get(name);
|
|
76
|
+
return typeof File !== "undefined" && value instanceof File ? value.name : String(value ?? "");
|
|
77
|
+
};
|
|
78
|
+
export const validateFormData = (formData, rules) => {
|
|
79
|
+
const errors = {};
|
|
80
|
+
for (const [name, rule] of Object.entries(rules)) {
|
|
81
|
+
const value = fieldStringValue(formData, name);
|
|
82
|
+
const message = rule.message ?? `${name} is invalid.`;
|
|
83
|
+
if (rule.required && value.trim() === "") {
|
|
84
|
+
errors[name] = message;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (rule.minLength !== undefined && value.length < rule.minLength) {
|
|
88
|
+
errors[name] = message;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (rule.maxLength !== undefined && value.length > rule.maxLength) {
|
|
92
|
+
errors[name] = message;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
96
|
+
errors[name] = message;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const customError = rule.validate?.(value, formData);
|
|
100
|
+
if (customError) {
|
|
101
|
+
errors[name] = customError;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, values: valuesForFormData(formData) };
|
|
105
|
+
};
|
|
106
|
+
const isSettableControl = (element) => element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
|
|
107
|
+
const clearCustomValidity = (form) => {
|
|
108
|
+
for (const element of Array.from(form.elements)) {
|
|
109
|
+
if (isSettableControl(element)) {
|
|
110
|
+
element.setCustomValidity("");
|
|
111
|
+
element.removeAttribute("aria-invalid");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const namedControl = (form, name) => Array.from(form.elements).find((element) => isSettableControl(element) && element.name === name);
|
|
116
|
+
const applyValidationErrors = (form, errors) => {
|
|
117
|
+
let firstInvalid;
|
|
118
|
+
for (const [name, message] of Object.entries(errors)) {
|
|
119
|
+
const control = namedControl(form, name);
|
|
120
|
+
if (!control) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
control.setCustomValidity(message);
|
|
124
|
+
control.setAttribute("aria-invalid", "true");
|
|
125
|
+
firstInvalid ??= control;
|
|
126
|
+
}
|
|
127
|
+
firstInvalid?.focus();
|
|
128
|
+
form.reportValidity();
|
|
129
|
+
};
|
|
130
|
+
export const enhanceForm = (form, options = {}) => {
|
|
131
|
+
const submit = options.submit ?? ((context) => fetch(context.request));
|
|
132
|
+
const listener = (event) => {
|
|
133
|
+
if (event.defaultPrevented) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
clearCustomValidity(form);
|
|
138
|
+
if (!form.noValidate && !form.checkValidity()) {
|
|
139
|
+
form.reportValidity();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const formData = event.submitter instanceof HTMLElement ? new FormData(form, event.submitter) : new FormData(form);
|
|
143
|
+
const request = requestForForm(form, formData);
|
|
144
|
+
const context = { form, request, formData };
|
|
145
|
+
void (async () => {
|
|
146
|
+
try {
|
|
147
|
+
const validation = await options.validate?.(context);
|
|
148
|
+
if (validation && !validation.ok) {
|
|
149
|
+
applyValidationErrors(form, validation.errors);
|
|
150
|
+
await options.onInvalid?.({ ...context, errors: validation.errors });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const response = await submit(context);
|
|
154
|
+
const locationHeader = response.headers.get("location");
|
|
155
|
+
if (response.redirected || (response.status >= 300 && response.status < 400 && locationHeader)) {
|
|
156
|
+
await options.navigate?.(locationHeader ?? response.url, { replace: true });
|
|
157
|
+
}
|
|
158
|
+
await options.onSuccess?.({ ...context, response });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
await options.onError?.({ ...context, error });
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
};
|
|
165
|
+
form.addEventListener("submit", listener);
|
|
166
|
+
return () => form.removeEventListener("submit", listener);
|
|
167
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type FragmentHandle = {
|
|
2
|
+
nodes: Node[];
|
|
3
|
+
remove: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare const createFragmentNodes: (templateHtml: string) => Node[];
|
|
6
|
+
export declare const mountFragment: (parent: ParentNode, before: Node | null, nodes: readonly Node[]) => FragmentHandle;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const createFragmentNodes = (templateHtml) => {
|
|
2
|
+
const template = document.createElement("template");
|
|
3
|
+
template.innerHTML = templateHtml;
|
|
4
|
+
return Array.from(template.content.childNodes).map((node) => node.cloneNode(true));
|
|
5
|
+
};
|
|
6
|
+
export const mountFragment = (parent, before, nodes) => {
|
|
7
|
+
for (const node of nodes) {
|
|
8
|
+
parent.insertBefore(node, before);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
nodes: [...nodes],
|
|
12
|
+
remove: () => {
|
|
13
|
+
for (const node of nodes) {
|
|
14
|
+
node.parentNode?.removeChild(node);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
};
|