@esportsplus/template 0.16.14 → 0.17.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/README.md +33 -1
- package/bench/runtime.bench.ts +207 -0
- package/build/attributes.js +4 -1
- package/build/compiler/plugins/vite.d.ts +8 -4
- package/build/compiler/plugins/vite.js +37 -2
- package/build/hmr.d.ts +10 -0
- package/build/hmr.js +42 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/slot/array.js +69 -4
- package/build/slot/effect.d.ts +3 -3
- package/build/slot/effect.js +36 -17
- package/build/slot/render.js +1 -2
- package/build/utilities.d.ts +2 -1
- package/build/utilities.js +2 -1
- package/llm.txt +63 -4
- package/package.json +2 -1
- package/src/attributes.ts +4 -1
- package/src/compiler/plugins/vite.ts +74 -5
- package/src/hmr.ts +70 -0
- package/src/index.ts +1 -0
- package/src/slot/array.ts +104 -4
- package/src/slot/effect.ts +46 -20
- package/src/slot/render.ts +1 -4
- package/src/utilities.ts +3 -1
- package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
- package/tests/compiler/codegen.ts +292 -0
- package/tests/compiler/integration.ts +252 -0
- package/tests/compiler/ts-parser.ts +160 -0
- package/tests/compiler/vite-hmr.ts +126 -0
- package/{test/constants.test.ts → tests/constants.ts} +5 -1
- package/tests/event/onconnect.ts +147 -0
- package/tests/event/onresize.ts +187 -0
- package/tests/event/ontick.ts +273 -0
- package/tests/hmr.ts +146 -0
- package/{test/slot/array.test.ts → tests/slot/array.ts} +475 -0
- package/tests/slot/async.ts +389 -0
- package/vitest.bench.config.ts +18 -0
- package/vitest.config.ts +1 -1
- package/storage/compiler-architecture-2026-01-13.md +0 -420
- /package/{test → examples}/index.ts +0 -0
- /package/{test → examples}/vite.config.ts +0 -0
- /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
- /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
- /package/{test → tests}/dist/test.js +0 -0
- /package/{test → tests}/dist/test.js.map +0 -0
- /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
- /package/{test/html.test.ts → tests/html.ts} +0 -0
- /package/{test/render.test.ts → tests/render.ts} +0 -0
- /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
- /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
- /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
- /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
- /package/{test/svg.test.ts → tests/svg.ts} +0 -0
- /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ High-performance, compiler-optimized HTML templating library for JavaScript/Type
|
|
|
9
9
|
- **Reactive integration** - Works with `@esportsplus/reactivity` for dynamic updates
|
|
10
10
|
- **Event delegation** - Efficient event handling with automatic delegation
|
|
11
11
|
- **Lifecycle events** - `onconnect`, `ondisconnect`, `onrender`, `onresize`, `ontick`
|
|
12
|
+
- **Async slots** - Async function support with fallback content in `EffectSlot`
|
|
13
|
+
- **Non-destructive reordering** - Uses `moveBefore` DOM API for array sort/reverse when available
|
|
14
|
+
- **HMR support** - Fine-grained hot module replacement for templates in development
|
|
12
15
|
- **Type-safe** - Full TypeScript support
|
|
13
16
|
|
|
14
17
|
## Installation
|
|
@@ -29,10 +32,12 @@ import { defineConfig } from 'vite';
|
|
|
29
32
|
import template from '@esportsplus/template/compiler/vite';
|
|
30
33
|
|
|
31
34
|
export default defineConfig({
|
|
32
|
-
plugins: [template]
|
|
35
|
+
plugins: [template()]
|
|
33
36
|
});
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
HMR is automatically enabled in development mode (`vite dev`). When a file containing `html` templates is saved, only the affected template factories are invalidated and re-created — no full page reload needed.
|
|
40
|
+
|
|
36
41
|
### TypeScript Compiler (tsc)
|
|
37
42
|
|
|
38
43
|
For direct `tsc` compilation, use the transformer:
|
|
@@ -179,6 +184,31 @@ const todoList = (todos: string[]) =>
|
|
|
179
184
|
html`<ul>${html.reactive(todos, todo => html`<li>${todo}</li>`)}</ul>`;
|
|
180
185
|
```
|
|
181
186
|
|
|
187
|
+
Array sort and reverse operations use the `moveBefore` DOM API when available, preserving element state (focus, animations, iframe content) during reordering. Falls back to `insertBefore` in older browsers.
|
|
188
|
+
|
|
189
|
+
### Async Slots
|
|
190
|
+
|
|
191
|
+
Async functions are supported in effect slots, with an optional fallback callback for loading states:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { html } from '@esportsplus/template';
|
|
195
|
+
|
|
196
|
+
// Async with fallback content while loading
|
|
197
|
+
const asyncContent = () =>
|
|
198
|
+
html`<div>${async (fallback: (content: any) => void) => {
|
|
199
|
+
fallback(html`<span>Loading...</span>`);
|
|
200
|
+
const data = await fetchData();
|
|
201
|
+
return html`<span>${data}</span>`;
|
|
202
|
+
}}</div>`;
|
|
203
|
+
|
|
204
|
+
// Simple async (no fallback)
|
|
205
|
+
const simpleAsync = () =>
|
|
206
|
+
html`<div>${async () => {
|
|
207
|
+
const data = await fetchData();
|
|
208
|
+
return html`<span>${data}</span>`;
|
|
209
|
+
}}</div>`;
|
|
210
|
+
```
|
|
211
|
+
|
|
182
212
|
## Events
|
|
183
213
|
|
|
184
214
|
### Standard DOM Events
|
|
@@ -270,6 +300,8 @@ const circle = (fill: string) =>
|
|
|
270
300
|
| `slot` | Slot rendering |
|
|
271
301
|
| `ArraySlot` | Reactive array rendering |
|
|
272
302
|
| `EffectSlot` | Reactive effect rendering |
|
|
303
|
+
| `accept` | HMR accept handler (dev only) |
|
|
304
|
+
| `createHotTemplate` | HMR template factory (dev only) |
|
|
273
305
|
|
|
274
306
|
### Types
|
|
275
307
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe('attributes - apply', () => {
|
|
5
|
+
let element: HTMLDivElement;
|
|
6
|
+
|
|
7
|
+
bench('setAttribute style', () => {
|
|
8
|
+
element = document.createElement('div');
|
|
9
|
+
element.setAttribute('style', 'color: red; font-size: 14px; display: flex;');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
bench('style.cssText', () => {
|
|
13
|
+
element = document.createElement('div');
|
|
14
|
+
element.style.cssText = 'color: red; font-size: 14px; display: flex;';
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
bench('className assignment', () => {
|
|
18
|
+
element = document.createElement('div');
|
|
19
|
+
element.className = 'foo bar baz qux';
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
bench('setAttribute class', () => {
|
|
23
|
+
element = document.createElement('div');
|
|
24
|
+
element.setAttribute('class', 'foo bar baz qux');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
describe('attributes - class list rebuild', () => {
|
|
30
|
+
bench('Set for..of + string concat', () => {
|
|
31
|
+
let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
|
|
32
|
+
result = '';
|
|
33
|
+
|
|
34
|
+
for (let key of set) {
|
|
35
|
+
result += (result ? ' ' : '') + key;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
bench('Array.from(set).join', () => {
|
|
40
|
+
let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']);
|
|
41
|
+
|
|
42
|
+
Array.from(set).join(' ');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
bench('set forEach + string concat', () => {
|
|
46
|
+
let set = new Set(['alpha', 'beta', 'gamma', 'delta', 'epsilon']),
|
|
47
|
+
result = '';
|
|
48
|
+
|
|
49
|
+
set.forEach(key => {
|
|
50
|
+
result += (result ? ' ' : '') + key;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
describe('event - defineProperty overhead', () => {
|
|
57
|
+
let event: Event;
|
|
58
|
+
|
|
59
|
+
bench('defineProperty per dispatch', () => {
|
|
60
|
+
event = new Event('click');
|
|
61
|
+
let node: HTMLElement | null = document.createElement('div');
|
|
62
|
+
|
|
63
|
+
Object.defineProperty(event, 'currentTarget', {
|
|
64
|
+
configurable: true,
|
|
65
|
+
get() {
|
|
66
|
+
return node || document;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
bench('defineProperty once + closure update', () => {
|
|
72
|
+
event = new Event('click');
|
|
73
|
+
let currentNode: HTMLElement | null = null;
|
|
74
|
+
|
|
75
|
+
Object.defineProperty(event, 'currentTarget', {
|
|
76
|
+
configurable: true,
|
|
77
|
+
get() {
|
|
78
|
+
return currentNode || document;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
currentNode = document.createElement('div');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
describe('marker - comment vs text node', () => {
|
|
88
|
+
let comment = document.createComment('$'),
|
|
89
|
+
textNode = document.createTextNode('');
|
|
90
|
+
|
|
91
|
+
bench('clone comment node', () => {
|
|
92
|
+
comment.cloneNode();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
bench('clone text node', () => {
|
|
96
|
+
textNode.cloneNode();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
describe('ontick - Set iteration', () => {
|
|
102
|
+
let tasks = new Set<VoidFunction>();
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < 10; i++) {
|
|
105
|
+
tasks.add(() => {});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bench('for..of Set', () => {
|
|
109
|
+
for (let task of tasks) {
|
|
110
|
+
task();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
bench('Set.forEach', () => {
|
|
115
|
+
tasks.forEach(task => task());
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
describe('array sync - fragment append', () => {
|
|
121
|
+
let fragment: DocumentFragment,
|
|
122
|
+
nodes: Node[];
|
|
123
|
+
|
|
124
|
+
bench('individual append', () => {
|
|
125
|
+
fragment = document.createDocumentFragment();
|
|
126
|
+
nodes = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < 50; i++) {
|
|
129
|
+
nodes.push(document.createElement('div'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let i = 0, n = nodes.length; i < n; i++) {
|
|
133
|
+
fragment.append(nodes[i]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
bench('batch append spread', () => {
|
|
138
|
+
fragment = document.createDocumentFragment();
|
|
139
|
+
nodes = [];
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < 50; i++) {
|
|
142
|
+
nodes.push(document.createElement('div'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fragment.append(...nodes);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
describe('array sort - full resync vs minimal moves', () => {
|
|
151
|
+
let parent: HTMLDivElement,
|
|
152
|
+
fragment: DocumentFragment;
|
|
153
|
+
|
|
154
|
+
bench('full detach + reattach (current)', () => {
|
|
155
|
+
parent = document.createElement('div');
|
|
156
|
+
fragment = document.createDocumentFragment();
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < 50; i++) {
|
|
159
|
+
parent.appendChild(document.createElement('span'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let children = Array.from(parent.children);
|
|
163
|
+
|
|
164
|
+
// Simulate: detach all, reattach in new order
|
|
165
|
+
for (let i = 0, n = children.length; i < n; i++) {
|
|
166
|
+
fragment.append(children[i]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
parent.appendChild(fragment);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
bench('targeted insertBefore (LIS approach)', () => {
|
|
173
|
+
parent = document.createElement('div');
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < 50; i++) {
|
|
176
|
+
parent.appendChild(document.createElement('span'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let children = Array.from(parent.children);
|
|
180
|
+
|
|
181
|
+
// Simulate: only move 5 out of 50 elements (90% stay)
|
|
182
|
+
for (let i = 0; i < 5; i++) {
|
|
183
|
+
let idx = Math.floor(Math.random() * children.length);
|
|
184
|
+
|
|
185
|
+
parent.insertBefore(children[idx], children[(idx + 10) % children.length]);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
describe('fragment - dedup empty', () => {
|
|
192
|
+
let tmpl = document.createElement('template');
|
|
193
|
+
|
|
194
|
+
bench('fragment() call', () => {
|
|
195
|
+
let element = tmpl.cloneNode() as HTMLTemplateElement;
|
|
196
|
+
|
|
197
|
+
element.innerHTML = '';
|
|
198
|
+
|
|
199
|
+
element.content;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
bench('cached fragment clone', () => {
|
|
203
|
+
let cached = document.createDocumentFragment();
|
|
204
|
+
|
|
205
|
+
cached.cloneNode(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
package/build/attributes.js
CHANGED
|
@@ -12,7 +12,10 @@ function apply(element, name, value) {
|
|
|
12
12
|
else if (name === 'class') {
|
|
13
13
|
element.className = value;
|
|
14
14
|
}
|
|
15
|
-
else if (name === 'style'
|
|
15
|
+
else if (name === 'style') {
|
|
16
|
+
element.style.cssText = value;
|
|
17
|
+
}
|
|
18
|
+
else if ((name[0] === 'd' && name.startsWith('data-')) || element['ownerSVGElement']) {
|
|
16
19
|
element.setAttribute(name, value);
|
|
17
20
|
}
|
|
18
21
|
else {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
declare const _default: ({ root }?: {
|
|
2
2
|
root?: string;
|
|
3
3
|
}) => {
|
|
4
|
-
configResolved
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
configResolved(config: any): void;
|
|
5
|
+
handleHotUpdate(_ctx: {
|
|
6
|
+
file: string;
|
|
7
|
+
modules: any[];
|
|
8
|
+
}): void;
|
|
9
|
+
transform(code: string, id: string): {
|
|
8
10
|
code: string;
|
|
9
11
|
map: null;
|
|
10
12
|
} | null;
|
|
13
|
+
enforce: "pre";
|
|
14
|
+
name: string;
|
|
11
15
|
watchChange: (id: string) => void;
|
|
12
16
|
};
|
|
13
17
|
export default _default;
|
|
@@ -1,8 +1,43 @@
|
|
|
1
|
+
import { NAMESPACE, PACKAGE_NAME } from '../constants.js';
|
|
1
2
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { PACKAGE_NAME } from '../constants.js';
|
|
3
3
|
import reactivity from '@esportsplus/reactivity/compiler';
|
|
4
4
|
import template from '../index.js';
|
|
5
|
-
|
|
5
|
+
const TEMPLATE_SEARCH = NAMESPACE + '.template(';
|
|
6
|
+
const TEMPLATE_CALL_REGEX = new RegExp('(const\\s+(\\w+)\\s*=\\s*' + NAMESPACE + '\\.template\\()(`)', 'g');
|
|
7
|
+
let base = plugin.vite({
|
|
6
8
|
name: PACKAGE_NAME,
|
|
7
9
|
plugins: [reactivity, template]
|
|
8
10
|
});
|
|
11
|
+
function injectHMR(code, id) {
|
|
12
|
+
let hmrId = id.replace(/\\/g, '/'), hotReplace = NAMESPACE + '.createHotTemplate("' + hmrId + '", "', injected = code.replace(TEMPLATE_CALL_REGEX, function (_match, prefix, varName, backtick) {
|
|
13
|
+
return prefix.replace(TEMPLATE_SEARCH, hotReplace + varName + '", ') + backtick;
|
|
14
|
+
});
|
|
15
|
+
if (injected === code) {
|
|
16
|
+
return code;
|
|
17
|
+
}
|
|
18
|
+
injected += '\nif (import.meta.hot) { import.meta.hot.accept(() => { ' + NAMESPACE + '.accept("' + hmrId + '"); }); }';
|
|
19
|
+
return injected;
|
|
20
|
+
}
|
|
21
|
+
export default ({ root } = {}) => {
|
|
22
|
+
let isDev = false, vitePlugin = base({ root });
|
|
23
|
+
return {
|
|
24
|
+
...vitePlugin,
|
|
25
|
+
configResolved(config) {
|
|
26
|
+
vitePlugin.configResolved(config);
|
|
27
|
+
isDev = config?.command === 'serve' || config?.mode === 'development';
|
|
28
|
+
},
|
|
29
|
+
handleHotUpdate(_ctx) {
|
|
30
|
+
},
|
|
31
|
+
transform(code, id) {
|
|
32
|
+
let result = vitePlugin.transform(code, id);
|
|
33
|
+
if (!result || !isDev) {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
let injected = injectHMR(result.code, id);
|
|
37
|
+
if (injected === result.code) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
return { code: injected, map: null };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
package/build/hmr.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare let modules: Map<string, Map<string, HotTemplate>>;
|
|
2
|
+
type HotTemplate = {
|
|
3
|
+
cached: DocumentFragment | undefined;
|
|
4
|
+
factory: () => DocumentFragment;
|
|
5
|
+
html: string;
|
|
6
|
+
};
|
|
7
|
+
declare const accept: (moduleId: string) => void;
|
|
8
|
+
declare const createHotTemplate: (moduleId: string, templateId: string, html: string) => () => DocumentFragment;
|
|
9
|
+
declare const hmrReset: () => void;
|
|
10
|
+
export { accept, createHotTemplate, hmrReset, modules };
|
package/build/hmr.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let clone = (node, deep = true) => node.cloneNode(deep), modules = new Map(), tmpl = typeof document !== 'undefined' ? document.createElement('template') : null;
|
|
2
|
+
function invalidate(moduleId) {
|
|
3
|
+
let templates = modules.get(moduleId);
|
|
4
|
+
if (!templates) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
for (let [, entry] of templates) {
|
|
8
|
+
entry.cached = undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function register(moduleId, templateId, html) {
|
|
12
|
+
let entry = {
|
|
13
|
+
cached: undefined,
|
|
14
|
+
factory: () => {
|
|
15
|
+
if (!entry.cached) {
|
|
16
|
+
let element = tmpl.cloneNode();
|
|
17
|
+
element.innerHTML = entry.html;
|
|
18
|
+
entry.cached = element.content;
|
|
19
|
+
}
|
|
20
|
+
return clone(entry.cached, true);
|
|
21
|
+
},
|
|
22
|
+
html
|
|
23
|
+
};
|
|
24
|
+
(modules.get(moduleId) ?? (modules.set(moduleId, new Map()), modules.get(moduleId))).set(templateId, entry);
|
|
25
|
+
return entry.factory;
|
|
26
|
+
}
|
|
27
|
+
const accept = (moduleId) => {
|
|
28
|
+
invalidate(moduleId);
|
|
29
|
+
};
|
|
30
|
+
const createHotTemplate = (moduleId, templateId, html) => {
|
|
31
|
+
let existing = modules.get(moduleId)?.get(templateId);
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.cached = undefined;
|
|
34
|
+
existing.html = html;
|
|
35
|
+
return existing.factory;
|
|
36
|
+
}
|
|
37
|
+
return register(moduleId, templateId, html);
|
|
38
|
+
};
|
|
39
|
+
const hmrReset = () => {
|
|
40
|
+
modules.clear();
|
|
41
|
+
};
|
|
42
|
+
export { accept, createHotTemplate, hmrReset, modules };
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
|
@@ -5,6 +5,7 @@ if (typeof Node !== 'undefined') {
|
|
|
5
5
|
}
|
|
6
6
|
export * from './attributes.js';
|
|
7
7
|
export * from './event/index.js';
|
|
8
|
+
export * from './hmr.js';
|
|
8
9
|
export * from './utilities.js';
|
|
9
10
|
export { default as html } from './html.js';
|
|
10
11
|
export { default as render } from './render.js';
|
package/build/slot/array.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { read, root, signal, write } from '@esportsplus/reactivity';
|
|
2
2
|
import { ARRAY_SLOT } from '../constants.js';
|
|
3
|
-
import { clone,
|
|
3
|
+
import { clone, EMPTY_FRAGMENT, marker, raf } from '../utilities.js';
|
|
4
4
|
import { ondisconnect, remove } from './cleanup.js';
|
|
5
|
-
|
|
5
|
+
function lis(arr) {
|
|
6
|
+
let n = arr.length;
|
|
7
|
+
if (n === 0) {
|
|
8
|
+
return new Set();
|
|
9
|
+
}
|
|
10
|
+
let ends = new Int32Array(n), predecessors = new Int32Array(n), len = 0;
|
|
11
|
+
for (let i = 0; i < n; i++) {
|
|
12
|
+
let lo = 0, hi = len, val = arr[i];
|
|
13
|
+
while (lo < hi) {
|
|
14
|
+
let mid = (lo + hi) >> 1;
|
|
15
|
+
if (arr[ends[mid]] < val) {
|
|
16
|
+
lo = mid + 1;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
hi = mid;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
ends[lo] = i;
|
|
23
|
+
predecessors[i] = lo > 0 ? ends[lo - 1] : -1;
|
|
24
|
+
if (lo >= len) {
|
|
25
|
+
len = lo + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let idx = ends[len - 1], result = new Set();
|
|
29
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
30
|
+
result.add(idx);
|
|
31
|
+
idx = predecessors[idx];
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
6
35
|
class ArraySlot {
|
|
7
36
|
array;
|
|
8
37
|
marker;
|
|
@@ -153,12 +182,34 @@ class ArraySlot {
|
|
|
153
182
|
this.marker.after(this.fragment);
|
|
154
183
|
return;
|
|
155
184
|
}
|
|
156
|
-
let sorted = new Array(n);
|
|
185
|
+
let end = n > 0 ? nodes[n - 1].tail.nextSibling : null, keep = lis(order), parent = this.marker.parentNode, sorted = new Array(n);
|
|
157
186
|
for (let i = 0; i < n; i++) {
|
|
158
187
|
sorted[i] = nodes[order[i]];
|
|
159
188
|
}
|
|
160
189
|
this.nodes = sorted;
|
|
161
|
-
|
|
190
|
+
if (!parent || keep.size === n) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let ref = end, useMoveBefore = 'moveBefore' in parent;
|
|
194
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
195
|
+
let group = sorted[i];
|
|
196
|
+
if (keep.has(i)) {
|
|
197
|
+
ref = group.head;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
let node = group.tail;
|
|
201
|
+
while (node) {
|
|
202
|
+
let prev = node === group.head ? null : node.previousSibling;
|
|
203
|
+
if (useMoveBefore) {
|
|
204
|
+
parent.moveBefore(node, ref);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
parent.insertBefore(node, ref);
|
|
208
|
+
}
|
|
209
|
+
ref = node;
|
|
210
|
+
node = prev;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
162
213
|
}
|
|
163
214
|
splice(start, stop = this.nodes.length, items) {
|
|
164
215
|
if (!items.length) {
|
|
@@ -173,6 +224,20 @@ class ArraySlot {
|
|
|
173
224
|
if (!n) {
|
|
174
225
|
return;
|
|
175
226
|
}
|
|
227
|
+
let parent = this.marker.parentNode;
|
|
228
|
+
if (parent && 'moveBefore' in parent) {
|
|
229
|
+
let ref = nodes[0].tail.nextSibling;
|
|
230
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
231
|
+
let group = nodes[i], node = group.tail;
|
|
232
|
+
while (node) {
|
|
233
|
+
let prev = node === group.head ? null : node.previousSibling;
|
|
234
|
+
parent.moveBefore(node, ref);
|
|
235
|
+
ref = node;
|
|
236
|
+
node = prev;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
176
241
|
for (let i = 0; i < n; i++) {
|
|
177
242
|
let group = nodes[i], next, node = group.head;
|
|
178
243
|
while (node) {
|
package/build/slot/effect.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Element,
|
|
1
|
+
import { Element, SlotGroup } from '../types.js';
|
|
2
2
|
declare class EffectSlot {
|
|
3
3
|
anchor: Element;
|
|
4
|
-
disposer: VoidFunction;
|
|
4
|
+
disposer: VoidFunction | null;
|
|
5
5
|
group: SlotGroup | null;
|
|
6
6
|
scheduled: boolean;
|
|
7
7
|
textnode: Node | null;
|
|
8
|
-
constructor(anchor: Element, fn: (
|
|
8
|
+
constructor(anchor: Element, fn: ((...args: any[]) => any));
|
|
9
9
|
dispose(): void;
|
|
10
10
|
update(value: unknown): void;
|
|
11
11
|
}
|
package/build/slot/effect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effect } from '@esportsplus/reactivity';
|
|
2
|
+
import { isAsyncFunction } from '@esportsplus/utilities';
|
|
2
3
|
import { raf, text } from '../utilities.js';
|
|
3
4
|
import { remove } from './cleanup.js';
|
|
4
5
|
import render from './render.js';
|
|
@@ -18,37 +19,47 @@ class EffectSlot {
|
|
|
18
19
|
scheduled = false;
|
|
19
20
|
textnode = null;
|
|
20
21
|
constructor(anchor, fn) {
|
|
21
|
-
let dispose = fn.length ? () => this.dispose() : undefined, value;
|
|
22
22
|
this.anchor = anchor;
|
|
23
|
-
this.disposer =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
this.disposer = null;
|
|
24
|
+
if (isAsyncFunction(fn)) {
|
|
25
|
+
fn((content) => this.update(content)).then((value) => this.update(value), () => { });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
let dispose = fn.length ? () => this.dispose() : undefined, value;
|
|
29
|
+
this.disposer = effect(() => {
|
|
30
|
+
value = read(fn(dispose));
|
|
31
|
+
if (!this.disposer) {
|
|
32
32
|
this.update(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
}
|
|
34
|
+
else if (!this.scheduled) {
|
|
35
|
+
this.scheduled = true;
|
|
36
|
+
raf(() => {
|
|
37
|
+
this.scheduled = false;
|
|
38
|
+
this.update(value);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
dispose() {
|
|
38
|
-
let { anchor, group, textnode } = this;
|
|
45
|
+
let { anchor, disposer, group, textnode } = this;
|
|
46
|
+
if (!disposer) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
39
49
|
if (textnode) {
|
|
40
50
|
group = { head: anchor, tail: textnode };
|
|
41
51
|
}
|
|
42
52
|
else if (group) {
|
|
43
53
|
group.head = anchor;
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
disposer();
|
|
46
56
|
if (group) {
|
|
47
57
|
remove(group);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
update(value) {
|
|
51
61
|
let { anchor, group, textnode } = this;
|
|
62
|
+
value = read(value);
|
|
52
63
|
if (group) {
|
|
53
64
|
remove(group);
|
|
54
65
|
this.group = null;
|
|
@@ -68,14 +79,22 @@ class EffectSlot {
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
else {
|
|
71
|
-
let fragment = render(anchor, value), head
|
|
82
|
+
let fragment = render(anchor, value), head, tail;
|
|
83
|
+
if (fragment.nodeType === 11) {
|
|
84
|
+
head = fragment.firstChild;
|
|
85
|
+
tail = fragment.lastChild;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
head = fragment;
|
|
89
|
+
tail = fragment;
|
|
90
|
+
}
|
|
72
91
|
if (textnode?.isConnected) {
|
|
73
92
|
remove({ head: textnode, tail: textnode });
|
|
74
93
|
}
|
|
75
94
|
if (head) {
|
|
76
95
|
this.group = {
|
|
77
96
|
head: head,
|
|
78
|
-
tail:
|
|
97
|
+
tail: tail
|
|
79
98
|
};
|
|
80
99
|
anchor.after(fragment);
|
|
81
100
|
}
|
package/build/slot/render.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isArray } from '@esportsplus/utilities';
|
|
2
2
|
import { ARRAY_SLOT } from '../constants.js';
|
|
3
|
-
import { clone,
|
|
4
|
-
const EMPTY_FRAGMENT = fragment('');
|
|
3
|
+
import { clone, EMPTY_FRAGMENT, text } from '../utilities.js';
|
|
5
4
|
export default function render(anchor, value) {
|
|
6
5
|
if (value == null || value === false || value === '') {
|
|
7
6
|
return EMPTY_FRAGMENT;
|
package/build/utilities.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
declare const clone: <T extends Node>(node: T, options?: boolean | ImportNodeOptions) => T;
|
|
2
2
|
declare const fragment: (html: string) => DocumentFragment;
|
|
3
|
+
declare const EMPTY_FRAGMENT: DocumentFragment;
|
|
3
4
|
declare const marker: ChildNode;
|
|
4
5
|
declare const raf: typeof requestAnimationFrame;
|
|
5
6
|
declare const template: (html: string) => () => DocumentFragment;
|
|
6
7
|
declare const text: (value: string) => Node;
|
|
7
|
-
export { clone,
|
|
8
|
+
export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
|
package/build/utilities.js
CHANGED
|
@@ -8,6 +8,7 @@ const fragment = (html) => {
|
|
|
8
8
|
element.innerHTML = html;
|
|
9
9
|
return element.content;
|
|
10
10
|
};
|
|
11
|
+
const EMPTY_FRAGMENT = fragment('');
|
|
11
12
|
const marker = fragment(SLOT_HTML).firstChild;
|
|
12
13
|
const raf = globalThis?.requestAnimationFrame;
|
|
13
14
|
const template = (html) => {
|
|
@@ -28,4 +29,4 @@ const text = (value) => {
|
|
|
28
29
|
}
|
|
29
30
|
return element;
|
|
30
31
|
};
|
|
31
|
-
export { clone,
|
|
32
|
+
export { clone, EMPTY_FRAGMENT, fragment, marker, raf, template, text };
|