@derivesome/tree-web 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~ +10 -0
- package/.tsconfig.json.~undo-tree~ +6 -0
- package/dist/cjs/css.d.ts +5 -0
- package/dist/cjs/css.d.ts.map +1 -0
- package/dist/cjs/css.js +14 -0
- package/dist/cjs/css.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/jsx-globals.d.ts +826 -0
- package/dist/cjs/jsx-globals.d.ts.map +1 -0
- package/dist/cjs/jsx-globals.js +18 -0
- package/dist/cjs/jsx-globals.js.map +1 -0
- package/dist/cjs/mount.d.ts +2 -0
- package/dist/cjs/mount.d.ts.map +1 -0
- package/dist/cjs/mount.js +9 -0
- package/dist/cjs/mount.js.map +1 -0
- package/dist/cjs/renderer.d.ts +3 -0
- package/dist/cjs/renderer.d.ts.map +1 -0
- package/dist/cjs/renderer.js +123 -0
- package/dist/cjs/renderer.js.map +1 -0
- package/dist/esm/css.d.ts +5 -0
- package/dist/esm/css.d.ts.map +1 -0
- package/dist/esm/css.js +14 -0
- package/dist/esm/css.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/jsx-globals.d.ts +826 -0
- package/dist/esm/jsx-globals.d.ts.map +1 -0
- package/dist/esm/jsx-globals.js +18 -0
- package/dist/esm/jsx-globals.js.map +1 -0
- package/dist/esm/mount.d.ts +2 -0
- package/dist/esm/mount.d.ts.map +1 -0
- package/dist/esm/mount.js +9 -0
- package/dist/esm/mount.js.map +1 -0
- package/dist/esm/renderer.d.ts +3 -0
- package/dist/esm/renderer.d.ts.map +1 -0
- package/dist/esm/renderer.js +123 -0
- package/dist/esm/renderer.js.map +1 -0
- package/package.json +47 -0
- package/package.json~ +53 -0
- package/src/.context.ts.~undo-tree~ +6 -0
- package/src/.css.ts.~undo-tree~ +6 -0
- package/src/.index.ts.~undo-tree~ +8 -0
- package/src/.jsx-globals.ts.~undo-tree~ +192 -0
- package/src/.jsx-types.ts.~undo-tree~ +23 -0
- package/src/.mount.test.ts.~undo-tree~ +438 -0
- package/src/.mount.ts.~undo-tree~ +16 -0
- package/src/.renderer.ts.~undo-tree~ +96 -0
- package/src/.tree.ts.~undo-tree~ +489 -0
- package/src/.velement.ts.~undo-tree~ +1738 -0
- package/src/context.ts~ +0 -0
- package/src/css.ts +13 -0
- package/src/css.ts~ +13 -0
- package/src/index.ts +4 -0
- package/src/index.ts~ +4 -0
- package/src/jsx-globals.ts +861 -0
- package/src/jsx-globals.ts~ +854 -0
- package/src/jsx-types.ts~ +772 -0
- package/src/mount.test.ts~ +375 -0
- package/src/mount.ts +6 -0
- package/src/mount.ts~ +10 -0
- package/src/renderer.ts +133 -0
- package/src/renderer.ts~ +133 -0
- package/src/tree.ts~ +212 -0
- package/src/velement.ts~ +856 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +23 -0
- package/tsconfig.json~ +23 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as F from './velement';
|
|
2
|
+
import { Renderer } from './renderer';
|
|
3
|
+
import { describe, expect, assert } from 'vitest';
|
|
4
|
+
import { mount } from './mount';
|
|
5
|
+
import { tn } from './tree';
|
|
6
|
+
import { derived, ref } from '@derivesome/core';
|
|
7
|
+
|
|
8
|
+
type BrowserEnv = {
|
|
9
|
+
doc: F.VDocument;
|
|
10
|
+
root: F.VElement;
|
|
11
|
+
renderer: Renderer<F.VNode>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const createEnv = (): BrowserEnv => {
|
|
16
|
+
const doc = F.createDocument();
|
|
17
|
+
const root = doc.createElement('div');
|
|
18
|
+
|
|
19
|
+
const renderer: Renderer<F.VNode> = {
|
|
20
|
+
createComment: (content) => doc.createComment(content),
|
|
21
|
+
createText: (content) => doc.createTextNode(content),
|
|
22
|
+
createElement: (tag) => doc.createElement(tag),
|
|
23
|
+
createMarker: (name) => doc.createComment(name || '?'),
|
|
24
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
25
|
+
removeChild: (parent, child) => parent.removeChild(child),
|
|
26
|
+
setProp: (node, key, value) => (node as F.VElement).setAttribute(key, value + ''),
|
|
27
|
+
removeProp: (node, key) => (node as F.VElement).removeAttribute(key),
|
|
28
|
+
setText: (node, text) => (node as F.VText).data = text,
|
|
29
|
+
moveRange: (parent, startMarker, endMarker, before) => {
|
|
30
|
+
const list = parent.childNodes;
|
|
31
|
+
const startIdx = list.indexOf(startMarker);
|
|
32
|
+
const endIdx = list.indexOf(endMarker);
|
|
33
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
34
|
+
const range = list.splice(startIdx, endIdx - startIdx + 1);
|
|
35
|
+
const insertAt =
|
|
36
|
+
before === null ? list.length : list.indexOf(before);
|
|
37
|
+
list.splice(insertAt, 0, ...range);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
doc,
|
|
44
|
+
renderer,
|
|
45
|
+
root
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
describe('mount', it => {
|
|
51
|
+
it('mounts simple text node', () => {
|
|
52
|
+
const env = createEnv();
|
|
53
|
+
mount(tn.value('foobar'), env.renderer, env.root);
|
|
54
|
+
|
|
55
|
+
expect(env.root.childNodes.length === 1);
|
|
56
|
+
const child = env.root.childNodes[0]!;
|
|
57
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
58
|
+
expect(child).toMatchObject({ data: 'foobar' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('mounts simple number node', () => {
|
|
62
|
+
const env = createEnv();
|
|
63
|
+
mount(tn.value(203), env.renderer, env.root);
|
|
64
|
+
|
|
65
|
+
expect(env.root.childNodes.length === 1);
|
|
66
|
+
const child = env.root.childNodes[0]!;
|
|
67
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
68
|
+
expect(child).toMatchObject({ data: '203' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('mounts simple true node', () => {
|
|
72
|
+
const env = createEnv();
|
|
73
|
+
mount(tn.value(true), env.renderer, env.root);
|
|
74
|
+
|
|
75
|
+
expect(env.root.childNodes.length === 1);
|
|
76
|
+
const child = env.root.childNodes[0]!;
|
|
77
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
78
|
+
expect(child).toMatchObject({ data: '' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('mounts simple false node', () => {
|
|
82
|
+
const env = createEnv();
|
|
83
|
+
mount(tn.value(false), env.renderer, env.root);
|
|
84
|
+
|
|
85
|
+
expect(env.root.childNodes.length === 1);
|
|
86
|
+
const child = env.root.childNodes[0]!;
|
|
87
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
88
|
+
expect(child).toMatchObject({ data: '' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('mounts reactive number and response to updates', () => {
|
|
92
|
+
const env = createEnv();
|
|
93
|
+
const count = ref<number>(0);
|
|
94
|
+
mount(tn.value(count), env.renderer, env.root);
|
|
95
|
+
|
|
96
|
+
expect(env.root.childNodes.length === 1);
|
|
97
|
+
expect(env.root.childNodes[0]!).toBeInstanceOf(F.VText);
|
|
98
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: '0' });
|
|
99
|
+
|
|
100
|
+
count.set(x => x + 1);
|
|
101
|
+
|
|
102
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: '1' });
|
|
103
|
+
|
|
104
|
+
count.set(x => x + 1);
|
|
105
|
+
|
|
106
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: '2' });
|
|
107
|
+
|
|
108
|
+
count.set(x => x + 1);
|
|
109
|
+
|
|
110
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: '3' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('mounts a list', () => {
|
|
114
|
+
const env = createEnv();
|
|
115
|
+
mount(tn.element('ul', {
|
|
116
|
+
children: [
|
|
117
|
+
tn.element('li', { children: 'item 1', key: 0 }),
|
|
118
|
+
tn.element('li', { children: 'item 2', key: 1 }),
|
|
119
|
+
tn.element('li', { children: 'item 3', key: 2 }),
|
|
120
|
+
tn.element('li', { children: 'item 4', key: 3 }),
|
|
121
|
+
]
|
|
122
|
+
}), env.renderer, env.root);
|
|
123
|
+
expect(env.root.childNodes.length === 1);
|
|
124
|
+
const child = env.root.childNodes[0]!;
|
|
125
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('mounts a reactive list', () => {
|
|
129
|
+
const env = createEnv();
|
|
130
|
+
|
|
131
|
+
let idCounter: number = 1;
|
|
132
|
+
type TodoItem = { title: string, id: number };
|
|
133
|
+
const todos = ref<TodoItem[]>([]);
|
|
134
|
+
|
|
135
|
+
const makeTodo = () => todos.set((items) => [...items, { title: `Todo number ${idCounter}`, id: idCounter++ }]);
|
|
136
|
+
const removeTodo = (id: number) => todos.set(items => items.filter(x => x.id !== id));
|
|
137
|
+
|
|
138
|
+
mount(tn.element('ul', {
|
|
139
|
+
children: tn.list(todos, (todo) => tn.element('li', { children: todo.title }))
|
|
140
|
+
}), env.renderer, env.root);
|
|
141
|
+
expect(env.root.childNodes.length === 1);
|
|
142
|
+
const child = env.root.childNodes[0]!;
|
|
143
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
144
|
+
const ul = child as F.VElement;
|
|
145
|
+
|
|
146
|
+
expect(ul.childNodes.length === 0);
|
|
147
|
+
makeTodo();
|
|
148
|
+
expect(ul.childNodes.length === 1);
|
|
149
|
+
makeTodo();
|
|
150
|
+
expect(ul.childNodes.length === 2);
|
|
151
|
+
makeTodo();
|
|
152
|
+
expect(ul.childNodes.length === 3);
|
|
153
|
+
|
|
154
|
+
// The expected sequence of todo id's right now is [1, 2, 3] (since id's start at 1)
|
|
155
|
+
const expectedIds: number[] = [1, 2, 3]
|
|
156
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
157
|
+
const el = ul.children[i]!;
|
|
158
|
+
expect(el.textContent).toBe(`Todo number ${expectedIds[i]}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
removeTodo(2);
|
|
162
|
+
|
|
163
|
+
expect(ul.childNodes.length === 2);
|
|
164
|
+
|
|
165
|
+
// The expected sequence of todo id's right now is [1, 3] (since we removed 2)
|
|
166
|
+
const nextExpectedIds: number[] = [1, 3];
|
|
167
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
168
|
+
const el = ul.children[i]!;
|
|
169
|
+
expect(el.textContent).toBe(`Todo number ${nextExpectedIds[i]}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('infers a reactive list from a derived and mounts', () => {
|
|
175
|
+
const env = createEnv();
|
|
176
|
+
|
|
177
|
+
let idCounter: number = 1;
|
|
178
|
+
type TodoItem = { title: string, id: number };
|
|
179
|
+
const todos = ref<TodoItem[]>([]);
|
|
180
|
+
|
|
181
|
+
const makeTodo = () => todos.set((items) => [...items, { title: `Todo number ${idCounter}`, id: idCounter++ }]);
|
|
182
|
+
const removeTodo = (id: number) => todos.set(items => items.filter(x => x.id !== id));
|
|
183
|
+
|
|
184
|
+
mount(tn.element('ul', {
|
|
185
|
+
children: derived(() => todos.get().map(todo => tn.element('li', { children: todo.title })))
|
|
186
|
+
}), env.renderer, env.root);
|
|
187
|
+
expect(env.root.childNodes.length === 1);
|
|
188
|
+
const child = env.root.childNodes[0]!;
|
|
189
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
190
|
+
const ul = child as F.VElement;
|
|
191
|
+
|
|
192
|
+
expect(ul.childNodes.length === 0);
|
|
193
|
+
makeTodo();
|
|
194
|
+
expect(ul.childNodes.length === 1);
|
|
195
|
+
makeTodo();
|
|
196
|
+
expect(ul.childNodes.length === 2);
|
|
197
|
+
makeTodo();
|
|
198
|
+
expect(ul.childNodes.length === 3);
|
|
199
|
+
|
|
200
|
+
// The expected sequence of todo id's right now is [1, 2, 3] (since id's start at 1)
|
|
201
|
+
const expectedIds: number[] = [1, 2, 3]
|
|
202
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
203
|
+
const el = ul.children[i]!;
|
|
204
|
+
expect(el.textContent).toBe(`Todo number ${expectedIds[i]}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
removeTodo(2);
|
|
208
|
+
|
|
209
|
+
expect(ul.childNodes.length === 2);
|
|
210
|
+
|
|
211
|
+
// The expected sequence of todo id's right now is [1, 3] (since we removed 2)
|
|
212
|
+
const nextExpectedIds: number[] = [1, 3];
|
|
213
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
214
|
+
const el = ul.children[i]!;
|
|
215
|
+
expect(el.textContent).toBe(`Todo number ${nextExpectedIds[i]}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(F.toHTML(ul, ' '));
|
|
219
|
+
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('sets static element attributes', () => {
|
|
223
|
+
const env = createEnv();
|
|
224
|
+
mount(tn.element('a', { href: 'https://example.com', id: 'link' }), env.renderer, env.root);
|
|
225
|
+
|
|
226
|
+
const el = env.root.childNodes[0]! as F.VElement;
|
|
227
|
+
expect(el).toBeInstanceOf(F.VElement);
|
|
228
|
+
expect(el.getAttribute('href')).toBe('https://example.com');
|
|
229
|
+
expect(el.getAttribute('id')).toBe('link');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('updates reactive element attribute when ref changes', () => {
|
|
233
|
+
const env = createEnv();
|
|
234
|
+
const disabled = ref<boolean | null>(null);
|
|
235
|
+
mount(tn.element('button', { disabled }), env.renderer, env.root);
|
|
236
|
+
|
|
237
|
+
const el = env.root.childNodes[0]! as F.VElement;
|
|
238
|
+
expect(el.hasAttribute('disabled')).toBe(false);
|
|
239
|
+
|
|
240
|
+
disabled.set(true);
|
|
241
|
+
expect(el.getAttribute('disabled')).toBe('true');
|
|
242
|
+
|
|
243
|
+
disabled.set(null);
|
|
244
|
+
expect(el.hasAttribute('disabled')).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('mounts nested elements', () => {
|
|
248
|
+
const env = createEnv();
|
|
249
|
+
mount(
|
|
250
|
+
tn.element('div', {
|
|
251
|
+
children: tn.element('p', {
|
|
252
|
+
children: tn.element('span', { children: 'hello' })
|
|
253
|
+
})
|
|
254
|
+
}),
|
|
255
|
+
env.renderer,
|
|
256
|
+
env.root,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const div = env.root.childNodes[0]! as F.VElement;
|
|
260
|
+
expect(div).toBeInstanceOf(F.VElement);
|
|
261
|
+
const p = div.children[0]!;
|
|
262
|
+
expect(p.tagName).toBe('P');
|
|
263
|
+
const span = p.children[0]!;
|
|
264
|
+
expect(span.tagName).toBe('SPAN');
|
|
265
|
+
expect(span.textContent).toBe('hello');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('mounts a void node without adding children', () => {
|
|
269
|
+
const env = createEnv();
|
|
270
|
+
mount(tn.none(), env.renderer, env.root);
|
|
271
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('mounts a function component', () => {
|
|
275
|
+
const env = createEnv();
|
|
276
|
+
const Greeting = (props: { name: string }) =>
|
|
277
|
+
tn.element('span', { children: `Hello, ${props.name}` });
|
|
278
|
+
|
|
279
|
+
mount(tn.fn(Greeting as any, { name: 'world' }), env.renderer, env.root);
|
|
280
|
+
|
|
281
|
+
const span = env.root.childNodes[0]! as F.VElement;
|
|
282
|
+
expect(span).toBeInstanceOf(F.VElement);
|
|
283
|
+
expect(span.textContent).toBe('Hello, world');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('cleanup removes mounted nodes from the DOM', () => {
|
|
287
|
+
const env = createEnv();
|
|
288
|
+
const cleanup = mount(tn.element('p', { children: 'bye' }), env.renderer, env.root);
|
|
289
|
+
|
|
290
|
+
expect(env.root.childNodes.length).toBe(1);
|
|
291
|
+
cleanup();
|
|
292
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('cleanup stops reactive updates', () => {
|
|
296
|
+
const env = createEnv();
|
|
297
|
+
const count = ref(0);
|
|
298
|
+
const cleanup = mount(tn.value(count), env.renderer, env.root);
|
|
299
|
+
|
|
300
|
+
const textNode = env.root.childNodes[0]! as F.VText;
|
|
301
|
+
expect(textNode.data).toBe('0');
|
|
302
|
+
|
|
303
|
+
count.set(1);
|
|
304
|
+
expect(textNode.data).toBe('1');
|
|
305
|
+
|
|
306
|
+
cleanup();
|
|
307
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
308
|
+
|
|
309
|
+
// Updating the ref after cleanup should not throw and should not
|
|
310
|
+
// affect the (already removed) text node.
|
|
311
|
+
count.set(2);
|
|
312
|
+
expect(textNode.data).toBe('1');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('reorders list items when source order changes (explicit keys)', () => {
|
|
316
|
+
const env = createEnv();
|
|
317
|
+
type Item = { id: number; label: string };
|
|
318
|
+
const items = ref<Item[]>([
|
|
319
|
+
{ id: 1, label: 'A' },
|
|
320
|
+
{ id: 2, label: 'B' },
|
|
321
|
+
{ id: 3, label: 'C' },
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
mount(
|
|
325
|
+
tn.element('ul', {
|
|
326
|
+
children: tn.list(items, (item) =>
|
|
327
|
+
tn.element('li', { children: item.label, key: item.id }),
|
|
328
|
+
),
|
|
329
|
+
}),
|
|
330
|
+
env.renderer,
|
|
331
|
+
env.root,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const ul = env.root.childNodes[0]! as F.VElement;
|
|
335
|
+
const labelsOf = () => ul.children.map((c) => c.textContent);
|
|
336
|
+
|
|
337
|
+
expect(labelsOf()).toEqual(['A', 'B', 'C']);
|
|
338
|
+
|
|
339
|
+
// Reverse the order
|
|
340
|
+
items.set([
|
|
341
|
+
{ id: 3, label: 'C' },
|
|
342
|
+
{ id: 1, label: 'A' },
|
|
343
|
+
{ id: 2, label: 'B' },
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
expect(labelsOf()).toEqual(['C', 'A', 'B']);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('preserves existing DOM nodes when prepending to a keyed list', () => {
|
|
350
|
+
const env = createEnv();
|
|
351
|
+
type Item = { id: number; label: string };
|
|
352
|
+
const items = ref<Item[]>([{ id: 2, label: 'B' }]);
|
|
353
|
+
|
|
354
|
+
mount(
|
|
355
|
+
tn.element('ul', {
|
|
356
|
+
children: tn.list(items, (item) =>
|
|
357
|
+
tn.element('li', { children: item.label, key: item.id }),
|
|
358
|
+
),
|
|
359
|
+
}),
|
|
360
|
+
env.renderer,
|
|
361
|
+
env.root,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const ul = env.root.childNodes[0]! as F.VElement;
|
|
365
|
+
const originalB = ul.children[0]!;
|
|
366
|
+
|
|
367
|
+
items.set([{ id: 1, label: 'A' }, { id: 2, label: 'B' }]);
|
|
368
|
+
|
|
369
|
+
expect(ul.children.length).toBe(2);
|
|
370
|
+
expect(ul.children[0]!.textContent).toBe('A');
|
|
371
|
+
expect(ul.children[1]!.textContent).toBe('B');
|
|
372
|
+
// The B element should be the same DOM node (not recreated)
|
|
373
|
+
expect(ul.children[1]).toBe(originalB);
|
|
374
|
+
});
|
|
375
|
+
})
|
package/src/mount.ts
ADDED
package/src/mount.ts~
ADDED
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { makeGlobalPrefixed } from "@derivesome/core";
|
|
2
|
+
import { type Renderer } from "@derivesome/tree";
|
|
3
|
+
import { cssPropsToString } from "./css";
|
|
4
|
+
|
|
5
|
+
const eventPropRe = /^on([A-Z][a-zA-Z]*)/;
|
|
6
|
+
|
|
7
|
+
function propToEventName(key: string): string | null {
|
|
8
|
+
const m = eventPropRe.exec(key);
|
|
9
|
+
return m ? m[1]!.toLowerCase() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const renderer: Renderer<Node> = {
|
|
13
|
+
createComment: (content) => document.createComment(content),
|
|
14
|
+
createText: (content) => document.createTextNode(content),
|
|
15
|
+
createElement: (tag) => document.createElement(tag),
|
|
16
|
+
createMarker: (name) => document.createComment(name || "?"),
|
|
17
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
18
|
+
removeChild: (parent, child) => parent.removeChild(child),
|
|
19
|
+
setText: (node, text) => ((node as Text).data = text),
|
|
20
|
+
moveRange: (parent, startMarker, endMarker, before) => {
|
|
21
|
+
const list = Array.from(parent.childNodes) as Node[];
|
|
22
|
+
const startIdx = list.indexOf(startMarker);
|
|
23
|
+
const endIdx = list.indexOf(endMarker);
|
|
24
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
25
|
+
const range = list.splice(startIdx, endIdx - startIdx + 1);
|
|
26
|
+
const insertAt = before === null ? list.length : list.indexOf(before);
|
|
27
|
+
list.splice(insertAt, 0, ...range);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
setProp(node: globalThis.Node, key: string, value: unknown): void {
|
|
31
|
+
if (!(node instanceof Element)) return;
|
|
32
|
+
|
|
33
|
+
const eventName = propToEventName(key);
|
|
34
|
+
if (eventName !== null) {
|
|
35
|
+
// Store listener on the element so we can clean it up
|
|
36
|
+
const stored = getStoredListeners(node);
|
|
37
|
+
const prev = stored.get(key);
|
|
38
|
+
if (prev) node.removeEventListener(eventName, prev);
|
|
39
|
+
const listener = value as EventListenerOrEventListenerObject;
|
|
40
|
+
node.addEventListener(eventName, listener);
|
|
41
|
+
stored.set(key, listener);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (key === "class" || key === "className") {
|
|
46
|
+
node.setAttribute("class", String(value));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key === "style") {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
(node as HTMLElement).style.cssText = value;
|
|
53
|
+
} else if (typeof value === "object" && value !== null) {
|
|
54
|
+
(node as Element).setAttribute(key, cssPropsToString(value));
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key in node) {
|
|
60
|
+
// Prefer IDL attribute (e.g. value, checked, disabled)
|
|
61
|
+
(node as unknown as Record<string, unknown>)[key] = value;
|
|
62
|
+
} else {
|
|
63
|
+
node.setAttribute(key, String(value));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
removeProp(node: globalThis.Node, key: string): void {
|
|
68
|
+
if (!(node instanceof Element)) return;
|
|
69
|
+
|
|
70
|
+
const eventName = propToEventName(key);
|
|
71
|
+
if (eventName !== null) {
|
|
72
|
+
const stored = getStoredListeners(node);
|
|
73
|
+
const prev = stored.get(key);
|
|
74
|
+
if (prev) {
|
|
75
|
+
node.removeEventListener(eventName, prev);
|
|
76
|
+
stored.delete(key);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (key === "class" || key === "className") {
|
|
82
|
+
node.removeAttribute("class");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (key === "style") {
|
|
87
|
+
(node as HTMLElement).removeAttribute("style");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (key in node) {
|
|
92
|
+
(node as unknown as Record<string, unknown>)[key] = null;
|
|
93
|
+
} else {
|
|
94
|
+
node.removeAttribute(key);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
//moveRange(
|
|
98
|
+
// parent: globalThis.Node,
|
|
99
|
+
// startMarker: globalThis.Node,
|
|
100
|
+
// endMarker: globalThis.Node,
|
|
101
|
+
// before: globalThis.Node | null,
|
|
102
|
+
//): void {
|
|
103
|
+
// // Collect nodes from startMarker to endMarker inclusive
|
|
104
|
+
// const nodes: globalThis.Node[] = [];
|
|
105
|
+
// let current: globalThis.ChildNode | null =
|
|
106
|
+
// startMarker as globalThis.ChildNode;
|
|
107
|
+
// while (current !== null) {
|
|
108
|
+
// nodes.push(current);
|
|
109
|
+
// if (current === endMarker) break;
|
|
110
|
+
// current = current.nextSibling;
|
|
111
|
+
// }
|
|
112
|
+
// // Re-insert in collected order before the target position
|
|
113
|
+
// for (const n of nodes) {
|
|
114
|
+
// parent.insertBefore(n, before);
|
|
115
|
+
// }
|
|
116
|
+
//},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const listenerMap = makeGlobalPrefixed(
|
|
120
|
+
new WeakMap<Element, Map<string, EventListenerOrEventListenerObject>>(),
|
|
121
|
+
"EventListenerMap",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
function getStoredListeners(
|
|
125
|
+
el: Element,
|
|
126
|
+
): Map<string, EventListenerOrEventListenerObject> {
|
|
127
|
+
let m = listenerMap.get(el);
|
|
128
|
+
if (!m) {
|
|
129
|
+
m = new Map();
|
|
130
|
+
listenerMap.set(el, m);
|
|
131
|
+
}
|
|
132
|
+
return m;
|
|
133
|
+
}
|
package/src/renderer.ts~
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { makeGlobalPrefixed } from "@derivesome/core";
|
|
2
|
+
import { type Renderer } from "@derivesome/tree";
|
|
3
|
+
import { cssPropsToString } from "./css";
|
|
4
|
+
|
|
5
|
+
const eventPropRe = /^on([A-Z][a-zA-Z]*)/;
|
|
6
|
+
|
|
7
|
+
function propToEventName(key: string): string | null {
|
|
8
|
+
const m = eventPropRe.exec(key);
|
|
9
|
+
return m ? m[1]!.toLowerCase() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const renderer: Renderer<Node> = {
|
|
13
|
+
createComment: (content) => document.createComment(content),
|
|
14
|
+
createText: (content) => document.createTextNode(content),
|
|
15
|
+
createElement: (tag) => document.createElement(tag),
|
|
16
|
+
createMarker: (name) => document.createComment(name || "?"),
|
|
17
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
18
|
+
removeChild: (parent, child) => parent.removeChild(child),
|
|
19
|
+
setText: (node, text) => ((node as Text).data = text),
|
|
20
|
+
moveRange: (parent, startMarker, endMarker, before) => {
|
|
21
|
+
const list = Array.from(parent.childNodes) as Node[];
|
|
22
|
+
const startIdx = list.indexOf(startMarker);
|
|
23
|
+
const endIdx = list.indexOf(endMarker);
|
|
24
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
25
|
+
const range = list.splice(startIdx, endIdx - startIdx + 1);
|
|
26
|
+
const insertAt = before === null ? list.length : list.indexOf(before);
|
|
27
|
+
list.splice(insertAt, 0, ...range);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
setProp(node: globalThis.Node, key: string, value: unknown): void {
|
|
31
|
+
if (!(node instanceof Element)) return;
|
|
32
|
+
|
|
33
|
+
const eventName = propToEventName(key);
|
|
34
|
+
if (eventName !== null) {
|
|
35
|
+
// Store listener on the element so we can clean it up
|
|
36
|
+
const stored = getStoredListeners(node);
|
|
37
|
+
const prev = stored.get(key);
|
|
38
|
+
if (prev) node.removeEventListener(eventName, prev);
|
|
39
|
+
const listener = value as EventListenerOrEventListenerObject;
|
|
40
|
+
node.addEventListener(eventName, listener);
|
|
41
|
+
stored.set(key, listener);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (key === "class" || key === "className") {
|
|
46
|
+
node.setAttribute("class", String(value));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key === "style") {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
(node as HTMLElement).style.cssText = value;
|
|
53
|
+
} else if (typeof value === "object" && value !== null) {
|
|
54
|
+
(node as Element).setAttribute(key, cssPropsToString(value));
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key in node) {
|
|
60
|
+
// Prefer IDL attribute (e.g. value, checked, disabled)
|
|
61
|
+
(node as unknown as Record<string, unknown>)[key] = value;
|
|
62
|
+
} else {
|
|
63
|
+
node.setAttribute(key, String(value));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
removeProp(node: globalThis.Node, key: string): void {
|
|
68
|
+
if (!(node instanceof Element)) return;
|
|
69
|
+
|
|
70
|
+
const eventName = propToEventName(key);
|
|
71
|
+
if (eventName !== null) {
|
|
72
|
+
const stored = getStoredListeners(node);
|
|
73
|
+
const prev = stored.get(key);
|
|
74
|
+
if (prev) {
|
|
75
|
+
node.removeEventListener(eventName, prev);
|
|
76
|
+
stored.delete(key);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (key === "class" || key === "className") {
|
|
82
|
+
node.removeAttribute("class");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (key === "style") {
|
|
87
|
+
(node as HTMLElement).removeAttribute("style");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (key in node) {
|
|
92
|
+
(node as unknown as Record<string, unknown>)[key] = null;
|
|
93
|
+
} else {
|
|
94
|
+
node.removeAttribute(key);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
//moveRange(
|
|
98
|
+
// parent: globalThis.Node,
|
|
99
|
+
// startMarker: globalThis.Node,
|
|
100
|
+
// endMarker: globalThis.Node,
|
|
101
|
+
// before: globalThis.Node | null,
|
|
102
|
+
//): void {
|
|
103
|
+
// // Collect nodes from startMarker to endMarker inclusive
|
|
104
|
+
// const nodes: globalThis.Node[] = [];
|
|
105
|
+
// let current: globalThis.ChildNode | null =
|
|
106
|
+
// startMarker as globalThis.ChildNode;
|
|
107
|
+
// while (current !== null) {
|
|
108
|
+
// nodes.push(current);
|
|
109
|
+
// if (current === endMarker) break;
|
|
110
|
+
// current = current.nextSibling;
|
|
111
|
+
// }
|
|
112
|
+
// // Re-insert in collected order before the target position
|
|
113
|
+
// for (const n of nodes) {
|
|
114
|
+
// parent.insertBefore(n, before);
|
|
115
|
+
// }
|
|
116
|
+
//},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const listenerMap = makeGlobalPrefixed(new WeakMap<
|
|
120
|
+
Element,
|
|
121
|
+
Map<string, EventListenerOrEventListenerObject>
|
|
122
|
+
>(), 'EventListenerMap');
|
|
123
|
+
|
|
124
|
+
function getStoredListeners(
|
|
125
|
+
el: Element,
|
|
126
|
+
): Map<string, EventListenerOrEventListenerObject> {
|
|
127
|
+
let m = listenerMap.get(el);
|
|
128
|
+
if (!m) {
|
|
129
|
+
m = new Map();
|
|
130
|
+
listenerMap.set(el, m);
|
|
131
|
+
}
|
|
132
|
+
return m;
|
|
133
|
+
}
|