ilha 0.0.1 → 0.2.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/README.md +477 -175
- package/dist/index.d.ts +83 -32
- package/dist/index.js +409 -205
- package/package.json +4 -20
package/dist/index.js
CHANGED
|
@@ -1,15 +1,74 @@
|
|
|
1
1
|
import { effect, setActiveSub, signal } from "alien-signals";
|
|
2
2
|
//#region src/index.ts
|
|
3
|
+
const __DEV__ = typeof process !== "undefined" ? process.env?.["NODE_ENV"] !== "production" : true;
|
|
4
|
+
function warn(msg) {
|
|
5
|
+
if (__DEV__) console.warn(`[ilha] ${msg}`);
|
|
6
|
+
}
|
|
7
|
+
function syncAttributes(from, to) {
|
|
8
|
+
for (const { name, value } of to.attributes) if (from.getAttribute(name) !== value) from.setAttribute(name, value);
|
|
9
|
+
for (const { name } of Array.from(from.attributes)) if (!to.hasAttribute(name)) from.removeAttribute(name);
|
|
10
|
+
}
|
|
11
|
+
function morphChildren(fromParent, toParent) {
|
|
12
|
+
const fromNodes = Array.from(fromParent.childNodes);
|
|
13
|
+
const toNodes = Array.from(toParent.childNodes);
|
|
14
|
+
for (let i = fromNodes.length - 1; i >= toNodes.length; i--) fromNodes[i].remove();
|
|
15
|
+
for (let i = 0; i < toNodes.length; i++) {
|
|
16
|
+
const toNode = toNodes[i];
|
|
17
|
+
const fromNode = fromNodes[i];
|
|
18
|
+
if (!fromNode) {
|
|
19
|
+
fromParent.appendChild(toNode.cloneNode(true));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (fromNode.nodeType !== toNode.nodeType) {
|
|
23
|
+
fromParent.replaceChild(toNode.cloneNode(true), fromNode);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (fromNode.nodeType === 3 || fromNode.nodeType === 8) {
|
|
27
|
+
if (fromNode.nodeValue !== toNode.nodeValue) fromNode.nodeValue = toNode.nodeValue;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (fromNode.nodeType === 1) {
|
|
31
|
+
const fromEl = fromNode;
|
|
32
|
+
const toEl = toNode;
|
|
33
|
+
if (fromEl.localName !== toEl.localName || fromEl.namespaceURI !== toEl.namespaceURI) {
|
|
34
|
+
fromParent.replaceChild(toEl.cloneNode(true), fromEl);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (fromEl.localName === "input" && fromEl.type !== toEl.type) {
|
|
38
|
+
fromParent.replaceChild(toEl.cloneNode(true), fromEl);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
syncAttributes(fromEl, toEl);
|
|
42
|
+
if (fromEl.localName === "textarea") {
|
|
43
|
+
const newText = toEl.textContent ?? "";
|
|
44
|
+
if (fromEl.textContent !== newText) fromEl.textContent = newText;
|
|
45
|
+
fromEl.value = fromEl.defaultValue;
|
|
46
|
+
} else morphChildren(fromEl, toEl);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function morphInner(from, to) {
|
|
51
|
+
if (from.localName !== to.localName || from.namespaceURI !== to.namespaceURI) throw new Error("[ilha] morph: elements must match");
|
|
52
|
+
morphChildren(from, to);
|
|
53
|
+
}
|
|
3
54
|
function validateSchema(schema, value) {
|
|
4
55
|
const result = schema["~standard"].validate(value);
|
|
5
56
|
if (result instanceof Promise) throw new Error("[ilha] Async schemas are not supported.");
|
|
6
57
|
if (result.issues) throw new Error(`[ilha] Validation failed:\n${result.issues.map((i) => ` - ${i.message}`).join("\n")}`);
|
|
7
58
|
return result.value;
|
|
8
59
|
}
|
|
60
|
+
const ESC = {
|
|
61
|
+
"&": "&",
|
|
62
|
+
"<": "<",
|
|
63
|
+
">": ">",
|
|
64
|
+
"\"": """,
|
|
65
|
+
"'": "'"
|
|
66
|
+
};
|
|
9
67
|
function escapeHtml(value) {
|
|
10
|
-
return String(value).replace(
|
|
68
|
+
return String(value).replace(/[&<>"']/g, (c) => ESC[c]);
|
|
11
69
|
}
|
|
12
70
|
function dedentString(str) {
|
|
71
|
+
if (str.length === 0 || str[0] !== "\n") return str;
|
|
13
72
|
const lines = str.split("\n");
|
|
14
73
|
while (lines.length && lines[0].trim() === "") lines.shift();
|
|
15
74
|
while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
|
|
@@ -21,8 +80,12 @@ const RAW = Symbol("ilha.raw");
|
|
|
21
80
|
const SLOT_ACCESSOR = Symbol("ilha.slotAccessor");
|
|
22
81
|
const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
23
82
|
const SLOT_ATTR = "data-ilha-slot";
|
|
24
|
-
const
|
|
83
|
+
const PROPS_ATTR = "data-ilha-props";
|
|
25
84
|
const STATE_ATTR = "data-ilha-state";
|
|
85
|
+
const CSS_ATTR = "data-ilha-css";
|
|
86
|
+
function buildScopedStyle(css) {
|
|
87
|
+
return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css}}</style>`;
|
|
88
|
+
}
|
|
26
89
|
function makeSlotAccessor(render) {
|
|
27
90
|
const fn = (props) => ({
|
|
28
91
|
[RAW]: true,
|
|
@@ -48,21 +111,37 @@ function ilhaRaw(value) {
|
|
|
48
111
|
value
|
|
49
112
|
};
|
|
50
113
|
}
|
|
114
|
+
function ilhaCss(strings, ...values) {
|
|
115
|
+
if (typeof strings === "string") return strings;
|
|
116
|
+
let result = "";
|
|
117
|
+
for (let i = 0; i < strings.length; i++) {
|
|
118
|
+
result += strings[i];
|
|
119
|
+
if (i < values.length) result += String(values[i]);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function interpolateValue(v) {
|
|
124
|
+
if (v == null) return "";
|
|
125
|
+
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
126
|
+
if (typeof v === "object" && RAW in v) return v.value;
|
|
127
|
+
if (isSlotAccessor(v)) return v.toString();
|
|
128
|
+
if (isSignalAccessor(v)) return escapeHtml(v());
|
|
129
|
+
if (typeof v === "function") return escapeHtml(v());
|
|
130
|
+
return escapeHtml(v);
|
|
131
|
+
}
|
|
51
132
|
function ilhaHtml(strings, ...values) {
|
|
52
133
|
let result = "";
|
|
53
134
|
for (let i = 0; i < strings.length; i++) {
|
|
54
135
|
result += strings[i];
|
|
55
|
-
if (i < values.length)
|
|
56
|
-
const v = values[i];
|
|
57
|
-
if (v == null) continue;
|
|
58
|
-
if (typeof v === "object" && RAW in v) result += v.value;
|
|
59
|
-
else if (isSlotAccessor(v)) result += v.toString();
|
|
60
|
-
else if (isSignalAccessor(v)) result += escapeHtml(v());
|
|
61
|
-
else if (typeof v === "function") result += escapeHtml(v());
|
|
62
|
-
else result += escapeHtml(v);
|
|
63
|
-
}
|
|
136
|
+
if (i < values.length) result += interpolateValue(values[i]);
|
|
64
137
|
}
|
|
65
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
[RAW]: true,
|
|
140
|
+
value: dedentString(result)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function unwrapHtml(v) {
|
|
144
|
+
return typeof v === "object" && RAW in v ? v.value : v;
|
|
66
145
|
}
|
|
67
146
|
const contextRegistry = /* @__PURE__ */ new Map();
|
|
68
147
|
function ilhaContext(key, initial) {
|
|
@@ -75,39 +154,23 @@ function ilhaContext(key, initial) {
|
|
|
75
154
|
contextRegistry.set(key, accessor);
|
|
76
155
|
return accessor;
|
|
77
156
|
}
|
|
78
|
-
function
|
|
79
|
-
const derived = {};
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
const result = entry.fn({
|
|
82
|
-
state,
|
|
83
|
-
input,
|
|
84
|
-
signal: new AbortController().signal
|
|
85
|
-
});
|
|
86
|
-
if (result instanceof Promise) derived[entry.key] = {
|
|
87
|
-
loading: true,
|
|
88
|
-
value: void 0,
|
|
89
|
-
error: void 0
|
|
90
|
-
};
|
|
91
|
-
else derived[entry.key] = {
|
|
92
|
-
loading: false,
|
|
93
|
-
value: result,
|
|
94
|
-
error: void 0
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
return derived;
|
|
98
|
-
}
|
|
99
|
-
function buildDerivedSignals(entries, state, input) {
|
|
157
|
+
function buildDerivedSignals(entries, state, input, derivedSnapshot) {
|
|
100
158
|
const envelopes = /* @__PURE__ */ new Map();
|
|
101
159
|
const stops = [];
|
|
102
160
|
for (const entry of entries) {
|
|
103
|
-
const env = signal({
|
|
161
|
+
const env = signal(derivedSnapshot?.[entry.key] ?? {
|
|
104
162
|
loading: true,
|
|
105
163
|
value: void 0,
|
|
106
164
|
error: void 0
|
|
107
165
|
});
|
|
108
166
|
envelopes.set(entry.key, env);
|
|
109
167
|
let ac = new AbortController();
|
|
168
|
+
let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
|
|
110
169
|
const stopEffect = effect(() => {
|
|
170
|
+
if (skipFirst) {
|
|
171
|
+
skipFirst = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
111
174
|
ac.abort();
|
|
112
175
|
ac = new AbortController();
|
|
113
176
|
const currentAc = ac;
|
|
@@ -184,8 +247,7 @@ function resolveBindConfig(el) {
|
|
|
184
247
|
return input.checked ? input.value : void 0;
|
|
185
248
|
},
|
|
186
249
|
write: (el, v) => {
|
|
187
|
-
|
|
188
|
-
input.checked = String(v ?? "") === input.value;
|
|
250
|
+
el.checked = String(v ?? "") === el.value;
|
|
189
251
|
}
|
|
190
252
|
};
|
|
191
253
|
if (tag === "input" && type === "number") return {
|
|
@@ -201,15 +263,15 @@ function resolveBindConfig(el) {
|
|
|
201
263
|
write: (el, v) => el.value = String(v ?? "")
|
|
202
264
|
};
|
|
203
265
|
}
|
|
204
|
-
function applyBindings(
|
|
266
|
+
function applyBindings(host, bindings, state) {
|
|
205
267
|
const cleanups = [];
|
|
206
268
|
for (const binding of bindings) {
|
|
207
|
-
const
|
|
269
|
+
const accessor = typeof binding.target === "function" ? binding.target : state[binding.target];
|
|
270
|
+
const targets = binding.selector === "" ? [host] : Array.from(host.querySelectorAll(binding.selector));
|
|
271
|
+
if (__DEV__ && binding.selector !== "" && targets.length === 0) warn(`bind(): selector "${binding.selector}" matched no elements inside the island host. Check that the element exists in your render output.`);
|
|
208
272
|
for (const target of targets) {
|
|
209
273
|
const { event, read, write } = resolveBindConfig(target);
|
|
210
|
-
const
|
|
211
|
-
const input = target;
|
|
212
|
-
const isRadio = input.tagName.toLowerCase() === "input" && input.type?.toLowerCase() === "radio";
|
|
274
|
+
const isRadio = target.tagName.toLowerCase() === "input" && target.type?.toLowerCase() === "radio";
|
|
213
275
|
write(target, accessor());
|
|
214
276
|
const listener = () => {
|
|
215
277
|
const raw = read(target);
|
|
@@ -229,112 +291,148 @@ function applyBindings(el, bindings, state) {
|
|
|
229
291
|
}
|
|
230
292
|
return () => cleanups.forEach((c) => c());
|
|
231
293
|
}
|
|
232
|
-
function parseOnArgs(selectorOrCombined
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const atIdx = selectorOrCombined.lastIndexOf("@");
|
|
237
|
-
if (atIdx === -1) {
|
|
238
|
-
selector = "";
|
|
239
|
-
rawEvent = selectorOrCombined.startsWith("@") ? selectorOrCombined.slice(1) : selectorOrCombined;
|
|
240
|
-
} else {
|
|
241
|
-
selector = selectorOrCombined.slice(0, atIdx);
|
|
242
|
-
rawEvent = selectorOrCombined.slice(atIdx + 1);
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
selector = selectorOrCombined;
|
|
246
|
-
rawEvent = callbackOrEventType;
|
|
247
|
-
}
|
|
248
|
-
const parts = rawEvent.split(":");
|
|
294
|
+
function parseOnArgs(selectorOrCombined) {
|
|
295
|
+
const atIdx = selectorOrCombined.lastIndexOf("@");
|
|
296
|
+
const selector = atIdx === -1 ? "" : selectorOrCombined.slice(0, atIdx);
|
|
297
|
+
const parts = (atIdx === -1 ? selectorOrCombined : selectorOrCombined.slice(atIdx + 1)).split(":");
|
|
249
298
|
const eventType = parts[0];
|
|
250
299
|
const modifiers = new Set(parts.slice(1));
|
|
251
|
-
const options = {
|
|
252
|
-
once: modifiers.has("once"),
|
|
253
|
-
capture: modifiers.has("capture"),
|
|
254
|
-
passive: modifiers.has("passive")
|
|
255
|
-
};
|
|
256
300
|
return {
|
|
257
301
|
selector,
|
|
258
302
|
eventType,
|
|
259
|
-
options
|
|
303
|
+
options: {
|
|
304
|
+
once: modifiers.has("once"),
|
|
305
|
+
capture: modifiers.has("capture"),
|
|
306
|
+
passive: modifiers.has("passive")
|
|
307
|
+
}
|
|
260
308
|
};
|
|
261
309
|
}
|
|
310
|
+
const _mountedHosts = __DEV__ ? /* @__PURE__ */ new WeakSet() : null;
|
|
262
311
|
var IlhaBuilder = class IlhaBuilder {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this.
|
|
266
|
-
this._deriveds = _deriveds;
|
|
267
|
-
this._ons = _ons;
|
|
268
|
-
this._effects = _effects;
|
|
269
|
-
this._slots = _slots;
|
|
270
|
-
this._transition = _transition;
|
|
271
|
-
this._binds = _binds;
|
|
312
|
+
_cfg;
|
|
313
|
+
constructor(cfg) {
|
|
314
|
+
this._cfg = cfg;
|
|
272
315
|
}
|
|
273
316
|
input(schema) {
|
|
274
|
-
return new IlhaBuilder(
|
|
317
|
+
return new IlhaBuilder({
|
|
318
|
+
schema,
|
|
319
|
+
states: [],
|
|
320
|
+
deriveds: [],
|
|
321
|
+
ons: [],
|
|
322
|
+
effects: [],
|
|
323
|
+
onMounts: [],
|
|
324
|
+
slots: {},
|
|
325
|
+
transition: null,
|
|
326
|
+
binds: [],
|
|
327
|
+
css: null
|
|
328
|
+
});
|
|
275
329
|
}
|
|
276
330
|
state(key, init) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
331
|
+
const cfg = this._cfg;
|
|
332
|
+
return new IlhaBuilder({
|
|
333
|
+
...cfg,
|
|
334
|
+
states: [...cfg.states, {
|
|
335
|
+
key,
|
|
336
|
+
init
|
|
337
|
+
}]
|
|
338
|
+
});
|
|
281
339
|
}
|
|
282
340
|
derived(key, fn) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
341
|
+
const cfg = this._cfg;
|
|
342
|
+
return new IlhaBuilder({
|
|
343
|
+
...cfg,
|
|
344
|
+
deriveds: [...cfg.deriveds, {
|
|
345
|
+
key,
|
|
346
|
+
fn
|
|
347
|
+
}]
|
|
348
|
+
});
|
|
287
349
|
}
|
|
288
|
-
bind(selector,
|
|
289
|
-
return new IlhaBuilder(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
350
|
+
bind(selector, target) {
|
|
351
|
+
return new IlhaBuilder({
|
|
352
|
+
...this._cfg,
|
|
353
|
+
binds: [...this._cfg.binds, {
|
|
354
|
+
selector,
|
|
355
|
+
target
|
|
356
|
+
}]
|
|
357
|
+
});
|
|
293
358
|
}
|
|
294
|
-
on(selectorOrCombined,
|
|
295
|
-
const parsed = parseOnArgs(selectorOrCombined
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
359
|
+
on(selectorOrCombined, handler) {
|
|
360
|
+
const parsed = parseOnArgs(selectorOrCombined);
|
|
361
|
+
return new IlhaBuilder({
|
|
362
|
+
...this._cfg,
|
|
363
|
+
ons: [...this._cfg.ons, {
|
|
364
|
+
selector: parsed.selector,
|
|
365
|
+
event: parsed.eventType,
|
|
366
|
+
options: parsed.options,
|
|
367
|
+
handler
|
|
368
|
+
}]
|
|
369
|
+
});
|
|
303
370
|
}
|
|
304
371
|
effect(fn) {
|
|
305
|
-
return new IlhaBuilder(
|
|
372
|
+
return new IlhaBuilder({
|
|
373
|
+
...this._cfg,
|
|
374
|
+
effects: [...this._cfg.effects, { fn }]
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
onMount(fn) {
|
|
378
|
+
return new IlhaBuilder({
|
|
379
|
+
...this._cfg,
|
|
380
|
+
onMounts: [...this._cfg.onMounts, { fn }]
|
|
381
|
+
});
|
|
306
382
|
}
|
|
307
383
|
slot(name, island) {
|
|
308
|
-
return new IlhaBuilder(
|
|
309
|
-
...this.
|
|
310
|
-
|
|
311
|
-
|
|
384
|
+
return new IlhaBuilder({
|
|
385
|
+
...this._cfg,
|
|
386
|
+
slots: {
|
|
387
|
+
...this._cfg.slots,
|
|
388
|
+
[name]: island
|
|
389
|
+
}
|
|
390
|
+
});
|
|
312
391
|
}
|
|
313
392
|
transition(opts) {
|
|
314
|
-
return new IlhaBuilder(
|
|
393
|
+
return new IlhaBuilder({
|
|
394
|
+
...this._cfg,
|
|
395
|
+
transition: opts
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
css(strings, ...values) {
|
|
399
|
+
let source;
|
|
400
|
+
if (typeof strings === "string") source = strings;
|
|
401
|
+
else {
|
|
402
|
+
let result = "";
|
|
403
|
+
for (let i = 0; i < strings.length; i++) {
|
|
404
|
+
result += strings[i];
|
|
405
|
+
if (i < values.length) result += String(values[i]);
|
|
406
|
+
}
|
|
407
|
+
source = result;
|
|
408
|
+
}
|
|
409
|
+
if (__DEV__ && this._cfg.css !== null) warn("css(): called more than once on the same builder chain. The previous stylesheet has been discarded. Compose styles into a single .css() call instead.");
|
|
410
|
+
return new IlhaBuilder({
|
|
411
|
+
...this._cfg,
|
|
412
|
+
css: source
|
|
413
|
+
});
|
|
315
414
|
}
|
|
316
415
|
render(fn) {
|
|
317
|
-
const schema = this.
|
|
318
|
-
const
|
|
319
|
-
const deriveds = this._deriveds;
|
|
320
|
-
const ons = this._ons;
|
|
321
|
-
const effects = this._effects;
|
|
322
|
-
const slotDefs = this._slots;
|
|
323
|
-
const transition = this._transition;
|
|
324
|
-
const binds = this._binds;
|
|
416
|
+
const { schema, states, deriveds, ons, effects, onMounts, slots: slotDefs, transition, binds, css: cssSource } = this._cfg;
|
|
417
|
+
const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
|
|
325
418
|
function resolveInput(props) {
|
|
326
419
|
const value = props ?? {};
|
|
327
420
|
if (!schema) return value;
|
|
328
421
|
return validateSchema(schema, value);
|
|
329
422
|
}
|
|
330
|
-
function makeSlotsProxy(ssr) {
|
|
423
|
+
function makeSlotsProxy(ssr, host) {
|
|
331
424
|
return new Proxy({}, { get(_, prop) {
|
|
332
425
|
const name = String(prop);
|
|
333
426
|
if (!slotDefs[name]) return makeSlotAccessor(() => "");
|
|
334
|
-
if (ssr) return makeSlotAccessor((props) =>
|
|
427
|
+
if (ssr) return makeSlotAccessor((props) => {
|
|
428
|
+
const json = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
429
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(name)}"${json}>${slotDefs[name].toString(props)}</div>`;
|
|
430
|
+
});
|
|
335
431
|
return makeSlotAccessor((props) => {
|
|
336
|
-
const
|
|
337
|
-
|
|
432
|
+
const liveSlot = host?.querySelector(`[${SLOT_ATTR}="${escapeHtml(name)}"]`);
|
|
433
|
+
if (liveSlot) return liveSlot.outerHTML;
|
|
434
|
+
const json = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
435
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(name)}"${json}></div>`;
|
|
338
436
|
});
|
|
339
437
|
} });
|
|
340
438
|
}
|
|
@@ -361,7 +459,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
361
459
|
}
|
|
362
460
|
return state;
|
|
363
461
|
}
|
|
364
|
-
function renderToString(props) {
|
|
462
|
+
function renderToString(props, sync = false) {
|
|
365
463
|
const input = resolveInput(props);
|
|
366
464
|
const state = buildPlainState(input);
|
|
367
465
|
const slots = makeSlotsProxy(true);
|
|
@@ -382,19 +480,24 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
382
480
|
};
|
|
383
481
|
}
|
|
384
482
|
});
|
|
385
|
-
if (!results.some((r) => r.result instanceof Promise)) {
|
|
483
|
+
if (!results.some((r) => r.result instanceof Promise) || sync) {
|
|
386
484
|
const derived = {};
|
|
387
|
-
for (const r of results) derived[r.key] = {
|
|
485
|
+
for (const r of results) if (r.result instanceof Promise) derived[r.key] = {
|
|
486
|
+
loading: true,
|
|
487
|
+
value: void 0,
|
|
488
|
+
error: void 0
|
|
489
|
+
};
|
|
490
|
+
else derived[r.key] = {
|
|
388
491
|
loading: false,
|
|
389
492
|
value: r.result,
|
|
390
493
|
error: void 0
|
|
391
494
|
};
|
|
392
|
-
return fn({
|
|
495
|
+
return stylePrefix + unwrapHtml(fn({
|
|
393
496
|
state,
|
|
394
497
|
derived,
|
|
395
498
|
input,
|
|
396
499
|
slots
|
|
397
|
-
});
|
|
500
|
+
}));
|
|
398
501
|
}
|
|
399
502
|
return Promise.all(results.map(async (r) => {
|
|
400
503
|
try {
|
|
@@ -419,54 +522,82 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
419
522
|
})).then((resolved) => {
|
|
420
523
|
const derived = {};
|
|
421
524
|
for (const r of resolved) derived[r.key] = r.envelope;
|
|
422
|
-
return fn({
|
|
525
|
+
return stylePrefix + unwrapHtml(fn({
|
|
423
526
|
state,
|
|
424
527
|
derived,
|
|
425
528
|
input,
|
|
426
529
|
slots
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
function renderToStringSyncOnly(props) {
|
|
431
|
-
const input = resolveInput(props);
|
|
432
|
-
const state = buildPlainState(input);
|
|
433
|
-
const slots = makeSlotsProxy(true);
|
|
434
|
-
return fn({
|
|
435
|
-
state,
|
|
436
|
-
derived: makePlainDerived(deriveds, state, input),
|
|
437
|
-
input,
|
|
438
|
-
slots
|
|
530
|
+
}));
|
|
439
531
|
});
|
|
440
532
|
}
|
|
441
|
-
function mountIsland(
|
|
533
|
+
function mountIsland(host, props) {
|
|
534
|
+
if (__DEV__ && _mountedHosts) {
|
|
535
|
+
if (_mountedHosts.has(host)) {
|
|
536
|
+
warn(`mount(): this element is already mounted. Call the previous unmount() first to avoid memory leaks and duplicate event listeners.
|
|
537
|
+
Element: ${host.outerHTML.slice(0, 120)}`);
|
|
538
|
+
return () => {};
|
|
539
|
+
}
|
|
540
|
+
_mountedHosts.add(host);
|
|
541
|
+
}
|
|
542
|
+
if (props === void 0) {
|
|
543
|
+
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
544
|
+
if (rawProps) try {
|
|
545
|
+
props = JSON.parse(rawProps);
|
|
546
|
+
} catch {
|
|
547
|
+
warn("Failed to parse data-ilha-props — invalid JSON, falling back to empty props.");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
442
550
|
const input = resolveInput(props);
|
|
443
|
-
let
|
|
444
|
-
const rawState =
|
|
551
|
+
let snapshotRaw;
|
|
552
|
+
const rawState = host.getAttribute(STATE_ATTR);
|
|
445
553
|
if (rawState) try {
|
|
446
|
-
|
|
554
|
+
snapshotRaw = JSON.parse(rawState);
|
|
447
555
|
} catch {
|
|
448
|
-
|
|
556
|
+
warn("Failed to parse data-ilha-state — invalid JSON, snapshot ignored.");
|
|
557
|
+
}
|
|
558
|
+
const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount")) : void 0;
|
|
559
|
+
const derivedSnapshotRaw = snapshotRaw?._derived;
|
|
560
|
+
let derivedSnapshot;
|
|
561
|
+
if (derivedSnapshotRaw) {
|
|
562
|
+
derivedSnapshot = {};
|
|
563
|
+
for (const [k, v] of Object.entries(derivedSnapshotRaw)) if (v.error && !(v.error instanceof Error)) derivedSnapshot[k] = {
|
|
564
|
+
...v,
|
|
565
|
+
error: new Error(String(v.error))
|
|
566
|
+
};
|
|
567
|
+
else derivedSnapshot[k] = v;
|
|
449
568
|
}
|
|
450
|
-
const
|
|
569
|
+
const hydrated = snapshotRaw != null;
|
|
570
|
+
const shouldSkipOnMount = hydrated && snapshotRaw?.["_skipOnMount"] === true;
|
|
571
|
+
const state = buildSignalState(input, stateSnapshot);
|
|
451
572
|
const cleanups = [];
|
|
452
573
|
if (transition?.enter) {
|
|
453
|
-
const result = transition.enter(
|
|
574
|
+
const result = transition.enter(host);
|
|
454
575
|
if (result instanceof Promise) result.catch(console.error);
|
|
455
576
|
}
|
|
456
|
-
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input);
|
|
577
|
+
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input, derivedSnapshot);
|
|
457
578
|
cleanups.push(stopDerived);
|
|
579
|
+
const slotCleanups = /* @__PURE__ */ new Map();
|
|
458
580
|
const slotEls = /* @__PURE__ */ new Map();
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
581
|
+
function mountSlots() {
|
|
582
|
+
for (const [name, childIsland] of Object.entries(slotDefs)) {
|
|
583
|
+
const slotEl = host.querySelector(`[${SLOT_ATTR}="${name}"]`);
|
|
584
|
+
if (!slotEl) continue;
|
|
585
|
+
if (slotEls.get(name) === slotEl) continue;
|
|
586
|
+
slotEls.set(name, slotEl);
|
|
587
|
+
slotCleanups.get(name)?.();
|
|
588
|
+
let slotProps;
|
|
589
|
+
const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
|
|
590
|
+
if (rawProps) try {
|
|
591
|
+
slotProps = JSON.parse(rawProps);
|
|
592
|
+
} catch {
|
|
593
|
+
warn(`Failed to parse props on [${SLOT_ATTR}="${name}"] — invalid JSON ignored.`);
|
|
594
|
+
}
|
|
595
|
+
const prevSub = setActiveSub(void 0);
|
|
596
|
+
try {
|
|
597
|
+
slotCleanups.set(name, childIsland.mount(slotEl, slotProps));
|
|
598
|
+
} finally {
|
|
599
|
+
setActiveSub(prevSub);
|
|
600
|
+
}
|
|
470
601
|
}
|
|
471
602
|
}
|
|
472
603
|
const listeners = [];
|
|
@@ -474,17 +605,22 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
474
605
|
function attachListeners() {
|
|
475
606
|
for (const entry of ons) {
|
|
476
607
|
if (entry.options.once && firedOnce.has(entry)) continue;
|
|
477
|
-
|
|
608
|
+
const targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector));
|
|
609
|
+
if (__DEV__ && entry.selector !== "" && targets.length === 0) warn(`on(): selector "${entry.selector}" matched no elements at mount time. If the element is rendered later, this is expected — otherwise check your selector.`);
|
|
610
|
+
targets.forEach((listenerTarget) => {
|
|
478
611
|
const listener = (event) => {
|
|
479
612
|
if (entry.options.once) {
|
|
480
613
|
firedOnce.add(entry);
|
|
481
614
|
for (const l of listeners.filter((l) => l.entry === entry)) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
482
615
|
listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
|
|
483
616
|
}
|
|
617
|
+
const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
|
|
484
618
|
const result = entry.handler({
|
|
485
619
|
state,
|
|
620
|
+
derived,
|
|
486
621
|
input,
|
|
487
|
-
|
|
622
|
+
host,
|
|
623
|
+
target: eventTarget,
|
|
488
624
|
event
|
|
489
625
|
});
|
|
490
626
|
if (result instanceof Promise) result.catch(console.error);
|
|
@@ -493,9 +629,9 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
493
629
|
...entry.options,
|
|
494
630
|
once: false
|
|
495
631
|
};
|
|
496
|
-
|
|
632
|
+
listenerTarget.addEventListener(entry.event, listener, opts);
|
|
497
633
|
listeners.push({
|
|
498
|
-
target,
|
|
634
|
+
target: listenerTarget,
|
|
499
635
|
type: entry.event,
|
|
500
636
|
fn: listener,
|
|
501
637
|
options: opts,
|
|
@@ -508,54 +644,54 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
508
644
|
for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
509
645
|
listeners.length = 0;
|
|
510
646
|
}
|
|
511
|
-
const slots = makeSlotsProxy(false);
|
|
512
|
-
|
|
647
|
+
const slots = makeSlotsProxy(false, host);
|
|
648
|
+
if (!(hydrated && host.childNodes.length > 0)) host.innerHTML = stylePrefix + unwrapHtml(fn({
|
|
513
649
|
state,
|
|
514
650
|
derived,
|
|
515
651
|
input,
|
|
516
652
|
slots
|
|
517
|
-
});
|
|
653
|
+
}));
|
|
518
654
|
attachListeners();
|
|
519
|
-
let stopBindings = applyBindings(
|
|
655
|
+
let stopBindings = applyBindings(host, binds, state);
|
|
520
656
|
cleanups.push(() => stopBindings());
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
let
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
console.warn(`[ilha] Failed to parse data-props on [${SLOT_ATTR}="${name}"]`);
|
|
657
|
+
mountSlots();
|
|
658
|
+
cleanups.push(() => slotCleanups.forEach((unmount) => unmount()));
|
|
659
|
+
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
660
|
+
const prevSub = setActiveSub(void 0);
|
|
661
|
+
let userCleanup;
|
|
662
|
+
try {
|
|
663
|
+
userCleanup = entry.fn({
|
|
664
|
+
state,
|
|
665
|
+
derived,
|
|
666
|
+
input,
|
|
667
|
+
host,
|
|
668
|
+
hydrated
|
|
669
|
+
});
|
|
670
|
+
} finally {
|
|
671
|
+
setActiveSub(prevSub);
|
|
537
672
|
}
|
|
538
|
-
cleanups.push(
|
|
673
|
+
if (userCleanup) cleanups.push(userCleanup);
|
|
539
674
|
}
|
|
540
675
|
let initialized = false;
|
|
541
676
|
const stopRender = effect(() => {
|
|
542
|
-
const html = fn({
|
|
677
|
+
const html = stylePrefix + unwrapHtml(fn({
|
|
543
678
|
state,
|
|
544
679
|
derived,
|
|
545
680
|
input,
|
|
546
681
|
slots
|
|
547
|
-
});
|
|
682
|
+
}));
|
|
548
683
|
if (!initialized) {
|
|
549
684
|
initialized = true;
|
|
550
685
|
return;
|
|
551
686
|
}
|
|
552
|
-
snapshotSlots();
|
|
553
687
|
detachListeners();
|
|
554
688
|
stopBindings();
|
|
555
|
-
|
|
556
|
-
|
|
689
|
+
const tpl = document.createElement("template");
|
|
690
|
+
tpl.innerHTML = `<div>${html}</div>`;
|
|
691
|
+
morphInner(host, tpl.content.firstElementChild);
|
|
557
692
|
attachListeners();
|
|
558
|
-
stopBindings = applyBindings(
|
|
693
|
+
stopBindings = applyBindings(host, binds, state);
|
|
694
|
+
mountSlots();
|
|
559
695
|
});
|
|
560
696
|
cleanups.push(stopRender);
|
|
561
697
|
cleanups.push(detachListeners);
|
|
@@ -569,7 +705,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
569
705
|
userCleanup = entry.fn({
|
|
570
706
|
state,
|
|
571
707
|
input,
|
|
572
|
-
|
|
708
|
+
host
|
|
573
709
|
});
|
|
574
710
|
});
|
|
575
711
|
cleanups.push(() => {
|
|
@@ -577,9 +713,13 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
577
713
|
if (userCleanup) userCleanup();
|
|
578
714
|
});
|
|
579
715
|
}
|
|
716
|
+
let tornDown = false;
|
|
580
717
|
return () => {
|
|
718
|
+
if (tornDown) return;
|
|
719
|
+
tornDown = true;
|
|
720
|
+
if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
|
|
581
721
|
if (transition?.leave) {
|
|
582
|
-
const result = transition.leave(
|
|
722
|
+
const result = transition.leave(host);
|
|
583
723
|
if (result instanceof Promise) {
|
|
584
724
|
result.then(() => cleanups.forEach((c) => c())).catch(console.error);
|
|
585
725
|
return;
|
|
@@ -591,57 +731,111 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
591
731
|
const island = function(props) {
|
|
592
732
|
return renderToString(props);
|
|
593
733
|
};
|
|
594
|
-
island.toString = (props) =>
|
|
595
|
-
island.mount = (
|
|
734
|
+
island.toString = (props) => renderToString(props, true);
|
|
735
|
+
island.mount = (host, props) => mountIsland(host, props);
|
|
736
|
+
island.hydratable = async (props, opts) => {
|
|
737
|
+
const { name, as: tag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
|
|
738
|
+
const resolvedProps = props ?? {};
|
|
739
|
+
const inner = await renderToString(resolvedProps);
|
|
740
|
+
const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
|
|
741
|
+
let stateAttr = "";
|
|
742
|
+
if (snapshot !== false) {
|
|
743
|
+
const doState = snapshot === true || snapshot.state !== false;
|
|
744
|
+
const doDerived = snapshot === true || snapshot.derived !== false;
|
|
745
|
+
const doSkipOnMount = explicitSkipOnMount ?? (doState || doDerived);
|
|
746
|
+
const snapshotData = {};
|
|
747
|
+
const input = resolveInput(resolvedProps);
|
|
748
|
+
const plainState = buildPlainState(input);
|
|
749
|
+
if (doState) for (const entry of states) snapshotData[entry.key] = plainState[entry.key]();
|
|
750
|
+
if (doDerived) {
|
|
751
|
+
const derivedResults = {};
|
|
752
|
+
for (const entry of deriveds) try {
|
|
753
|
+
const result = await Promise.resolve(entry.fn({
|
|
754
|
+
state: plainState,
|
|
755
|
+
input,
|
|
756
|
+
signal: new AbortController().signal
|
|
757
|
+
}));
|
|
758
|
+
derivedResults[entry.key] = {
|
|
759
|
+
loading: false,
|
|
760
|
+
value: result,
|
|
761
|
+
error: void 0
|
|
762
|
+
};
|
|
763
|
+
} catch (err) {
|
|
764
|
+
derivedResults[entry.key] = {
|
|
765
|
+
loading: false,
|
|
766
|
+
value: void 0,
|
|
767
|
+
error: err instanceof Error ? err.message : String(err)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
snapshotData["_derived"] = derivedResults;
|
|
771
|
+
}
|
|
772
|
+
if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
|
|
773
|
+
stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
|
|
774
|
+
}
|
|
775
|
+
return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}>${inner}</${tag}>`;
|
|
776
|
+
};
|
|
596
777
|
return island;
|
|
597
778
|
}
|
|
598
779
|
};
|
|
599
780
|
function ilhaFrom(selector, island, props) {
|
|
600
|
-
const
|
|
601
|
-
if (!
|
|
781
|
+
const host = typeof selector === "string" ? document.querySelector(selector) : selector;
|
|
782
|
+
if (!host) {
|
|
602
783
|
console.warn(`[ilha] from(): element not found: ${selector}`);
|
|
603
784
|
return null;
|
|
604
785
|
}
|
|
605
|
-
return island.mount(
|
|
786
|
+
return island.mount(host, props);
|
|
606
787
|
}
|
|
607
788
|
function mountAll(registry, options = {}) {
|
|
608
789
|
const root = options.root ?? document.body;
|
|
609
790
|
const lazy = options.lazy ?? false;
|
|
610
|
-
const hydrate = options.hydrate ?? false;
|
|
611
791
|
const unmounts = [];
|
|
612
|
-
function activateEl(
|
|
613
|
-
const name =
|
|
792
|
+
function activateEl(host) {
|
|
793
|
+
const name = host.getAttribute("data-ilha");
|
|
614
794
|
if (!name) return;
|
|
615
795
|
const island = registry[name];
|
|
616
|
-
if (!island)
|
|
796
|
+
if (!island) {
|
|
797
|
+
warn(`mount(): no island registered under the name "${name}". Available names: [${Object.keys(registry).join(", ")}]. Check the data-ilha attribute on the element.`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
617
800
|
let props = {};
|
|
618
|
-
const rawProps =
|
|
801
|
+
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
619
802
|
if (rawProps) try {
|
|
620
803
|
props = JSON.parse(rawProps);
|
|
621
804
|
} catch {
|
|
622
|
-
|
|
805
|
+
warn(`Failed to parse ${PROPS_ATTR} on [data-ilha="${name}"] — invalid JSON ignored.`);
|
|
623
806
|
}
|
|
624
|
-
|
|
625
|
-
const snapshot = el.innerHTML;
|
|
626
|
-
const unmount = island.mount(el, props);
|
|
627
|
-
if (!el.innerHTML) el.innerHTML = snapshot;
|
|
628
|
-
unmounts.push(unmount);
|
|
629
|
-
} else unmounts.push(island.mount(el, props));
|
|
807
|
+
unmounts.push(island.mount(host, props));
|
|
630
808
|
}
|
|
631
809
|
const els = Array.from(root.querySelectorAll("[data-ilha]"));
|
|
632
810
|
if (lazy && typeof IntersectionObserver !== "undefined") {
|
|
811
|
+
let disposed = false;
|
|
633
812
|
const io = new IntersectionObserver((entries) => {
|
|
813
|
+
if (disposed) return;
|
|
634
814
|
for (const entry of entries) if (entry.isIntersecting) {
|
|
635
815
|
activateEl(entry.target);
|
|
636
816
|
io.unobserve(entry.target);
|
|
637
817
|
}
|
|
638
818
|
});
|
|
639
819
|
els.forEach((el) => io.observe(el));
|
|
640
|
-
unmounts.push(() =>
|
|
820
|
+
unmounts.push(() => {
|
|
821
|
+
disposed = true;
|
|
822
|
+
io.disconnect();
|
|
823
|
+
});
|
|
641
824
|
} else els.forEach(activateEl);
|
|
642
825
|
return { unmount: () => unmounts.forEach((u) => u()) };
|
|
643
826
|
}
|
|
644
|
-
const rootBuilder = new IlhaBuilder(
|
|
827
|
+
const rootBuilder = new IlhaBuilder({
|
|
828
|
+
schema: null,
|
|
829
|
+
states: [],
|
|
830
|
+
deriveds: [],
|
|
831
|
+
ons: [],
|
|
832
|
+
effects: [],
|
|
833
|
+
onMounts: [],
|
|
834
|
+
slots: {},
|
|
835
|
+
transition: null,
|
|
836
|
+
binds: [],
|
|
837
|
+
css: null
|
|
838
|
+
});
|
|
645
839
|
const ilha = Object.assign(rootBuilder, {
|
|
646
840
|
html: ilhaHtml,
|
|
647
841
|
raw: ilhaRaw,
|
|
@@ -649,10 +843,20 @@ const ilha = Object.assign(rootBuilder, {
|
|
|
649
843
|
from: ilhaFrom,
|
|
650
844
|
context: ilhaContext
|
|
651
845
|
});
|
|
846
|
+
function type(coerce) {
|
|
847
|
+
return { "~standard": {
|
|
848
|
+
version: 1,
|
|
849
|
+
vendor: "ilha",
|
|
850
|
+
validate(value) {
|
|
851
|
+
return { value: coerce ? coerce(value) : value };
|
|
852
|
+
}
|
|
853
|
+
} };
|
|
854
|
+
}
|
|
652
855
|
const html = ilhaHtml;
|
|
653
856
|
const raw = ilhaRaw;
|
|
857
|
+
const css = ilhaCss;
|
|
654
858
|
const mount = mountAll;
|
|
655
859
|
const from = ilhaFrom;
|
|
656
860
|
const context = ilhaContext;
|
|
657
861
|
//#endregion
|
|
658
|
-
export { context, ilha as default, from, html, mount, raw };
|
|
862
|
+
export { context, css, ilha as default, from, html, mount, raw, type };
|