@derivesome/tree 0.1.1
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/.package.json.~undo-tree~ +4 -0
- package/.tsconfig.json.~undo-tree~ +4 -0
- package/dist/cjs/brands.d.ts +3 -0
- package/dist/cjs/brands.d.ts.map +1 -0
- package/dist/cjs/brands.js +5 -0
- package/dist/cjs/brands.js.map +1 -0
- package/dist/cjs/context.d.ts +28 -0
- package/dist/cjs/context.d.ts.map +1 -0
- package/dist/cjs/context.js +48 -0
- package/dist/cjs/context.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mount.d.ts +6 -0
- package/dist/cjs/mount.d.ts.map +1 -0
- package/dist/cjs/mount.js +209 -0
- package/dist/cjs/mount.js.map +1 -0
- package/dist/cjs/props.d.ts +12 -0
- package/dist/cjs/props.d.ts.map +1 -0
- package/dist/cjs/props.js +80 -0
- package/dist/cjs/props.js.map +1 -0
- package/dist/cjs/renderer.d.ts +29 -0
- package/dist/cjs/renderer.d.ts.map +1 -0
- package/dist/cjs/renderer.js +3 -0
- package/dist/cjs/renderer.js.map +1 -0
- package/dist/cjs/tree-node-like.d.ts +8 -0
- package/dist/cjs/tree-node-like.d.ts.map +1 -0
- package/dist/cjs/tree-node-like.js +4 -0
- package/dist/cjs/tree-node-like.js.map +1 -0
- package/dist/cjs/tree.d.ts +46 -0
- package/dist/cjs/tree.d.ts.map +1 -0
- package/dist/cjs/tree.js +154 -0
- package/dist/cjs/tree.js.map +1 -0
- package/dist/cjs/velement.d.ts +185 -0
- package/dist/cjs/velement.d.ts.map +1 -0
- package/dist/cjs/velement.js +874 -0
- package/dist/cjs/velement.js.map +1 -0
- package/dist/esm/brands.d.ts +3 -0
- package/dist/esm/brands.d.ts.map +1 -0
- package/dist/esm/brands.js +5 -0
- package/dist/esm/brands.js.map +1 -0
- package/dist/esm/context.d.ts +28 -0
- package/dist/esm/context.d.ts.map +1 -0
- package/dist/esm/context.js +48 -0
- package/dist/esm/context.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mount.d.ts +6 -0
- package/dist/esm/mount.d.ts.map +1 -0
- package/dist/esm/mount.js +209 -0
- package/dist/esm/mount.js.map +1 -0
- package/dist/esm/props.d.ts +12 -0
- package/dist/esm/props.d.ts.map +1 -0
- package/dist/esm/props.js +80 -0
- package/dist/esm/props.js.map +1 -0
- package/dist/esm/renderer.d.ts +29 -0
- package/dist/esm/renderer.d.ts.map +1 -0
- package/dist/esm/renderer.js +3 -0
- package/dist/esm/renderer.js.map +1 -0
- package/dist/esm/tree-node-like.d.ts +8 -0
- package/dist/esm/tree-node-like.d.ts.map +1 -0
- package/dist/esm/tree-node-like.js +4 -0
- package/dist/esm/tree-node-like.js.map +1 -0
- package/dist/esm/tree.d.ts +46 -0
- package/dist/esm/tree.d.ts.map +1 -0
- package/dist/esm/tree.js +154 -0
- package/dist/esm/tree.js.map +1 -0
- package/dist/esm/velement.d.ts +185 -0
- package/dist/esm/velement.d.ts.map +1 -0
- package/dist/esm/velement.js +874 -0
- package/dist/esm/velement.js.map +1 -0
- package/package.json +46 -0
- package/package.json~ +52 -0
- package/src/#mount.test.ts# +372 -0
- package/src/.brands.ts.~undo-tree~ +6 -0
- package/src/.context.ts.~undo-tree~ +6 -0
- package/src/.index.ts.~undo-tree~ +11 -0
- package/src/.mount.test.ts.~undo-tree~ +438 -0
- package/src/.mount.ts.~undo-tree~ +70 -0
- package/src/.node-like.ts.~undo-tree~ +8 -0
- package/src/.props.ts.~undo-tree~ +125 -0
- package/src/.renderer.ts.~undo-tree~ +18 -0
- package/src/.tree-node-like.ts.~undo-tree~ +12 -0
- package/src/.tree.ts.~undo-tree~ +46 -0
- package/src/.velement.ts.~undo-tree~ +1739 -0
- package/src/brands.ts +2 -0
- package/src/brands.ts~ +0 -0
- package/src/context.ts +61 -0
- package/src/context.ts~ +0 -0
- package/src/index.ts +5 -0
- package/src/index.ts~ +4 -0
- package/src/mount.test.ts +405 -0
- package/src/mount.test.ts~ +375 -0
- package/src/mount.ts +332 -0
- package/src/mount.ts~ +306 -0
- package/src/node-like.ts~ +0 -0
- package/src/props.ts +99 -0
- package/src/props.ts~ +86 -0
- package/src/renderer.ts +37 -0
- package/src/renderer.ts~ +37 -0
- package/src/tree-node-like.ts +8 -0
- package/src/tree-node-like.ts~ +6 -0
- package/src/tree.ts +226 -0
- package/src/tree.ts~ +227 -0
- package/src/velement.ts +990 -0
- package/src/velement.ts~ +966 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +23 -0
- package/tsconfig.json~ +23 -0
package/src/mount.ts~
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { derived, isPlainObject, isReference } from "@derivesome/core";
|
|
2
|
+
import { TreeContext, withCleanupScope } from "./context";
|
|
3
|
+
import { Renderer } from "./renderer";
|
|
4
|
+
import {
|
|
5
|
+
tnu,
|
|
6
|
+
TreeNode,
|
|
7
|
+
TreeNodeElement,
|
|
8
|
+
TreeNodeFunction,
|
|
9
|
+
TreeNodeList,
|
|
10
|
+
TreeNodeValue,
|
|
11
|
+
TreeNodeVoid,
|
|
12
|
+
} from "./tree";
|
|
13
|
+
import { forwardProps, TreeNodeProps } from "./props";
|
|
14
|
+
|
|
15
|
+
type Cleanup = () => void;
|
|
16
|
+
const noop: Cleanup = () => {};
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
type MountContext = {
|
|
20
|
+
forwardedProps: TreeNodeProps | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const createMountContext = (): MountContext => ({
|
|
24
|
+
forwardedProps: null
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function isEventKey(key: string): boolean {
|
|
28
|
+
return key.length > 2 && key.startsWith("on") && /^[A-Z]/.test(key[2]!);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function applyProp<N>(
|
|
32
|
+
renderer: Renderer<N>,
|
|
33
|
+
el: N,
|
|
34
|
+
key: string,
|
|
35
|
+
value: unknown,
|
|
36
|
+
): void {
|
|
37
|
+
if (value === null || value === undefined || value === false) {
|
|
38
|
+
renderer.removeProp(el, key);
|
|
39
|
+
} else {
|
|
40
|
+
renderer.setProp(el, key, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setupProp<N>(
|
|
45
|
+
renderer: Renderer<N>,
|
|
46
|
+
el: N,
|
|
47
|
+
key: string,
|
|
48
|
+
value: unknown,
|
|
49
|
+
): Cleanup | null {
|
|
50
|
+
if (key === "children") return null;
|
|
51
|
+
if (isReference(value)) {
|
|
52
|
+
const r = value;
|
|
53
|
+
applyProp(renderer, el, key, r.peek());
|
|
54
|
+
return r.observe((val) => applyProp(renderer, el, key, val));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
const cleanups = value
|
|
59
|
+
.map((x) => setupProp(renderer, el, key, x))
|
|
60
|
+
.filter((v) => v !== null);
|
|
61
|
+
return () => {
|
|
62
|
+
cleanups.forEach((fn) => fn());
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value === "function" && !isEventKey(key)) {
|
|
67
|
+
// Computed prop — wrap in derived so dependency tracking works
|
|
68
|
+
const d = derived(value as () => unknown);
|
|
69
|
+
return d.observe((val) => applyProp(renderer, el, key, val), {
|
|
70
|
+
immediate: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Static value or event handler
|
|
75
|
+
applyProp(renderer, el, key, value);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mountElement<N>(
|
|
80
|
+
ctx: MountContext,
|
|
81
|
+
node: TreeNodeElement,
|
|
82
|
+
renderer: Renderer<N>,
|
|
83
|
+
parent: N,
|
|
84
|
+
before: N | null,
|
|
85
|
+
): Cleanup {
|
|
86
|
+
const props = ctx.forwardedProps ? forwardProps(node.props, ctx.forwardedProps) : node.props;
|
|
87
|
+
|
|
88
|
+
const el = renderer.createElement(node.tag);
|
|
89
|
+
const cleanups: Cleanup[] = [];
|
|
90
|
+
|
|
91
|
+
for (const [key, value] of Object.entries(props)) {
|
|
92
|
+
const cleanup = setupProp(renderer, el, key, value);
|
|
93
|
+
if (cleanup) cleanups.push(cleanup);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cleanups.push(mountChildren({...ctx, forwardedProps: null}, node.children, renderer, el, null));
|
|
97
|
+
renderer.insertBefore(parent, el, before);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
cleanups.forEach((c) => c());
|
|
101
|
+
renderer.removeChild(parent, el);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type ItemState<N> = {
|
|
106
|
+
itemStart: N;
|
|
107
|
+
itemEnd: N;
|
|
108
|
+
cleanup: Cleanup;
|
|
109
|
+
node: TreeNode;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function mountList<N>(
|
|
113
|
+
ctx: MountContext,
|
|
114
|
+
node: TreeNodeList,
|
|
115
|
+
renderer: Renderer<N>,
|
|
116
|
+
parent: N,
|
|
117
|
+
before: N | null,
|
|
118
|
+
): Cleanup {
|
|
119
|
+
const startMarker = renderer.createMarker("each");
|
|
120
|
+
const endMarker = renderer.createMarker("/each");
|
|
121
|
+
renderer.insertBefore(parent, startMarker, before);
|
|
122
|
+
renderer.insertBefore(parent, endMarker, before);
|
|
123
|
+
|
|
124
|
+
const itemMap = new Map<string | number, ItemState<N>>();
|
|
125
|
+
|
|
126
|
+
const update = (list: TreeNode[]): void => {
|
|
127
|
+
const newKeys = list.map((item, i) => {
|
|
128
|
+
const k = item.props.key;
|
|
129
|
+
return typeof k === "string" || typeof k === "number" ? k : i;
|
|
130
|
+
});
|
|
131
|
+
const newKeySet = new Set(newKeys);
|
|
132
|
+
|
|
133
|
+
// Remove items that dropped out of the list
|
|
134
|
+
for (const [key, state] of itemMap) {
|
|
135
|
+
if (!newKeySet.has(key)) {
|
|
136
|
+
state.cleanup();
|
|
137
|
+
renderer.removeChild(parent, state.itemStart);
|
|
138
|
+
renderer.removeChild(parent, state.itemEnd);
|
|
139
|
+
itemMap.delete(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create newly-appeared items (appended before endMarker for now; reordered below)
|
|
144
|
+
for (let i = 0; i < list.length; i++) {
|
|
145
|
+
const key = newKeys[i]!;
|
|
146
|
+
const item = list[i]!;
|
|
147
|
+
if (!itemMap.has(key)) {
|
|
148
|
+
const itemStart = renderer.createMarker(`[${String(key)}]`);
|
|
149
|
+
const itemEnd = renderer.createMarker(`[/${String(key)}]`);
|
|
150
|
+
renderer.insertBefore(parent, itemStart, endMarker);
|
|
151
|
+
renderer.insertBefore(parent, itemEnd, endMarker);
|
|
152
|
+
const cleanup = mountNode(ctx, item, renderer, parent, itemEnd);
|
|
153
|
+
itemMap.set(key, { itemStart, itemEnd, cleanup, node: item });
|
|
154
|
+
} else {
|
|
155
|
+
// Index-based keys (no explicit key prop) mean the same position may
|
|
156
|
+
// now hold different content. Remount so the DOM stays in sync.
|
|
157
|
+
const isIndexKey = key === i;
|
|
158
|
+
if (isIndexKey) {
|
|
159
|
+
const state = itemMap.get(key)!;
|
|
160
|
+
if (state.node !== item) {
|
|
161
|
+
state.cleanup();
|
|
162
|
+
state.cleanup = mountNode(ctx, item, renderer, parent, state.itemEnd);
|
|
163
|
+
state.node = item;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Reorder to match the new key sequence.
|
|
170
|
+
// Process from the end so we can use each item's start marker as the
|
|
171
|
+
// insertion cursor for the preceding item.
|
|
172
|
+
let cursor: N = endMarker;
|
|
173
|
+
for (let i = newKeys.length - 1; i >= 0; i--) {
|
|
174
|
+
const key = newKeys[i]!;
|
|
175
|
+
const state = itemMap.get(key)!;
|
|
176
|
+
renderer.moveRange(parent, state.itemStart, state.itemEnd, cursor);
|
|
177
|
+
cursor = state.itemStart;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const unsub = node.items.observe(update, { immediate: true });
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
unsub();
|
|
185
|
+
for (const [, state] of itemMap) {
|
|
186
|
+
state.cleanup();
|
|
187
|
+
renderer.removeChild(parent, state.itemStart);
|
|
188
|
+
renderer.removeChild(parent, state.itemEnd);
|
|
189
|
+
}
|
|
190
|
+
itemMap.clear();
|
|
191
|
+
renderer.removeChild(parent, startMarker);
|
|
192
|
+
renderer.removeChild(parent, endMarker);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveText(content: unknown): string {
|
|
197
|
+
if (typeof content === "boolean") return "";
|
|
198
|
+
if (isReference(content)) return String(content.peek());
|
|
199
|
+
if (typeof content === "function")
|
|
200
|
+
return String((content as () => string | number | boolean)());
|
|
201
|
+
return String(content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mountValue<N>(
|
|
205
|
+
_ctx: MountContext,
|
|
206
|
+
node: TreeNodeValue,
|
|
207
|
+
renderer: Renderer<N>,
|
|
208
|
+
parent: N,
|
|
209
|
+
before: N | null,
|
|
210
|
+
): Cleanup {
|
|
211
|
+
const domText = renderer.createText(resolveText(node.value));
|
|
212
|
+
renderer.insertBefore(parent, domText, before);
|
|
213
|
+
|
|
214
|
+
let unsub: Cleanup | null = null;
|
|
215
|
+
|
|
216
|
+
const content = node.value;
|
|
217
|
+
|
|
218
|
+
if (isReference(content)) {
|
|
219
|
+
unsub = content.observe((val) => {
|
|
220
|
+
renderer.setText(domText, typeof val === "boolean" ? "" : String(val));
|
|
221
|
+
});
|
|
222
|
+
} else if (typeof content === "function") {
|
|
223
|
+
const d = derived(content as () => string | number | boolean);
|
|
224
|
+
unsub = d.observe((val) => {
|
|
225
|
+
renderer.setText(domText, typeof val === "boolean" ? "" : String(val));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return () => {
|
|
230
|
+
unsub?.();
|
|
231
|
+
renderer.removeChild(parent, domText);
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function mountFn<N>(
|
|
236
|
+
ctx: MountContext,
|
|
237
|
+
node: TreeNodeFunction,
|
|
238
|
+
renderer: Renderer<N>,
|
|
239
|
+
parent: N,
|
|
240
|
+
before: N | null,
|
|
241
|
+
): Cleanup {
|
|
242
|
+
return TreeContext.scoped(() => {
|
|
243
|
+
const [childNode, scopeCleanup] = withCleanupScope(() =>
|
|
244
|
+
tnu.normalizeOne(node.fn(node.props)),
|
|
245
|
+
);
|
|
246
|
+
const mountCleanup = mountNode({...ctx, forwardedProps: forwardProps({}, node.props)}, childNode, renderer, parent, before);
|
|
247
|
+
return () => {
|
|
248
|
+
mountCleanup();
|
|
249
|
+
scopeCleanup();
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function mountVoid<N>(
|
|
255
|
+
_ctx: MountContext,
|
|
256
|
+
_node: TreeNodeVoid,
|
|
257
|
+
_renderer: Renderer<N>,
|
|
258
|
+
_parent: N,
|
|
259
|
+
_before: N | null,
|
|
260
|
+
): Cleanup {
|
|
261
|
+
return noop;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function mountChildren<N>(
|
|
265
|
+
ctx: MountContext,
|
|
266
|
+
children: TreeNode[],
|
|
267
|
+
renderer: Renderer<N>,
|
|
268
|
+
parent: N,
|
|
269
|
+
before: N | null,
|
|
270
|
+
): Cleanup {
|
|
271
|
+
const cleanups = children.map((child) =>
|
|
272
|
+
mountNode(ctx, child, renderer, parent, before),
|
|
273
|
+
);
|
|
274
|
+
return () => cleanups.forEach((c) => c());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mountNode<N>(
|
|
278
|
+
ctx: MountContext,
|
|
279
|
+
node: TreeNode,
|
|
280
|
+
renderer: Renderer<N>,
|
|
281
|
+
parent: N,
|
|
282
|
+
before: N | null,
|
|
283
|
+
): Cleanup {
|
|
284
|
+
switch (node.type) {
|
|
285
|
+
case "element":
|
|
286
|
+
return mountElement(ctx, node, renderer, parent, before);
|
|
287
|
+
case "list":
|
|
288
|
+
return mountList(ctx, node, renderer, parent, before);
|
|
289
|
+
case "value":
|
|
290
|
+
return mountValue(ctx, node, renderer, parent, before);
|
|
291
|
+
case "function":
|
|
292
|
+
return mountFn(ctx, node, renderer, parent, before);
|
|
293
|
+
case "void":
|
|
294
|
+
return mountVoid(ctx, node, renderer, parent, before);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function mount<N>(
|
|
299
|
+
node: TreeNode,
|
|
300
|
+
renderer: Renderer<N>,
|
|
301
|
+
parent: N,
|
|
302
|
+
): Cleanup {
|
|
303
|
+
const ctx = createMountContext();
|
|
304
|
+
|
|
305
|
+
return mountNode(ctx, node, renderer, parent, null);
|
|
306
|
+
}
|
|
File without changes
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
derived,
|
|
3
|
+
isReference,
|
|
4
|
+
LooseDict,
|
|
5
|
+
unique,
|
|
6
|
+
zipMax,
|
|
7
|
+
} from "@derivesome/core";
|
|
8
|
+
|
|
9
|
+
export type TreeNodeProps = LooseDict & {
|
|
10
|
+
children?: unknown;
|
|
11
|
+
style?: unknown;
|
|
12
|
+
id?: unknown;
|
|
13
|
+
key?: string | number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const FORWARDED_PROPS: Set<string> = new Set([
|
|
17
|
+
"class",
|
|
18
|
+
"style",
|
|
19
|
+
"key",
|
|
20
|
+
"id",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export const isForwardedProp = (key: string): boolean => {
|
|
24
|
+
if (FORWARDED_PROPS.has(key)) return true;
|
|
25
|
+
// event handlers
|
|
26
|
+
if (key.length > 2 && key.startsWith("on")) return true;
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mergeProp = (a: unknown, b: unknown, key: string): unknown => {
|
|
31
|
+
if (typeof a === "undefined") return b;
|
|
32
|
+
if (typeof b === "undefined") return a;
|
|
33
|
+
if (a === null && b !== null) return b;
|
|
34
|
+
if (b === null && a !== null) return a;
|
|
35
|
+
|
|
36
|
+
if (key === "children") return a;
|
|
37
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
38
|
+
const z = zipMax(a, b);
|
|
39
|
+
return unique(z.map(([x, y]) => mergeProp(x, y, key)));
|
|
40
|
+
}
|
|
41
|
+
if (isReference(a) && isReference(b)) {
|
|
42
|
+
return derived(() => mergeProp(a.get(), b.get(), key));
|
|
43
|
+
}
|
|
44
|
+
if (typeof a === "function" && typeof b === "function") {
|
|
45
|
+
return (...args: any[]) => mergeProp(a(...args), b(...args), key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (key === "class") {
|
|
49
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
50
|
+
return unique([a, b]).join(" ").trim();
|
|
51
|
+
} else if (Array.isArray(a) && typeof b === "string") {
|
|
52
|
+
if (a.every((v) => typeof v === "string") || a.length <= 0) {
|
|
53
|
+
return unique([...a, b]);
|
|
54
|
+
}
|
|
55
|
+
} else if (typeof a === "string" && Array.isArray(b)) {
|
|
56
|
+
if (b.every((v) => typeof v === "string") || b.length <= 0) {
|
|
57
|
+
return unique([a, ...b]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return a ?? b;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const mergeProps = (
|
|
66
|
+
a: TreeNodeProps,
|
|
67
|
+
b: TreeNodeProps,
|
|
68
|
+
): TreeNodeProps => {
|
|
69
|
+
const merged: TreeNodeProps = { ...a };
|
|
70
|
+
|
|
71
|
+
const keys = unique([...Object.keys(a), ...Object.keys(b)]);
|
|
72
|
+
|
|
73
|
+
for (const k of keys) {
|
|
74
|
+
const valueA = a[k];
|
|
75
|
+
const valueB = b[k];
|
|
76
|
+
merged[k] = mergeProp(valueA, valueB, k);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return merged;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const forwardProps = (
|
|
83
|
+
a: TreeNodeProps,
|
|
84
|
+
b: TreeNodeProps,
|
|
85
|
+
): TreeNodeProps => {
|
|
86
|
+
const merged: TreeNodeProps = { ...a };
|
|
87
|
+
|
|
88
|
+
const keys = unique([...Object.keys(a), ...Object.keys(b)]).filter((x) =>
|
|
89
|
+
isForwardedProp(x),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
for (const k of keys) {
|
|
93
|
+
const valueA = a[k];
|
|
94
|
+
const valueB = b[k];
|
|
95
|
+
merged[k] = mergeProp(valueA, valueB, k);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return merged;
|
|
99
|
+
};
|
package/src/props.ts~
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
derived,
|
|
3
|
+
isReference,
|
|
4
|
+
LooseDict,
|
|
5
|
+
unique,
|
|
6
|
+
zipMax,
|
|
7
|
+
} from "@derivesome/core";
|
|
8
|
+
|
|
9
|
+
export type TreeNodeProps = LooseDict & {
|
|
10
|
+
children?: unknown;
|
|
11
|
+
style?: unknown;
|
|
12
|
+
id?: unknown;
|
|
13
|
+
key?: string | number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const FORWARDED_PROPS: Set<string> = new Set([
|
|
17
|
+
"class",
|
|
18
|
+
"style",
|
|
19
|
+
"key",
|
|
20
|
+
"id",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export const isForwardedProp = (key: string): boolean => {
|
|
24
|
+
if (FORWARDED_PROPS.has(key)) return true;
|
|
25
|
+
// event handlers
|
|
26
|
+
if (key.length > 2 && key.startsWith("on")) return true;
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mergeProp = (a: unknown, b: unknown, key: string): unknown => {
|
|
31
|
+
if (typeof a === "undefined") return b;
|
|
32
|
+
if (typeof b === "undefined") return a;
|
|
33
|
+
if (a === null && b !== null) return b;
|
|
34
|
+
if (b === null && a !== null) return a;
|
|
35
|
+
|
|
36
|
+
if (key === "children") return a;
|
|
37
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
38
|
+
const z = zipMax(a, b);
|
|
39
|
+
return z.map(([x, y]) => mergeProp(x, y, key));
|
|
40
|
+
}
|
|
41
|
+
if (isReference(a) && isReference(b)) {
|
|
42
|
+
return derived(() => mergeProp(a.get(), b.get(), key));
|
|
43
|
+
}
|
|
44
|
+
if (typeof a === "function" && typeof b === "function") {
|
|
45
|
+
return (...args: any[]) => mergeProp(a(...args), b(...args), key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (key === "class") {
|
|
49
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
50
|
+
return [a, b].join(" ").trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return a ?? b;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const mergeProps = (
|
|
58
|
+
a: TreeNodeProps,
|
|
59
|
+
b: TreeNodeProps,
|
|
60
|
+
): TreeNodeProps => {
|
|
61
|
+
const merged: TreeNodeProps = { ...a };
|
|
62
|
+
|
|
63
|
+
const keys = unique([...Object.keys(a), ...Object.keys(b)]);
|
|
64
|
+
|
|
65
|
+
for (const k of keys) {
|
|
66
|
+
const valueA = a[k];
|
|
67
|
+
const valueB = b[k];
|
|
68
|
+
merged[k] = mergeProp(valueA, valueB, k);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return merged;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const forwardProps = (a: TreeNodeProps, b: TreeNodeProps): TreeNodeProps => {
|
|
75
|
+
const merged: TreeNodeProps = { ...a };
|
|
76
|
+
|
|
77
|
+
const keys = unique([...Object.keys(a), ...Object.keys(b)]).filter(x => isForwardedProp(x));
|
|
78
|
+
|
|
79
|
+
for (const k of keys) {
|
|
80
|
+
const valueA = a[k];
|
|
81
|
+
const valueB = b[k];
|
|
82
|
+
merged[k] = mergeProp(valueA, valueB, k);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface Renderer<N> {
|
|
2
|
+
/** Create a native element node for the given tag */
|
|
3
|
+
createElement(tag: string): N;
|
|
4
|
+
|
|
5
|
+
/** Create a native text node */
|
|
6
|
+
createText(content: string): N;
|
|
7
|
+
|
|
8
|
+
/** Create a native text node */
|
|
9
|
+
createComment(content: string): N;
|
|
10
|
+
|
|
11
|
+
/** Create an inert marker node used as anchors for dynamic ranges */
|
|
12
|
+
createMarker(name?: string): N;
|
|
13
|
+
|
|
14
|
+
/** Insert `child` before `before` in `parent`, or append if `before` is null */
|
|
15
|
+
insertBefore(parent: N, child: N, before: N | null): void;
|
|
16
|
+
|
|
17
|
+
/** Remove `child` from `parent` */
|
|
18
|
+
removeChild(parent: N, child: N): void;
|
|
19
|
+
|
|
20
|
+
/** Update a text node's string content */
|
|
21
|
+
setText(node: N, content: string): void;
|
|
22
|
+
|
|
23
|
+
/** Set a property or attribute on an element */
|
|
24
|
+
setProp(node: N, key: string, value: unknown): void;
|
|
25
|
+
|
|
26
|
+
/** Remove a property or attribute from an element */
|
|
27
|
+
removeProp(node: N, key: string): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Move a contiguous range of sibling nodes — from `startMarker` through
|
|
31
|
+
* `endMarker` inclusive, along with everything between them — to immediately
|
|
32
|
+
* before `before` in `parent`. If `before` is null, appends to `parent`.
|
|
33
|
+
*
|
|
34
|
+
* Used by the `list` reconciler to reorder keyed items without unmounting.
|
|
35
|
+
*/
|
|
36
|
+
moveRange(parent: N, startMarker: N, endMarker: N, before: N | null): void;
|
|
37
|
+
}
|
package/src/renderer.ts~
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface Renderer<N> {
|
|
2
|
+
/** Create a native element node for the given tag */
|
|
3
|
+
createElement(tag: string): N;
|
|
4
|
+
|
|
5
|
+
/** Create a native text node */
|
|
6
|
+
createText(content: string): N;
|
|
7
|
+
|
|
8
|
+
/** Create a native text node */
|
|
9
|
+
createComment(content: string): N;
|
|
10
|
+
|
|
11
|
+
/** Create an inert marker node used as anchors for dynamic ranges */
|
|
12
|
+
createMarker(name?: string): N;
|
|
13
|
+
|
|
14
|
+
/** Insert `child` before `before` in `parent`, or append if `before` is null */
|
|
15
|
+
insertBefore(parent: N, child: N, before: N | null): void;
|
|
16
|
+
|
|
17
|
+
/** Remove `child` from `parent` */
|
|
18
|
+
removeChild(parent: N, child: N): void;
|
|
19
|
+
|
|
20
|
+
/** Update a text node's string content */
|
|
21
|
+
setText(node: N, content: string): void;
|
|
22
|
+
|
|
23
|
+
/** Set a property or attribute on an element */
|
|
24
|
+
setProp(node: N, key: string, value: unknown): void;
|
|
25
|
+
|
|
26
|
+
/** Remove a property or attribute from an element */
|
|
27
|
+
removeProp(node: N, key: string): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Move a contiguous range of sibling nodes — from `startMarker` through
|
|
31
|
+
* `endMarker` inclusive, along with everything between them — to immediately
|
|
32
|
+
* before `before` in `parent`. If `before` is null, appends to `parent`.
|
|
33
|
+
*
|
|
34
|
+
* Used by the `list` reconciler to reorder keyed items without unmounting.
|
|
35
|
+
*/
|
|
36
|
+
moveRange(parent: N, startMarker: N, endMarker: N, before: N | null): void;
|
|
37
|
+
}
|