sinho 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/.github/workflows/ci.yml +24 -0
- package/.github/workflows/deploy-docs.yml +47 -0
- package/.prettierrc +3 -0
- package/LICENSE.md +21 -0
- package/README.md +33 -0
- package/ci/check-size.js +8 -0
- package/dist/array_mutation.d.ts +16 -0
- package/dist/array_mutation.js +75 -0
- package/dist/array_mutation.js.map +1 -0
- package/dist/bundle.d.ts +1126 -0
- package/dist/bundle.js +1074 -0
- package/dist/bundle.min.js +1 -0
- package/dist/component.d.ts +253 -0
- package/dist/component.js +256 -0
- package/dist/component.js.map +1 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.js +34 -0
- package/dist/context.js.map +1 -0
- package/dist/create_element.d.ts +43 -0
- package/dist/create_element.js +43 -0
- package/dist/create_element.js.map +1 -0
- package/dist/dom.d.ts +602 -0
- package/dist/dom.js +97 -0
- package/dist/dom.js.map +1 -0
- package/dist/intrinsic/ClassComponent.d.ts +2 -0
- package/dist/intrinsic/ClassComponent.js +10 -0
- package/dist/intrinsic/ClassComponent.js.map +1 -0
- package/dist/intrinsic/Dynamic.d.ts +33 -0
- package/dist/intrinsic/Dynamic.js +53 -0
- package/dist/intrinsic/Dynamic.js.map +1 -0
- package/dist/intrinsic/ErrorBoundary.d.ts +14 -0
- package/dist/intrinsic/ErrorBoundary.js +36 -0
- package/dist/intrinsic/ErrorBoundary.js.map +1 -0
- package/dist/intrinsic/For.d.ts +10 -0
- package/dist/intrinsic/For.js +81 -0
- package/dist/intrinsic/For.js.map +1 -0
- package/dist/intrinsic/Fragment.d.ts +23 -0
- package/dist/intrinsic/Fragment.js +28 -0
- package/dist/intrinsic/Fragment.js.map +1 -0
- package/dist/intrinsic/If.d.ts +24 -0
- package/dist/intrinsic/If.js +47 -0
- package/dist/intrinsic/If.js.map +1 -0
- package/dist/intrinsic/Portal.d.ts +6 -0
- package/dist/intrinsic/Portal.js +15 -0
- package/dist/intrinsic/Portal.js.map +1 -0
- package/dist/intrinsic/Style.d.ts +7 -0
- package/dist/intrinsic/Style.js +70 -0
- package/dist/intrinsic/Style.js.map +1 -0
- package/dist/intrinsic/TagComponent.d.ts +4 -0
- package/dist/intrinsic/TagComponent.js +67 -0
- package/dist/intrinsic/TagComponent.js.map +1 -0
- package/dist/intrinsic/Text.d.ts +6 -0
- package/dist/intrinsic/Text.js +16 -0
- package/dist/intrinsic/Text.js.map +1 -0
- package/dist/intrinsic/mod.d.ts +5 -0
- package/dist/intrinsic/mod.js +6 -0
- package/dist/intrinsic/mod.js.map +1 -0
- package/dist/jsx-runtime/mod.d.ts +23 -0
- package/dist/jsx-runtime/mod.js +11 -0
- package/dist/jsx-runtime/mod.js.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.js +7 -0
- package/dist/mod.js.map +1 -0
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.js +25 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scope.d.ts +138 -0
- package/dist/scope.js +228 -0
- package/dist/scope.js.map +1 -0
- package/dist/template.d.ts +10 -0
- package/dist/template.js +7 -0
- package/dist/template.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/package.json +71 -0
- package/src/array_mutation.ts +118 -0
- package/src/component.ts +624 -0
- package/src/context.ts +70 -0
- package/src/create_element.ts +89 -0
- package/src/dom.ts +819 -0
- package/src/intrinsic/ClassComponent.ts +17 -0
- package/src/intrinsic/For.ts +122 -0
- package/src/intrinsic/Fragment.ts +38 -0
- package/src/intrinsic/If.ts +73 -0
- package/src/intrinsic/Portal.ts +25 -0
- package/src/intrinsic/Style.ts +120 -0
- package/src/intrinsic/TagComponent.ts +102 -0
- package/src/intrinsic/Text.ts +24 -0
- package/src/intrinsic/mod.ts +5 -0
- package/src/jsx-runtime/mod.ts +41 -0
- package/src/mod.ts +37 -0
- package/src/renderer.ts +45 -0
- package/src/scope.ts +404 -0
- package/src/template.ts +16 -0
- package/src/utils.ts +29 -0
- package/terser.config.json +16 -0
- package/tsconfig.json +18 -0
- package/web/README.md +41 -0
- package/web/babel.config.js +3 -0
- package/web/dist/shingo.min.d.ts +1131 -0
- package/web/dist/shingo.min.js +1 -0
- package/web/docusaurus.config.ts +151 -0
- package/web/package-lock.json +14850 -0
- package/web/package.json +54 -0
- package/web/sidebars.ts +31 -0
- package/web/src/components/monacoEditor.tsx +72 -0
- package/web/src/components/playground.tsx +89 -0
- package/web/src/components/playgroundComponent.tsx +168 -0
- package/web/src/css/custom.css +37 -0
- package/web/src/pages/index.module.css +31 -0
- package/web/src/pages/index.tsx +73 -0
- package/web/src/pages/playground.tsx +64 -0
- package/web/static/.nojekyll +0 -0
- package/web/static/dist/bundle.d.ts +1126 -0
- package/web/static/dist/bundle.min.js +1 -0
- package/web/tsconfig.json +8 -0
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
const createScope = (parent) => {
|
|
2
|
+
return {
|
|
3
|
+
_parent: parent,
|
|
4
|
+
_effects: [],
|
|
5
|
+
_subscopes: [],
|
|
6
|
+
_details: { ...parent?._details },
|
|
7
|
+
_run(fn) {
|
|
8
|
+
const prevScope = currScope;
|
|
9
|
+
currScope = this;
|
|
10
|
+
try {
|
|
11
|
+
return fn();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
currScope = prevScope;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
_cleanup() {
|
|
18
|
+
for (let i = this._subscopes.length - 1; i >= 0; i--) {
|
|
19
|
+
this._subscopes[i]._cleanup();
|
|
20
|
+
}
|
|
21
|
+
this._subscopes = [];
|
|
22
|
+
for (let i = this._effects.length - 1; i >= 0; i--) {
|
|
23
|
+
const effect = this._effects[i];
|
|
24
|
+
effect._clean?.();
|
|
25
|
+
effect._deps.forEach((signal) => signal._effects.delete(effect));
|
|
26
|
+
effect._deps.clear();
|
|
27
|
+
}
|
|
28
|
+
this._effects = [];
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
let rootScope = createScope();
|
|
33
|
+
let currScope = rootScope;
|
|
34
|
+
let currUntracked = false;
|
|
35
|
+
let currEffect;
|
|
36
|
+
let currBatch;
|
|
37
|
+
/** @ignore */
|
|
38
|
+
const useScope = () => currScope;
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new signal with the given value.
|
|
41
|
+
* @returns A tuple with the signal and its setter.
|
|
42
|
+
*/
|
|
43
|
+
const useSignal = (value, opts) => {
|
|
44
|
+
const signal = () => {
|
|
45
|
+
if (!currUntracked && currEffect) {
|
|
46
|
+
currEffect._deps.add(signal);
|
|
47
|
+
signal._effects.add(currEffect);
|
|
48
|
+
}
|
|
49
|
+
return signal.peek();
|
|
50
|
+
};
|
|
51
|
+
signal._effects = new Set();
|
|
52
|
+
signal.peek = () => value;
|
|
53
|
+
const setter = (arg, innerOpts) => {
|
|
54
|
+
innerOpts = { ...opts, ...innerOpts };
|
|
55
|
+
if (currBatch) {
|
|
56
|
+
const newValue = typeof arg == "function"
|
|
57
|
+
? arg(signal.peek())
|
|
58
|
+
: arg;
|
|
59
|
+
if (innerOpts?.force || newValue !== signal.peek()) {
|
|
60
|
+
if (innerOpts?.force) {
|
|
61
|
+
value = newValue;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
currBatch._setters.push(() => (value = newValue));
|
|
65
|
+
}
|
|
66
|
+
if (!innerOpts?.silent) {
|
|
67
|
+
signal._effects.forEach((effect) => currBatch._effects.add(effect));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
useBatch(() => setter(arg, innerOpts));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return [signal, setter];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Runs the given function in a batch.
|
|
79
|
+
*
|
|
80
|
+
* @param fn Any calls to signal setters inside the function will be batched
|
|
81
|
+
* and updated at the same time.
|
|
82
|
+
*/
|
|
83
|
+
const useBatch = (fn) => {
|
|
84
|
+
const prevBatch = currBatch;
|
|
85
|
+
currBatch = { _setters: [], _effects: new Set() };
|
|
86
|
+
try {
|
|
87
|
+
const result = fn();
|
|
88
|
+
while (currBatch._setters.length > 0 || currBatch._effects.size > 0) {
|
|
89
|
+
// Clean effect subscope
|
|
90
|
+
const effects = currBatch._effects;
|
|
91
|
+
currBatch._effects = new Set();
|
|
92
|
+
effects.forEach((effect) => effect._clean?.());
|
|
93
|
+
// Run signal updates
|
|
94
|
+
currBatch._setters.forEach((setter) => setter());
|
|
95
|
+
currBatch._setters = [];
|
|
96
|
+
// Run effects
|
|
97
|
+
effects.forEach((effect) => effect._run());
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
currBatch = prevBatch;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Creates an effect which will rerun when any accessed signal changes.
|
|
107
|
+
*
|
|
108
|
+
* @param fn The function to run; it can return a cleanup function.
|
|
109
|
+
*/
|
|
110
|
+
const useEffect = (fn, deps) => {
|
|
111
|
+
const untracked = !!deps;
|
|
112
|
+
const effect = {
|
|
113
|
+
_scope: currScope,
|
|
114
|
+
_deps: new Set(),
|
|
115
|
+
_run() {
|
|
116
|
+
const prevEffect = currEffect;
|
|
117
|
+
const prevUntracked = currUntracked;
|
|
118
|
+
currEffect = this;
|
|
119
|
+
try {
|
|
120
|
+
if (!deps) {
|
|
121
|
+
// For automatic dependency tracking
|
|
122
|
+
// clean up dependencies and listeners
|
|
123
|
+
this._deps.forEach((dep) => dep._effects.delete(this));
|
|
124
|
+
this._deps.clear();
|
|
125
|
+
}
|
|
126
|
+
else if (!this._deps.size) {
|
|
127
|
+
// Track specified dependencies
|
|
128
|
+
deps.forEach((dep) => dep());
|
|
129
|
+
}
|
|
130
|
+
// Run effect
|
|
131
|
+
currUntracked = untracked;
|
|
132
|
+
this._clean?.();
|
|
133
|
+
const cleanup = this._scope._run(() => useBatch(fn));
|
|
134
|
+
this._clean = !cleanup
|
|
135
|
+
? null
|
|
136
|
+
: () => {
|
|
137
|
+
this._scope._run(() => useBatch(cleanup));
|
|
138
|
+
this._clean = null;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
// Restore scope state
|
|
143
|
+
currEffect = prevEffect;
|
|
144
|
+
currUntracked = prevUntracked;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
effect._deps.forEach((signal) => signal._effects.add(effect));
|
|
149
|
+
currScope._effects.push(effect);
|
|
150
|
+
effect._run();
|
|
151
|
+
if (!effect._deps.size && !effect._clean) {
|
|
152
|
+
// Optimization: Destroy effect since there's no cleanup and this effect
|
|
153
|
+
// won't be called again
|
|
154
|
+
currScope._effects.pop();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Creates a memoized signal.
|
|
159
|
+
*
|
|
160
|
+
* @param fn The computation function.
|
|
161
|
+
*/
|
|
162
|
+
const useMemo = (fn, opts) => {
|
|
163
|
+
const [memo, setMemo] = useSignal();
|
|
164
|
+
let firstTime = true;
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
setMemo(fn, firstTime ? { ...opts, force: true } : opts);
|
|
167
|
+
firstTime = false;
|
|
168
|
+
});
|
|
169
|
+
return memo;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Executes a function inside a subscope which can be manually destroyed.
|
|
173
|
+
*
|
|
174
|
+
* @param fn The function to run in the subscope.
|
|
175
|
+
* @returns A function to manually destroy the subscope.
|
|
176
|
+
*/
|
|
177
|
+
const useSubscope = (fn, opts) => {
|
|
178
|
+
const parent = currScope;
|
|
179
|
+
const scope = createScope(parent);
|
|
180
|
+
Object.assign(scope._details, opts?.details);
|
|
181
|
+
parent._subscopes.push(scope);
|
|
182
|
+
const result = scope._run(fn);
|
|
183
|
+
return [
|
|
184
|
+
result,
|
|
185
|
+
() => {
|
|
186
|
+
const index = parent._subscopes.indexOf(scope);
|
|
187
|
+
if (index >= 0) {
|
|
188
|
+
parent._subscopes.splice(index, 1);
|
|
189
|
+
}
|
|
190
|
+
scope._cleanup();
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Creates a new signal with write capabilities.
|
|
196
|
+
*/
|
|
197
|
+
const useRef = (value, opts) => {
|
|
198
|
+
const [signal, setter] = useSignal(value, opts);
|
|
199
|
+
signal.set = setter;
|
|
200
|
+
return signal;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* @namespace
|
|
204
|
+
*/
|
|
205
|
+
const MaybeSignal = {
|
|
206
|
+
/**
|
|
207
|
+
* Transforms the given {@link MaybeSignal} into a {@link Signal}.
|
|
208
|
+
*/
|
|
209
|
+
upgrade: (signal) => () => MaybeSignal.get(signal),
|
|
210
|
+
/**
|
|
211
|
+
* Gets the value of the given {@link MaybeSignal}.
|
|
212
|
+
*/
|
|
213
|
+
get: (signal) => typeof signal == "function" ? signal() : signal,
|
|
214
|
+
/**
|
|
215
|
+
* Accesses the value of the given {@link MaybeSignal} without tracking.
|
|
216
|
+
*/
|
|
217
|
+
peek(signal) {
|
|
218
|
+
const prevUntracked = currUntracked;
|
|
219
|
+
currUntracked = true;
|
|
220
|
+
try {
|
|
221
|
+
return this.get(signal);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
currUntracked = prevUntracked;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const createRenderer = (override = {}) => ({
|
|
230
|
+
_ifConditions: [],
|
|
231
|
+
_node(fallback) {
|
|
232
|
+
return this._nodes?.next().value ?? fallback();
|
|
233
|
+
},
|
|
234
|
+
...override,
|
|
235
|
+
});
|
|
236
|
+
const useRenderer = () => {
|
|
237
|
+
const scope = useScope();
|
|
238
|
+
return (scope._details._renderer ??= createRenderer());
|
|
239
|
+
};
|
|
240
|
+
const runWithRenderer = (override, fn) => {
|
|
241
|
+
const currRenderer = useRenderer();
|
|
242
|
+
const _renderer = createRenderer({
|
|
243
|
+
...currRenderer,
|
|
244
|
+
...override,
|
|
245
|
+
});
|
|
246
|
+
const [result, destroy] = useSubscope(fn, {
|
|
247
|
+
details: { _renderer },
|
|
248
|
+
});
|
|
249
|
+
useEffect(() => destroy);
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const camelCaseToKebabCase = (value) => {
|
|
254
|
+
return ((value[0] ?? "").toLowerCase() +
|
|
255
|
+
value.slice(1).replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`));
|
|
256
|
+
};
|
|
257
|
+
const jsxPropNameToEventName = (value) => {
|
|
258
|
+
if (value.startsWith("on:")) {
|
|
259
|
+
return value.slice(3);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
return camelCaseToKebabCase(value.slice(2));
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const contextSym = Symbol("Context");
|
|
267
|
+
/**
|
|
268
|
+
* Creates a new context with the given value.
|
|
269
|
+
*/
|
|
270
|
+
const createContext = ((value, opts) => ({
|
|
271
|
+
[contextSym]: Math.random().toString(36).slice(2),
|
|
272
|
+
_init: value,
|
|
273
|
+
_opts: opts,
|
|
274
|
+
}));
|
|
275
|
+
const isContext = (value) => !!value?.[contextSym];
|
|
276
|
+
const provideContext = (context, element, value) => {
|
|
277
|
+
element.addEventListener(context[contextSym], (evt) => {
|
|
278
|
+
const innerValue = MaybeSignal.get(value);
|
|
279
|
+
if (innerValue !== undefined) {
|
|
280
|
+
evt.stopPropagation();
|
|
281
|
+
evt.detail(innerValue);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
const useContext = (context) => {
|
|
286
|
+
const renderer = useRenderer();
|
|
287
|
+
return useMemo(() => {
|
|
288
|
+
let result = context._init;
|
|
289
|
+
renderer._component?.dispatchEvent(new CustomEvent(context[contextSym], {
|
|
290
|
+
detail: (value) => (result = value),
|
|
291
|
+
bubbles: true,
|
|
292
|
+
composed: true,
|
|
293
|
+
}));
|
|
294
|
+
return result;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Defines a property in your component metadata that can be set from outside
|
|
300
|
+
* of the component.
|
|
301
|
+
*
|
|
302
|
+
* Make sure to avoid conflicts with native `HTMLElement` properties.
|
|
303
|
+
*
|
|
304
|
+
* You can get properties by accessing the {@link Signal} in `this.props`.
|
|
305
|
+
* It's also possible to set the properties directly on the component instance.
|
|
306
|
+
*
|
|
307
|
+
* It's also possible to define an attribute for the property by setting the
|
|
308
|
+
* `attribute` option. By default, the attribute name is the kebab-case version
|
|
309
|
+
* of the property name. The attribute will be observed and the signal updated
|
|
310
|
+
* on changes. You can also provide a custom name and a transform function to
|
|
311
|
+
* convert the attribute value to the property value.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```tsx
|
|
315
|
+
* class App extends Component("x-app", {
|
|
316
|
+
* greetingMessage: prop<string>("Hello, world!", {
|
|
317
|
+
* attribute: {
|
|
318
|
+
* name: "greeting",
|
|
319
|
+
* }
|
|
320
|
+
* }),
|
|
321
|
+
* }) {
|
|
322
|
+
* render() {
|
|
323
|
+
* return <h1>{this.props.greetingMessage}</h1>;
|
|
324
|
+
* }
|
|
325
|
+
* }
|
|
326
|
+
*
|
|
327
|
+
* defineComponents(App);
|
|
328
|
+
*
|
|
329
|
+
* const app = new App();
|
|
330
|
+
* app.greetingMessage = "Hello, universe!";
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
const prop = (defaultOrContext, opts) => ({
|
|
334
|
+
_tag: "p",
|
|
335
|
+
_defaultOrContext: defaultOrContext,
|
|
336
|
+
...opts,
|
|
337
|
+
});
|
|
338
|
+
/**
|
|
339
|
+
* Defines an event in your component metadata that can be dispatched by
|
|
340
|
+
* the component.
|
|
341
|
+
*
|
|
342
|
+
* Make sure your event name starts with `on` and to avoid conflicts with
|
|
343
|
+
* native `HTMLElement` events. The event name will be converted to kebab-case.
|
|
344
|
+
*
|
|
345
|
+
* You can dispatch events either using `HTMLElement.dispatchEvent` or by
|
|
346
|
+
* calling the event emitter function in `this.events` inside the `render`
|
|
347
|
+
* function of a component.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```tsx
|
|
351
|
+
* class App extends Component("x-app", {
|
|
352
|
+
* onSomethingHappen: event<string>(),
|
|
353
|
+
* // Event name will be `something-happen`
|
|
354
|
+
* }) {
|
|
355
|
+
* render() {
|
|
356
|
+
* // …
|
|
357
|
+
* this.events.onSomethingHappen({ detail: "Something happened! "});
|
|
358
|
+
* }
|
|
359
|
+
* }
|
|
360
|
+
*
|
|
361
|
+
* const app = new App();
|
|
362
|
+
* app.addEventListener("something-happen", (evt) => {
|
|
363
|
+
* // `evt` is `CustomEvent<string>`
|
|
364
|
+
* console.log(evt.detail);
|
|
365
|
+
* });
|
|
366
|
+
* ```
|
|
367
|
+
*
|
|
368
|
+
* You can also provide a custom event constructor:
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```tsx
|
|
372
|
+
* class App extends Component("x-app", {
|
|
373
|
+
* onSomethingClick: event(() => MouseEvent),
|
|
374
|
+
* }) {
|
|
375
|
+
* render() {
|
|
376
|
+
* return (
|
|
377
|
+
* <button onclick={evt => this.events.onSomethingClick(evt)}>
|
|
378
|
+
* Click me!
|
|
379
|
+
* </button>
|
|
380
|
+
* );
|
|
381
|
+
* }
|
|
382
|
+
* }
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
const event = ((eventConstructor = CustomEvent) => ({
|
|
386
|
+
_tag: "e",
|
|
387
|
+
_event: eventConstructor,
|
|
388
|
+
}));
|
|
389
|
+
const componentSym = Symbol("Component");
|
|
390
|
+
let mountEffects;
|
|
391
|
+
/**
|
|
392
|
+
* Creates an effect which will rerun when any accessed signal changes.
|
|
393
|
+
*
|
|
394
|
+
* If used inside of a component and the component is not yet mounted, the
|
|
395
|
+
* effect will run only after the component is mounted. Otherwise, the effect
|
|
396
|
+
* will run immediately.
|
|
397
|
+
*
|
|
398
|
+
* @param fn The function to run; it may return a cleanup function.
|
|
399
|
+
*/
|
|
400
|
+
const useMountEffect = (fn, deps) => {
|
|
401
|
+
if (mountEffects) {
|
|
402
|
+
mountEffects.push([fn, deps]);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
useEffect(fn, deps);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Creates a new web component class.
|
|
410
|
+
*
|
|
411
|
+
* Specify props and events using the `metadata` parameter.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```tsx
|
|
415
|
+
* class MyComponent extends Component("my-component", {
|
|
416
|
+
* myProp: prop<string>("Hello, world!"),
|
|
417
|
+
* onMyEvent: event(),
|
|
418
|
+
* }) {
|
|
419
|
+
* render() {
|
|
420
|
+
* return (
|
|
421
|
+
* <>
|
|
422
|
+
* <h1>{this.props.myProp}</h1>
|
|
423
|
+
* <button onclick={() => this.events.onMyEvent()}>Click me</button>
|
|
424
|
+
* </>
|
|
425
|
+
* );
|
|
426
|
+
* },
|
|
427
|
+
* }
|
|
428
|
+
*
|
|
429
|
+
* customElements.define("my-component", MyComponent);
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
const Component = ((tagName, metadata = {}, opts = {}) => {
|
|
433
|
+
// Extract attribute information
|
|
434
|
+
const observedAttributes = [];
|
|
435
|
+
const attributePropMap = new Map();
|
|
436
|
+
for (const name in metadata) {
|
|
437
|
+
const meta = metadata[name];
|
|
438
|
+
if (meta._tag == "p" && meta.attribute) {
|
|
439
|
+
if (typeof meta.attribute == "function") {
|
|
440
|
+
meta.attribute = { transform: meta.attribute };
|
|
441
|
+
}
|
|
442
|
+
const attribute = (meta.attribute = {
|
|
443
|
+
name: camelCaseToKebabCase(name),
|
|
444
|
+
static: false,
|
|
445
|
+
transform: (x) => x,
|
|
446
|
+
...meta.attribute,
|
|
447
|
+
});
|
|
448
|
+
attributePropMap.set(attribute.name, {
|
|
449
|
+
name,
|
|
450
|
+
meta: meta,
|
|
451
|
+
});
|
|
452
|
+
if (!attribute.static) {
|
|
453
|
+
observedAttributes.push(attribute.name);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Create base class
|
|
458
|
+
opts.shadow ??= { mode: "open" };
|
|
459
|
+
const getRenderParent = (component) => opts.shadow
|
|
460
|
+
? component.shadowRoot ?? component.attachShadow(opts.shadow)
|
|
461
|
+
: component;
|
|
462
|
+
class _Component extends HTMLElement {
|
|
463
|
+
static [componentSym] = {
|
|
464
|
+
_tagName: tagName,
|
|
465
|
+
};
|
|
466
|
+
static observedAttributes = observedAttributes;
|
|
467
|
+
props = {};
|
|
468
|
+
events = {};
|
|
469
|
+
[componentSym] = {};
|
|
470
|
+
constructor() {
|
|
471
|
+
super();
|
|
472
|
+
for (const name in metadata) {
|
|
473
|
+
const meta = metadata[name];
|
|
474
|
+
if (meta._tag == "p") {
|
|
475
|
+
const context = isContext(meta._defaultOrContext)
|
|
476
|
+
? meta._defaultOrContext
|
|
477
|
+
: null;
|
|
478
|
+
const [getter, setter] = useSignal(context ? undefined : meta._defaultOrContext);
|
|
479
|
+
this.props[name] = getter;
|
|
480
|
+
if (context) {
|
|
481
|
+
provideContext(context, this, getter);
|
|
482
|
+
}
|
|
483
|
+
Object.defineProperty(this, name, {
|
|
484
|
+
get: getter.peek,
|
|
485
|
+
set: (value) => setter(() => value, { force: true }),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
else if (meta._tag == "e" && name.startsWith("on")) {
|
|
489
|
+
const eventName = jsxPropNameToEventName(name);
|
|
490
|
+
this.events[name] = (arg) => this.dispatchEvent(new meta._event(eventName, arg));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
connectedCallback() {
|
|
495
|
+
const renderParent = getRenderParent(this);
|
|
496
|
+
this[componentSym]._destroy = useSubscope(() => runWithRenderer({
|
|
497
|
+
_svg: false,
|
|
498
|
+
_component: this,
|
|
499
|
+
_nodes: renderParent.childNodes.values(),
|
|
500
|
+
}, () => {
|
|
501
|
+
this[componentSym]._scope = useScope();
|
|
502
|
+
// Render
|
|
503
|
+
const prevMountEffects = mountEffects;
|
|
504
|
+
mountEffects = [];
|
|
505
|
+
try {
|
|
506
|
+
renderParent?.append(...this.render().build());
|
|
507
|
+
// Run mount effects
|
|
508
|
+
mountEffects.forEach(([fn, opts]) => useEffect(fn, opts));
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
mountEffects = prevMountEffects;
|
|
512
|
+
}
|
|
513
|
+
}))[1];
|
|
514
|
+
}
|
|
515
|
+
disconnectedCallback() {
|
|
516
|
+
this[componentSym]._destroy?.();
|
|
517
|
+
}
|
|
518
|
+
attributeChangedCallback(name, _, value) {
|
|
519
|
+
const prop = attributePropMap.get(name);
|
|
520
|
+
if (prop) {
|
|
521
|
+
this[prop.name] =
|
|
522
|
+
value != null
|
|
523
|
+
? prop.meta.attribute.transform.call(this, value)
|
|
524
|
+
: isContext(prop.meta._defaultOrContext)
|
|
525
|
+
? undefined
|
|
526
|
+
: prop.meta._defaultOrContext;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return _Component;
|
|
531
|
+
});
|
|
532
|
+
/**
|
|
533
|
+
* Determines whether the given value is a component created by
|
|
534
|
+
* extending {@link ComponentConstructor}.
|
|
535
|
+
*/
|
|
536
|
+
const isComponent = (value) => !!value?.[componentSym];
|
|
537
|
+
/**
|
|
538
|
+
* Defines a set of components with the given prefix.
|
|
539
|
+
*/
|
|
540
|
+
const defineComponents = (...args) => {
|
|
541
|
+
const [prefix, components] = typeof args[0] == "string"
|
|
542
|
+
? [args[0], args.slice(1)]
|
|
543
|
+
: ["", args];
|
|
544
|
+
for (const component of components) {
|
|
545
|
+
customElements.define(prefix + component[componentSym]._tagName, component);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/*
|
|
550
|
+
* The following functions are lifted from Preact <https://preactjs.com/> and
|
|
551
|
+
* modified.
|
|
552
|
+
*
|
|
553
|
+
* The MIT License (MIT)
|
|
554
|
+
*
|
|
555
|
+
* Copyright (c) 2015-present Jason Miller
|
|
556
|
+
*
|
|
557
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
558
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
559
|
+
* in the Software without restriction, including without limitation the rights
|
|
560
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
561
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
562
|
+
* furnished to do so, subject to the following conditions:
|
|
563
|
+
*
|
|
564
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
565
|
+
* copies or substantial portions of the Software.
|
|
566
|
+
*
|
|
567
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
568
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
569
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
570
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
571
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
572
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
573
|
+
* SOFTWARE.
|
|
574
|
+
*/
|
|
575
|
+
const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
|
|
576
|
+
const setStyle = (node, key, value) => {
|
|
577
|
+
if (key[0] == "-") {
|
|
578
|
+
node.style.setProperty(key, `${value}`);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
node.style[key] =
|
|
582
|
+
value == null
|
|
583
|
+
? ""
|
|
584
|
+
: typeof value != "number" || IS_NON_DIMENSIONAL.test(key)
|
|
585
|
+
? `${value}`
|
|
586
|
+
: `${value}px`;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const setAttr = (node, name, value, heuristic) => {
|
|
590
|
+
const removeAttribute = value == null || (value === false && !name.includes("-"));
|
|
591
|
+
if (name.startsWith("prop:")) {
|
|
592
|
+
node[name] = value;
|
|
593
|
+
}
|
|
594
|
+
else if (name.startsWith("attr:")) {
|
|
595
|
+
if (!removeAttribute) {
|
|
596
|
+
node.setAttribute(name, value);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
node.removeAttribute(name);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else if (!["innerHTML", "outerHTML"].includes(name)) {
|
|
603
|
+
if (![
|
|
604
|
+
// Default value in browsers is `-1` and an empty string is
|
|
605
|
+
// cast to `0` instead
|
|
606
|
+
"tabIndex",
|
|
607
|
+
"role",
|
|
608
|
+
...(heuristic
|
|
609
|
+
? [
|
|
610
|
+
"width",
|
|
611
|
+
"height",
|
|
612
|
+
"href",
|
|
613
|
+
"list",
|
|
614
|
+
"form",
|
|
615
|
+
"download",
|
|
616
|
+
"rowSpan",
|
|
617
|
+
"colSpan",
|
|
618
|
+
]
|
|
619
|
+
: []),
|
|
620
|
+
].includes(name) &&
|
|
621
|
+
name in node) {
|
|
622
|
+
try {
|
|
623
|
+
node[name] = value == null ? "" : value;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
catch (e) { }
|
|
627
|
+
}
|
|
628
|
+
// aria- and data- attributes have no boolean representation.
|
|
629
|
+
// A `false` value is different from the attribute not being
|
|
630
|
+
// present, so we can't remove it. For non-boolean aria
|
|
631
|
+
// attributes we could treat false as a removal, but the
|
|
632
|
+
// amount of exceptions would cost too many bytes. On top of
|
|
633
|
+
// that other frameworks generally stringify `false`.
|
|
634
|
+
if (typeof value == "function") ;
|
|
635
|
+
else if (!removeAttribute) {
|
|
636
|
+
node.setAttribute(name, value);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
node.removeAttribute(name);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const createTemplate = (build) => ({
|
|
645
|
+
build() {
|
|
646
|
+
const nodes = build();
|
|
647
|
+
return nodes.build?.() ?? nodes;
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const Text = ({ text, marker }) => createTemplate(() => {
|
|
652
|
+
const renderer = useRenderer();
|
|
653
|
+
const anchor = marker && renderer._node(() => document.createComment(""));
|
|
654
|
+
const node = renderer._node(() => document.createTextNode(""));
|
|
655
|
+
useEffect(() => {
|
|
656
|
+
const textContent = "" + (MaybeSignal.get(text) ?? "");
|
|
657
|
+
if (node.textContent != textContent) {
|
|
658
|
+
node.textContent = textContent;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
return anchor ? [anchor, node] : [node];
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Fragment is a component that can be used to wrap multiple children without
|
|
666
|
+
* introducing an extra DOM element.
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* ```tsx
|
|
670
|
+
* render() {
|
|
671
|
+
* return (
|
|
672
|
+
* <>
|
|
673
|
+
* <h1>Hello World</h1>
|
|
674
|
+
* <p>This is a paragraph.</p>
|
|
675
|
+
* </>
|
|
676
|
+
* );
|
|
677
|
+
* }
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
const Fragment = ({ children }) => createTemplate(() => {
|
|
681
|
+
return !Array.isArray(children)
|
|
682
|
+
? children == null
|
|
683
|
+
? []
|
|
684
|
+
: typeof children == "object"
|
|
685
|
+
? children
|
|
686
|
+
: Text({ text: children })
|
|
687
|
+
: children.flatMap((children) => Fragment({ children }).build());
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const hydrateElement = (node, svg, props, heuristic) => {
|
|
691
|
+
const { ref, style, children, dangerouslySetInnerHTML, ...attrs } = props;
|
|
692
|
+
for (const name in style ?? {}) {
|
|
693
|
+
const value = style[name];
|
|
694
|
+
const signal = value;
|
|
695
|
+
useEffect(() => {
|
|
696
|
+
setStyle(node, name, MaybeSignal.get(signal));
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
for (const name in attrs) {
|
|
700
|
+
const value = attrs[name];
|
|
701
|
+
if (name.startsWith("on")) {
|
|
702
|
+
// Register event
|
|
703
|
+
const s = useScope();
|
|
704
|
+
const listener = (evt) => {
|
|
705
|
+
s._run(() => useBatch(() => value(evt)));
|
|
706
|
+
};
|
|
707
|
+
const eventName = jsxPropNameToEventName(name);
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
node.addEventListener(eventName, listener);
|
|
710
|
+
return () => node.removeEventListener(eventName, listener);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
// Set attribute
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
setAttr(node, name, MaybeSignal.get(value), heuristic);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (dangerouslySetInnerHTML) {
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
const html = MaybeSignal.get(dangerouslySetInnerHTML).__html;
|
|
723
|
+
if (node.innerHTML != html) {
|
|
724
|
+
node.innerHTML = html;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (ref) {
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
ref.set(node);
|
|
731
|
+
return () => ref.set(undefined);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
if (props.children != null) {
|
|
735
|
+
node.append(...runWithRenderer({
|
|
736
|
+
_svg: svg,
|
|
737
|
+
_nodes: node.childNodes.values(),
|
|
738
|
+
}, () => Fragment({ children: props.children }).build()));
|
|
739
|
+
}
|
|
740
|
+
return node;
|
|
741
|
+
};
|
|
742
|
+
const TagComponent = (tagName, props = {}) => createTemplate(() => {
|
|
743
|
+
const renderer = useRenderer();
|
|
744
|
+
const svg = tagName == "svg" ? true : !!renderer._svg;
|
|
745
|
+
const node = hydrateElement(renderer._node(() => !svg
|
|
746
|
+
? document.createElement(tagName)
|
|
747
|
+
: document.createElementNS("http://www.w3.org/2000/svg", tagName)), svg, props, true);
|
|
748
|
+
return [node];
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const ClassComponent = (type, props) => createTemplate(() => {
|
|
752
|
+
const node = useRenderer()._node(() => new type());
|
|
753
|
+
customElements.upgrade(node);
|
|
754
|
+
hydrateElement(node, false, props);
|
|
755
|
+
return [node];
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Creates a template based on the given component type.
|
|
760
|
+
*
|
|
761
|
+
* @example
|
|
762
|
+
* ```tsx
|
|
763
|
+
* render() {
|
|
764
|
+
* return createElement("div", { id: "app" }, [
|
|
765
|
+
* createElement("h1", {}, "Hello, World!"),
|
|
766
|
+
* ]);
|
|
767
|
+
* }
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
const createElement = ((type, props = {}, children) => {
|
|
771
|
+
if (children != null) {
|
|
772
|
+
props.children = children;
|
|
773
|
+
}
|
|
774
|
+
return isComponent(type)
|
|
775
|
+
? ClassComponent(type, props)
|
|
776
|
+
: typeof type == "function"
|
|
777
|
+
? createTemplate(() => type(props))
|
|
778
|
+
: TagComponent(type, props);
|
|
779
|
+
});
|
|
780
|
+
/**
|
|
781
|
+
* Shorthand for {@link createElement} with convenience methods for intrinsic
|
|
782
|
+
* elements.
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* ```tsx
|
|
786
|
+
* render() {
|
|
787
|
+
* return h.div({ id: "app" }, [
|
|
788
|
+
* h.h1({}, "Hello, World!"),
|
|
789
|
+
* ]);
|
|
790
|
+
* }
|
|
791
|
+
* ```
|
|
792
|
+
*/
|
|
793
|
+
const h = new Proxy(createElement, {
|
|
794
|
+
get: (target, type) => (props, children) => target(type, props, children),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const getIndexMap = (array, keyFn) => {
|
|
798
|
+
const keyMap = new Map();
|
|
799
|
+
for (let i = 0; i < array.length; i++) {
|
|
800
|
+
const key = keyFn(array[i], i);
|
|
801
|
+
if (keyMap.has(key)) {
|
|
802
|
+
throw new Error(`Duplicate key '${key}'`);
|
|
803
|
+
}
|
|
804
|
+
keyMap.set(key, i);
|
|
805
|
+
}
|
|
806
|
+
return keyMap;
|
|
807
|
+
};
|
|
808
|
+
const useArrayMutation = (array, keyFn) => {
|
|
809
|
+
const [result, setResult] = useSignal({
|
|
810
|
+
_mutations: [],
|
|
811
|
+
_map: new Map(),
|
|
812
|
+
});
|
|
813
|
+
let indexMap = new Map();
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
const mutations = [];
|
|
816
|
+
const oldIndexMap = indexMap;
|
|
817
|
+
const newIndexMap = getIndexMap(array(), keyFn);
|
|
818
|
+
const transformToOldIndex = (i = NaN) => mutations
|
|
819
|
+
.map((mutation) => mutation._type == "r"
|
|
820
|
+
? (j) => j < mutation._index ? j : j == mutation._index ? NaN : j - 1
|
|
821
|
+
: mutation._type == "a"
|
|
822
|
+
? (j) => (j < mutation._index ? j : j + 1)
|
|
823
|
+
: mutation._type == "m"
|
|
824
|
+
? (j) => mutation._to <= j && j < mutation._from
|
|
825
|
+
? j + 1
|
|
826
|
+
: j == mutation._from
|
|
827
|
+
? mutation._to
|
|
828
|
+
: j
|
|
829
|
+
: (j) => j)
|
|
830
|
+
.reduce((i, fn) => fn(i), i);
|
|
831
|
+
for (const key of oldIndexMap.keys()) {
|
|
832
|
+
const i = transformToOldIndex(oldIndexMap.get(key));
|
|
833
|
+
if (!newIndexMap.has(key)) {
|
|
834
|
+
mutations.push({
|
|
835
|
+
_type: "r",
|
|
836
|
+
_key: key,
|
|
837
|
+
_index: i,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (let i = 0; i < array().length; i++) {
|
|
842
|
+
const key = keyFn(array()[i], i);
|
|
843
|
+
const oldIndex = transformToOldIndex(oldIndexMap.get(key));
|
|
844
|
+
if (isNaN(oldIndex)) {
|
|
845
|
+
mutations.push({
|
|
846
|
+
_type: "a",
|
|
847
|
+
_key: key,
|
|
848
|
+
_index: i,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
else if (oldIndex != i) {
|
|
852
|
+
mutations.push({
|
|
853
|
+
_type: "m",
|
|
854
|
+
_key: key,
|
|
855
|
+
_from: oldIndex,
|
|
856
|
+
_to: i,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (mutations.length > 0) {
|
|
861
|
+
setResult({
|
|
862
|
+
_mutations: mutations,
|
|
863
|
+
_map: newIndexMap,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
indexMap = newIndexMap;
|
|
867
|
+
});
|
|
868
|
+
return result;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* `For` is a component that can be used to render a list of items.
|
|
873
|
+
*/
|
|
874
|
+
const For = (props) => createTemplate(() => {
|
|
875
|
+
const renderer = useRenderer();
|
|
876
|
+
const items = MaybeSignal.upgrade(props.each ?? []);
|
|
877
|
+
const anchor = renderer._node(() => document.createComment(""));
|
|
878
|
+
const keyFn = props.key ?? ((_, i) => i);
|
|
879
|
+
const nodes = [anchor];
|
|
880
|
+
const keyMap = new Map();
|
|
881
|
+
const mutationResult = useArrayMutation(items, keyFn);
|
|
882
|
+
const lookForAnchor = (index) => {
|
|
883
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
884
|
+
const key = keyFn(items()[index - 1], index - 1);
|
|
885
|
+
const nodes = keyMap.get(key)?._subnodes ?? [];
|
|
886
|
+
if (nodes.length > 0) {
|
|
887
|
+
return nodes[nodes.length - 1];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return anchor;
|
|
891
|
+
};
|
|
892
|
+
useEffect(() => {
|
|
893
|
+
for (const mutation of mutationResult()._mutations) {
|
|
894
|
+
if (mutation._type == "r") {
|
|
895
|
+
const { _subnodes = [], _destroy } = keyMap.get(mutation._key) ?? {};
|
|
896
|
+
_destroy?.();
|
|
897
|
+
const index = nodes.indexOf(_subnodes[0]);
|
|
898
|
+
if (index > 0) {
|
|
899
|
+
nodes.splice(index, _subnodes.length);
|
|
900
|
+
}
|
|
901
|
+
_subnodes.forEach((node) => node.parentNode?.removeChild(node));
|
|
902
|
+
keyMap.delete(mutation._key);
|
|
903
|
+
}
|
|
904
|
+
else if (mutation._type == "a") {
|
|
905
|
+
let _subnodes = [];
|
|
906
|
+
const [, destroy] = useSubscope(() => {
|
|
907
|
+
const [index, setIndex] = useSignal(mutation._index);
|
|
908
|
+
const [item, setItem] = useSignal(items()[mutation._index]);
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
if (0 <= index() && index() < items().length) {
|
|
911
|
+
setItem(() => items()[index()]);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
const index = mutationResult()._map.get(mutation._key);
|
|
916
|
+
if (index != null) {
|
|
917
|
+
setIndex(index);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
_subnodes = props.children?.(item, index, items).build() ?? [];
|
|
921
|
+
const itemAnchor = lookForAnchor(mutation._index);
|
|
922
|
+
const anchorIndex = nodes.indexOf(itemAnchor);
|
|
923
|
+
if (anchorIndex >= 0) {
|
|
924
|
+
nodes.splice(anchorIndex + 1, 0, ..._subnodes);
|
|
925
|
+
}
|
|
926
|
+
_subnodes.forEach((node) => itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling));
|
|
927
|
+
});
|
|
928
|
+
keyMap.set(mutation._key, { _subnodes, _destroy: destroy });
|
|
929
|
+
}
|
|
930
|
+
else if (mutation._type == "m") {
|
|
931
|
+
const { _subnodes = [] } = keyMap.get(mutation._key) ?? {};
|
|
932
|
+
const index = nodes.indexOf(_subnodes[0]);
|
|
933
|
+
if (index >= 0) {
|
|
934
|
+
nodes.splice(index, _subnodes.length);
|
|
935
|
+
}
|
|
936
|
+
const itemAnchor = lookForAnchor(mutation._to);
|
|
937
|
+
const anchorIndex = nodes.indexOf(itemAnchor);
|
|
938
|
+
if (anchorIndex >= 0) {
|
|
939
|
+
nodes.splice(anchorIndex + 1, 0, ..._subnodes);
|
|
940
|
+
}
|
|
941
|
+
_subnodes.forEach((node) => itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling));
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}, [mutationResult]);
|
|
945
|
+
return nodes;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* `If` is a component that can be used to render conditionally.
|
|
950
|
+
*/
|
|
951
|
+
const If = (props) => {
|
|
952
|
+
const renderer = useRenderer();
|
|
953
|
+
renderer._ifConditions = [];
|
|
954
|
+
return ElseIf({ condition: props.condition, children: props.children });
|
|
955
|
+
};
|
|
956
|
+
/**
|
|
957
|
+
* `ElseIf` serves as an `else if` block for {@link If}. It can also be chained
|
|
958
|
+
* multiple times.
|
|
959
|
+
*/
|
|
960
|
+
const ElseIf = (props) => {
|
|
961
|
+
const renderer = useRenderer();
|
|
962
|
+
const conditions = renderer._ifConditions;
|
|
963
|
+
const condition = useMemo(() => conditions.every((condition) => !condition()) &&
|
|
964
|
+
MaybeSignal.get(props.condition));
|
|
965
|
+
renderer._ifConditions = [...conditions, condition];
|
|
966
|
+
return runWithRenderer({ _ifConditions: [] }, () => createTemplate(() => {
|
|
967
|
+
const anchor = renderer._node(() => document.createComment(""));
|
|
968
|
+
const nodes = [anchor];
|
|
969
|
+
const template = useMemo(() => condition() ? Fragment({ children: props.children }) : null);
|
|
970
|
+
let subnodes = [];
|
|
971
|
+
useEffect(() => {
|
|
972
|
+
subnodes.forEach((node) => node.parentNode?.removeChild(node));
|
|
973
|
+
nodes.length = 1;
|
|
974
|
+
const [, destroy] = useSubscope(() => {
|
|
975
|
+
subnodes = template()?.build() ?? [];
|
|
976
|
+
anchor.after(...subnodes);
|
|
977
|
+
nodes.push(...subnodes);
|
|
978
|
+
});
|
|
979
|
+
return destroy;
|
|
980
|
+
}, [template]);
|
|
981
|
+
return nodes;
|
|
982
|
+
}));
|
|
983
|
+
};
|
|
984
|
+
/**
|
|
985
|
+
* `Else` indicates the `else` block for {@link If} and {@link ElseIf}.
|
|
986
|
+
*/
|
|
987
|
+
const Else = ({ children, }) => {
|
|
988
|
+
return ElseIf({ condition: true, children });
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const Portal = ({ mount, children }) => createTemplate(() => runWithRenderer({ _nodes: undefined }, () => {
|
|
992
|
+
const nodes = Fragment({ children }).build();
|
|
993
|
+
useEffect(() => {
|
|
994
|
+
nodes.forEach((node) => mount.appendChild(node));
|
|
995
|
+
return () => {
|
|
996
|
+
nodes.forEach((node) => node.parentNode?.removeChild(node));
|
|
997
|
+
};
|
|
998
|
+
});
|
|
999
|
+
return [];
|
|
1000
|
+
}));
|
|
1001
|
+
|
|
1002
|
+
const styleSheetRegistrySym = Symbol("styleSheetRegistry");
|
|
1003
|
+
const globalStyleSheetRegistry = new Map();
|
|
1004
|
+
const getLocalStyleSheetRegistry = (obj) => (obj[styleSheetRegistrySym] ??= new Map());
|
|
1005
|
+
const useStyleSheet = (registry, css, cleanup) => {
|
|
1006
|
+
if (!globalStyleSheetRegistry.has(css)) {
|
|
1007
|
+
const sheet = new CSSStyleSheet();
|
|
1008
|
+
sheet.replaceSync(css);
|
|
1009
|
+
globalStyleSheetRegistry.set(css, {
|
|
1010
|
+
_sheet: sheet,
|
|
1011
|
+
_refs: 0,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const globalEntry = globalStyleSheetRegistry.get(css);
|
|
1015
|
+
globalEntry._refs++;
|
|
1016
|
+
if (!registry.has(css)) {
|
|
1017
|
+
registry.set(css, {
|
|
1018
|
+
_sheet: globalEntry._sheet,
|
|
1019
|
+
_refs: 0,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
const entry = registry.get(css);
|
|
1023
|
+
entry._refs++;
|
|
1024
|
+
useEffect(() => () => {
|
|
1025
|
+
if (!--entry._refs) {
|
|
1026
|
+
registry.delete(css);
|
|
1027
|
+
cleanup();
|
|
1028
|
+
}
|
|
1029
|
+
if (!--globalEntry._refs) {
|
|
1030
|
+
globalStyleSheetRegistry.delete(css);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
return entry._sheet;
|
|
1034
|
+
};
|
|
1035
|
+
const Style = (props) => {
|
|
1036
|
+
const css = props.children;
|
|
1037
|
+
if (typeof css == "function") {
|
|
1038
|
+
// Dynamic CSS will be inserted into the DOM as a <style> element.
|
|
1039
|
+
const styleEl = createElement("style", {}, Text({
|
|
1040
|
+
text: css,
|
|
1041
|
+
marker: false,
|
|
1042
|
+
}));
|
|
1043
|
+
return props.light
|
|
1044
|
+
? Portal({ mount: document.head, children: styleEl })
|
|
1045
|
+
: styleEl;
|
|
1046
|
+
}
|
|
1047
|
+
// Static CSS will be inserted as an adopted stylesheet and cached.
|
|
1048
|
+
if (css) {
|
|
1049
|
+
const renderer = useRenderer();
|
|
1050
|
+
const styleRoot = props.light
|
|
1051
|
+
? document
|
|
1052
|
+
: renderer._component?.shadowRoot ?? document;
|
|
1053
|
+
const registry = getLocalStyleSheetRegistry(styleRoot);
|
|
1054
|
+
const sheet = useStyleSheet(registry, css, () => {
|
|
1055
|
+
styleRoot.adoptedStyleSheets = styleRoot.adoptedStyleSheets.filter((s) => s != sheet);
|
|
1056
|
+
});
|
|
1057
|
+
styleRoot.adoptedStyleSheets.push(sheet);
|
|
1058
|
+
}
|
|
1059
|
+
return Fragment({});
|
|
1060
|
+
};
|
|
1061
|
+
const css = (strings, ...values) => {
|
|
1062
|
+
const result = () => strings.reduce((acc, string, i) => acc + string + (MaybeSignal.get(values[i]) ?? ""), "");
|
|
1063
|
+
return values.some((value) => typeof value == "function") ? result : result();
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
/** @ignore */
|
|
1067
|
+
const jsx = (type, props, key) => {
|
|
1068
|
+
if (props && key != null) {
|
|
1069
|
+
props.key = key;
|
|
1070
|
+
}
|
|
1071
|
+
return createElement(type, props);
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
export { Component, Else, ElseIf, For, Fragment, If, MaybeSignal, Portal, Style, createContext, createElement, css, defineComponents, event, h, isComponent, jsx, jsx as jsxDEV, jsx as jsxs, prop, useBatch, useContext, useMountEffect as useEffect, useMemo, useRef, useSignal };
|