ilha 0.0.1 → 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/README.md +387 -176
- package/dist/index.d.ts +79 -32
- package/dist/index.js +375 -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,7 +80,7 @@ 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";
|
|
26
85
|
function makeSlotAccessor(render) {
|
|
27
86
|
const fn = (props) => ({
|
|
@@ -48,21 +107,28 @@ function ilhaRaw(value) {
|
|
|
48
107
|
value
|
|
49
108
|
};
|
|
50
109
|
}
|
|
110
|
+
function interpolateValue(v) {
|
|
111
|
+
if (v == null) return "";
|
|
112
|
+
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
113
|
+
if (typeof v === "object" && RAW in v) return v.value;
|
|
114
|
+
if (isSlotAccessor(v)) return v.toString();
|
|
115
|
+
if (isSignalAccessor(v)) return escapeHtml(v());
|
|
116
|
+
if (typeof v === "function") return escapeHtml(v());
|
|
117
|
+
return escapeHtml(v);
|
|
118
|
+
}
|
|
51
119
|
function ilhaHtml(strings, ...values) {
|
|
52
120
|
let result = "";
|
|
53
121
|
for (let i = 0; i < strings.length; i++) {
|
|
54
122
|
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
|
-
}
|
|
123
|
+
if (i < values.length) result += interpolateValue(values[i]);
|
|
64
124
|
}
|
|
65
|
-
return
|
|
125
|
+
return {
|
|
126
|
+
[RAW]: true,
|
|
127
|
+
value: dedentString(result)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function unwrapHtml(v) {
|
|
131
|
+
return typeof v === "object" && RAW in v ? v.value : v;
|
|
66
132
|
}
|
|
67
133
|
const contextRegistry = /* @__PURE__ */ new Map();
|
|
68
134
|
function ilhaContext(key, initial) {
|
|
@@ -75,39 +141,23 @@ function ilhaContext(key, initial) {
|
|
|
75
141
|
contextRegistry.set(key, accessor);
|
|
76
142
|
return accessor;
|
|
77
143
|
}
|
|
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) {
|
|
144
|
+
function buildDerivedSignals(entries, state, input, derivedSnapshot) {
|
|
100
145
|
const envelopes = /* @__PURE__ */ new Map();
|
|
101
146
|
const stops = [];
|
|
102
147
|
for (const entry of entries) {
|
|
103
|
-
const env = signal({
|
|
148
|
+
const env = signal(derivedSnapshot?.[entry.key] ?? {
|
|
104
149
|
loading: true,
|
|
105
150
|
value: void 0,
|
|
106
151
|
error: void 0
|
|
107
152
|
});
|
|
108
153
|
envelopes.set(entry.key, env);
|
|
109
154
|
let ac = new AbortController();
|
|
155
|
+
let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
|
|
110
156
|
const stopEffect = effect(() => {
|
|
157
|
+
if (skipFirst) {
|
|
158
|
+
skipFirst = false;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
111
161
|
ac.abort();
|
|
112
162
|
ac = new AbortController();
|
|
113
163
|
const currentAc = ac;
|
|
@@ -184,8 +234,7 @@ function resolveBindConfig(el) {
|
|
|
184
234
|
return input.checked ? input.value : void 0;
|
|
185
235
|
},
|
|
186
236
|
write: (el, v) => {
|
|
187
|
-
|
|
188
|
-
input.checked = String(v ?? "") === input.value;
|
|
237
|
+
el.checked = String(v ?? "") === el.value;
|
|
189
238
|
}
|
|
190
239
|
};
|
|
191
240
|
if (tag === "input" && type === "number") return {
|
|
@@ -201,15 +250,15 @@ function resolveBindConfig(el) {
|
|
|
201
250
|
write: (el, v) => el.value = String(v ?? "")
|
|
202
251
|
};
|
|
203
252
|
}
|
|
204
|
-
function applyBindings(
|
|
253
|
+
function applyBindings(host, bindings, state) {
|
|
205
254
|
const cleanups = [];
|
|
206
255
|
for (const binding of bindings) {
|
|
207
|
-
const
|
|
256
|
+
const accessor = typeof binding.target === "function" ? binding.target : state[binding.target];
|
|
257
|
+
const targets = binding.selector === "" ? [host] : Array.from(host.querySelectorAll(binding.selector));
|
|
258
|
+
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
259
|
for (const target of targets) {
|
|
209
260
|
const { event, read, write } = resolveBindConfig(target);
|
|
210
|
-
const
|
|
211
|
-
const input = target;
|
|
212
|
-
const isRadio = input.tagName.toLowerCase() === "input" && input.type?.toLowerCase() === "radio";
|
|
261
|
+
const isRadio = target.tagName.toLowerCase() === "input" && target.type?.toLowerCase() === "radio";
|
|
213
262
|
write(target, accessor());
|
|
214
263
|
const listener = () => {
|
|
215
264
|
const raw = read(target);
|
|
@@ -229,112 +278,129 @@ function applyBindings(el, bindings, state) {
|
|
|
229
278
|
}
|
|
230
279
|
return () => cleanups.forEach((c) => c());
|
|
231
280
|
}
|
|
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(":");
|
|
281
|
+
function parseOnArgs(selectorOrCombined) {
|
|
282
|
+
const atIdx = selectorOrCombined.lastIndexOf("@");
|
|
283
|
+
const selector = atIdx === -1 ? "" : selectorOrCombined.slice(0, atIdx);
|
|
284
|
+
const parts = (atIdx === -1 ? selectorOrCombined : selectorOrCombined.slice(atIdx + 1)).split(":");
|
|
249
285
|
const eventType = parts[0];
|
|
250
286
|
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
287
|
return {
|
|
257
288
|
selector,
|
|
258
289
|
eventType,
|
|
259
|
-
options
|
|
290
|
+
options: {
|
|
291
|
+
once: modifiers.has("once"),
|
|
292
|
+
capture: modifiers.has("capture"),
|
|
293
|
+
passive: modifiers.has("passive")
|
|
294
|
+
}
|
|
260
295
|
};
|
|
261
296
|
}
|
|
297
|
+
const _mountedHosts = __DEV__ ? /* @__PURE__ */ new WeakSet() : null;
|
|
262
298
|
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;
|
|
299
|
+
_cfg;
|
|
300
|
+
constructor(cfg) {
|
|
301
|
+
this._cfg = cfg;
|
|
272
302
|
}
|
|
273
303
|
input(schema) {
|
|
274
|
-
return new IlhaBuilder(
|
|
304
|
+
return new IlhaBuilder({
|
|
305
|
+
schema,
|
|
306
|
+
states: [],
|
|
307
|
+
deriveds: [],
|
|
308
|
+
ons: [],
|
|
309
|
+
effects: [],
|
|
310
|
+
onMounts: [],
|
|
311
|
+
slots: {},
|
|
312
|
+
transition: null,
|
|
313
|
+
binds: []
|
|
314
|
+
});
|
|
275
315
|
}
|
|
276
316
|
state(key, init) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
317
|
+
const cfg = this._cfg;
|
|
318
|
+
return new IlhaBuilder({
|
|
319
|
+
...cfg,
|
|
320
|
+
states: [...cfg.states, {
|
|
321
|
+
key,
|
|
322
|
+
init
|
|
323
|
+
}]
|
|
324
|
+
});
|
|
281
325
|
}
|
|
282
326
|
derived(key, fn) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
327
|
+
const cfg = this._cfg;
|
|
328
|
+
return new IlhaBuilder({
|
|
329
|
+
...cfg,
|
|
330
|
+
deriveds: [...cfg.deriveds, {
|
|
331
|
+
key,
|
|
332
|
+
fn
|
|
333
|
+
}]
|
|
334
|
+
});
|
|
287
335
|
}
|
|
288
|
-
bind(selector,
|
|
289
|
-
return new IlhaBuilder(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
336
|
+
bind(selector, target) {
|
|
337
|
+
return new IlhaBuilder({
|
|
338
|
+
...this._cfg,
|
|
339
|
+
binds: [...this._cfg.binds, {
|
|
340
|
+
selector,
|
|
341
|
+
target
|
|
342
|
+
}]
|
|
343
|
+
});
|
|
293
344
|
}
|
|
294
|
-
on(selectorOrCombined,
|
|
295
|
-
const parsed = parseOnArgs(selectorOrCombined
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
345
|
+
on(selectorOrCombined, handler) {
|
|
346
|
+
const parsed = parseOnArgs(selectorOrCombined);
|
|
347
|
+
return new IlhaBuilder({
|
|
348
|
+
...this._cfg,
|
|
349
|
+
ons: [...this._cfg.ons, {
|
|
350
|
+
selector: parsed.selector,
|
|
351
|
+
event: parsed.eventType,
|
|
352
|
+
options: parsed.options,
|
|
353
|
+
handler
|
|
354
|
+
}]
|
|
355
|
+
});
|
|
303
356
|
}
|
|
304
357
|
effect(fn) {
|
|
305
|
-
return new IlhaBuilder(
|
|
358
|
+
return new IlhaBuilder({
|
|
359
|
+
...this._cfg,
|
|
360
|
+
effects: [...this._cfg.effects, { fn }]
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
onMount(fn) {
|
|
364
|
+
return new IlhaBuilder({
|
|
365
|
+
...this._cfg,
|
|
366
|
+
onMounts: [...this._cfg.onMounts, { fn }]
|
|
367
|
+
});
|
|
306
368
|
}
|
|
307
369
|
slot(name, island) {
|
|
308
|
-
return new IlhaBuilder(
|
|
309
|
-
...this.
|
|
310
|
-
|
|
311
|
-
|
|
370
|
+
return new IlhaBuilder({
|
|
371
|
+
...this._cfg,
|
|
372
|
+
slots: {
|
|
373
|
+
...this._cfg.slots,
|
|
374
|
+
[name]: island
|
|
375
|
+
}
|
|
376
|
+
});
|
|
312
377
|
}
|
|
313
378
|
transition(opts) {
|
|
314
|
-
return new IlhaBuilder(
|
|
379
|
+
return new IlhaBuilder({
|
|
380
|
+
...this._cfg,
|
|
381
|
+
transition: opts
|
|
382
|
+
});
|
|
315
383
|
}
|
|
316
384
|
render(fn) {
|
|
317
|
-
const schema = this.
|
|
318
|
-
const states = this._states;
|
|
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;
|
|
385
|
+
const { schema, states, deriveds, ons, effects, onMounts, slots: slotDefs, transition, binds } = this._cfg;
|
|
325
386
|
function resolveInput(props) {
|
|
326
387
|
const value = props ?? {};
|
|
327
388
|
if (!schema) return value;
|
|
328
389
|
return validateSchema(schema, value);
|
|
329
390
|
}
|
|
330
|
-
function makeSlotsProxy(ssr) {
|
|
391
|
+
function makeSlotsProxy(ssr, host) {
|
|
331
392
|
return new Proxy({}, { get(_, prop) {
|
|
332
393
|
const name = String(prop);
|
|
333
394
|
if (!slotDefs[name]) return makeSlotAccessor(() => "");
|
|
334
|
-
if (ssr) return makeSlotAccessor((props) =>
|
|
395
|
+
if (ssr) return makeSlotAccessor((props) => {
|
|
396
|
+
const json = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
397
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(name)}"${json}>${slotDefs[name].toString(props)}</div>`;
|
|
398
|
+
});
|
|
335
399
|
return makeSlotAccessor((props) => {
|
|
336
|
-
const
|
|
337
|
-
|
|
400
|
+
const liveSlot = host?.querySelector(`[${SLOT_ATTR}="${escapeHtml(name)}"]`);
|
|
401
|
+
if (liveSlot) return liveSlot.outerHTML;
|
|
402
|
+
const json = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
403
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(name)}"${json}></div>`;
|
|
338
404
|
});
|
|
339
405
|
} });
|
|
340
406
|
}
|
|
@@ -361,7 +427,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
361
427
|
}
|
|
362
428
|
return state;
|
|
363
429
|
}
|
|
364
|
-
function renderToString(props) {
|
|
430
|
+
function renderToString(props, sync = false) {
|
|
365
431
|
const input = resolveInput(props);
|
|
366
432
|
const state = buildPlainState(input);
|
|
367
433
|
const slots = makeSlotsProxy(true);
|
|
@@ -382,19 +448,24 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
382
448
|
};
|
|
383
449
|
}
|
|
384
450
|
});
|
|
385
|
-
if (!results.some((r) => r.result instanceof Promise)) {
|
|
451
|
+
if (!results.some((r) => r.result instanceof Promise) || sync) {
|
|
386
452
|
const derived = {};
|
|
387
|
-
for (const r of results) derived[r.key] = {
|
|
453
|
+
for (const r of results) if (r.result instanceof Promise) derived[r.key] = {
|
|
454
|
+
loading: true,
|
|
455
|
+
value: void 0,
|
|
456
|
+
error: void 0
|
|
457
|
+
};
|
|
458
|
+
else derived[r.key] = {
|
|
388
459
|
loading: false,
|
|
389
460
|
value: r.result,
|
|
390
461
|
error: void 0
|
|
391
462
|
};
|
|
392
|
-
return fn({
|
|
463
|
+
return unwrapHtml(fn({
|
|
393
464
|
state,
|
|
394
465
|
derived,
|
|
395
466
|
input,
|
|
396
467
|
slots
|
|
397
|
-
});
|
|
468
|
+
}));
|
|
398
469
|
}
|
|
399
470
|
return Promise.all(results.map(async (r) => {
|
|
400
471
|
try {
|
|
@@ -419,54 +490,82 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
419
490
|
})).then((resolved) => {
|
|
420
491
|
const derived = {};
|
|
421
492
|
for (const r of resolved) derived[r.key] = r.envelope;
|
|
422
|
-
return fn({
|
|
493
|
+
return unwrapHtml(fn({
|
|
423
494
|
state,
|
|
424
495
|
derived,
|
|
425
496
|
input,
|
|
426
497
|
slots
|
|
427
|
-
});
|
|
498
|
+
}));
|
|
428
499
|
});
|
|
429
500
|
}
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
501
|
+
function mountIsland(host, props) {
|
|
502
|
+
if (__DEV__ && _mountedHosts) {
|
|
503
|
+
if (_mountedHosts.has(host)) {
|
|
504
|
+
warn(`mount(): this element is already mounted. Call the previous unmount() first to avoid memory leaks and duplicate event listeners.
|
|
505
|
+
Element: ${host.outerHTML.slice(0, 120)}`);
|
|
506
|
+
return () => {};
|
|
507
|
+
}
|
|
508
|
+
_mountedHosts.add(host);
|
|
509
|
+
}
|
|
510
|
+
if (props === void 0) {
|
|
511
|
+
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
512
|
+
if (rawProps) try {
|
|
513
|
+
props = JSON.parse(rawProps);
|
|
514
|
+
} catch {
|
|
515
|
+
warn("Failed to parse data-ilha-props — invalid JSON, falling back to empty props.");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
442
518
|
const input = resolveInput(props);
|
|
443
|
-
let
|
|
444
|
-
const rawState =
|
|
519
|
+
let snapshotRaw;
|
|
520
|
+
const rawState = host.getAttribute(STATE_ATTR);
|
|
445
521
|
if (rawState) try {
|
|
446
|
-
|
|
522
|
+
snapshotRaw = JSON.parse(rawState);
|
|
447
523
|
} catch {
|
|
448
|
-
|
|
524
|
+
warn("Failed to parse data-ilha-state — invalid JSON, snapshot ignored.");
|
|
525
|
+
}
|
|
526
|
+
const stateSnapshot = snapshotRaw ? Object.fromEntries(Object.entries(snapshotRaw).filter(([k]) => k !== "_derived" && k !== "_skipOnMount")) : void 0;
|
|
527
|
+
const derivedSnapshotRaw = snapshotRaw?._derived;
|
|
528
|
+
let derivedSnapshot;
|
|
529
|
+
if (derivedSnapshotRaw) {
|
|
530
|
+
derivedSnapshot = {};
|
|
531
|
+
for (const [k, v] of Object.entries(derivedSnapshotRaw)) if (v.error && !(v.error instanceof Error)) derivedSnapshot[k] = {
|
|
532
|
+
...v,
|
|
533
|
+
error: new Error(String(v.error))
|
|
534
|
+
};
|
|
535
|
+
else derivedSnapshot[k] = v;
|
|
449
536
|
}
|
|
450
|
-
const
|
|
537
|
+
const hydrated = snapshotRaw != null;
|
|
538
|
+
const shouldSkipOnMount = hydrated && snapshotRaw?.["_skipOnMount"] === true;
|
|
539
|
+
const state = buildSignalState(input, stateSnapshot);
|
|
451
540
|
const cleanups = [];
|
|
452
541
|
if (transition?.enter) {
|
|
453
|
-
const result = transition.enter(
|
|
542
|
+
const result = transition.enter(host);
|
|
454
543
|
if (result instanceof Promise) result.catch(console.error);
|
|
455
544
|
}
|
|
456
|
-
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input);
|
|
545
|
+
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input, derivedSnapshot);
|
|
457
546
|
cleanups.push(stopDerived);
|
|
547
|
+
const slotCleanups = /* @__PURE__ */ new Map();
|
|
458
548
|
const slotEls = /* @__PURE__ */ new Map();
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
549
|
+
function mountSlots() {
|
|
550
|
+
for (const [name, childIsland] of Object.entries(slotDefs)) {
|
|
551
|
+
const slotEl = host.querySelector(`[${SLOT_ATTR}="${name}"]`);
|
|
552
|
+
if (!slotEl) continue;
|
|
553
|
+
if (slotEls.get(name) === slotEl) continue;
|
|
554
|
+
slotEls.set(name, slotEl);
|
|
555
|
+
slotCleanups.get(name)?.();
|
|
556
|
+
let slotProps;
|
|
557
|
+
const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
|
|
558
|
+
if (rawProps) try {
|
|
559
|
+
slotProps = JSON.parse(rawProps);
|
|
560
|
+
} catch {
|
|
561
|
+
warn(`Failed to parse props on [${SLOT_ATTR}="${name}"] — invalid JSON ignored.`);
|
|
562
|
+
}
|
|
563
|
+
const prevSub = setActiveSub(void 0);
|
|
564
|
+
try {
|
|
565
|
+
slotCleanups.set(name, childIsland.mount(slotEl, slotProps));
|
|
566
|
+
} finally {
|
|
567
|
+
setActiveSub(prevSub);
|
|
568
|
+
}
|
|
470
569
|
}
|
|
471
570
|
}
|
|
472
571
|
const listeners = [];
|
|
@@ -474,17 +573,22 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
474
573
|
function attachListeners() {
|
|
475
574
|
for (const entry of ons) {
|
|
476
575
|
if (entry.options.once && firedOnce.has(entry)) continue;
|
|
477
|
-
|
|
576
|
+
const targets = entry.selector === "" ? [host] : Array.from(host.querySelectorAll(entry.selector));
|
|
577
|
+
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.`);
|
|
578
|
+
targets.forEach((listenerTarget) => {
|
|
478
579
|
const listener = (event) => {
|
|
479
580
|
if (entry.options.once) {
|
|
480
581
|
firedOnce.add(entry);
|
|
481
582
|
for (const l of listeners.filter((l) => l.entry === entry)) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
482
583
|
listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
|
|
483
584
|
}
|
|
585
|
+
const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
|
|
484
586
|
const result = entry.handler({
|
|
485
587
|
state,
|
|
588
|
+
derived,
|
|
486
589
|
input,
|
|
487
|
-
|
|
590
|
+
host,
|
|
591
|
+
target: eventTarget,
|
|
488
592
|
event
|
|
489
593
|
});
|
|
490
594
|
if (result instanceof Promise) result.catch(console.error);
|
|
@@ -493,9 +597,9 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
493
597
|
...entry.options,
|
|
494
598
|
once: false
|
|
495
599
|
};
|
|
496
|
-
|
|
600
|
+
listenerTarget.addEventListener(entry.event, listener, opts);
|
|
497
601
|
listeners.push({
|
|
498
|
-
target,
|
|
602
|
+
target: listenerTarget,
|
|
499
603
|
type: entry.event,
|
|
500
604
|
fn: listener,
|
|
501
605
|
options: opts,
|
|
@@ -508,54 +612,54 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
508
612
|
for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
509
613
|
listeners.length = 0;
|
|
510
614
|
}
|
|
511
|
-
const slots = makeSlotsProxy(false);
|
|
512
|
-
|
|
615
|
+
const slots = makeSlotsProxy(false, host);
|
|
616
|
+
if (!(hydrated && host.childNodes.length > 0)) host.innerHTML = unwrapHtml(fn({
|
|
513
617
|
state,
|
|
514
618
|
derived,
|
|
515
619
|
input,
|
|
516
620
|
slots
|
|
517
|
-
});
|
|
621
|
+
}));
|
|
518
622
|
attachListeners();
|
|
519
|
-
let stopBindings = applyBindings(
|
|
623
|
+
let stopBindings = applyBindings(host, binds, state);
|
|
520
624
|
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}"]`);
|
|
625
|
+
mountSlots();
|
|
626
|
+
cleanups.push(() => slotCleanups.forEach((unmount) => unmount()));
|
|
627
|
+
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
628
|
+
const prevSub = setActiveSub(void 0);
|
|
629
|
+
let userCleanup;
|
|
630
|
+
try {
|
|
631
|
+
userCleanup = entry.fn({
|
|
632
|
+
state,
|
|
633
|
+
derived,
|
|
634
|
+
input,
|
|
635
|
+
host,
|
|
636
|
+
hydrated
|
|
637
|
+
});
|
|
638
|
+
} finally {
|
|
639
|
+
setActiveSub(prevSub);
|
|
537
640
|
}
|
|
538
|
-
cleanups.push(
|
|
641
|
+
if (userCleanup) cleanups.push(userCleanup);
|
|
539
642
|
}
|
|
540
643
|
let initialized = false;
|
|
541
644
|
const stopRender = effect(() => {
|
|
542
|
-
const html = fn({
|
|
645
|
+
const html = unwrapHtml(fn({
|
|
543
646
|
state,
|
|
544
647
|
derived,
|
|
545
648
|
input,
|
|
546
649
|
slots
|
|
547
|
-
});
|
|
650
|
+
}));
|
|
548
651
|
if (!initialized) {
|
|
549
652
|
initialized = true;
|
|
550
653
|
return;
|
|
551
654
|
}
|
|
552
|
-
snapshotSlots();
|
|
553
655
|
detachListeners();
|
|
554
656
|
stopBindings();
|
|
555
|
-
|
|
556
|
-
|
|
657
|
+
const tpl = document.createElement("template");
|
|
658
|
+
tpl.innerHTML = `<div>${html}</div>`;
|
|
659
|
+
morphInner(host, tpl.content.firstElementChild);
|
|
557
660
|
attachListeners();
|
|
558
|
-
stopBindings = applyBindings(
|
|
661
|
+
stopBindings = applyBindings(host, binds, state);
|
|
662
|
+
mountSlots();
|
|
559
663
|
});
|
|
560
664
|
cleanups.push(stopRender);
|
|
561
665
|
cleanups.push(detachListeners);
|
|
@@ -569,7 +673,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
569
673
|
userCleanup = entry.fn({
|
|
570
674
|
state,
|
|
571
675
|
input,
|
|
572
|
-
|
|
676
|
+
host
|
|
573
677
|
});
|
|
574
678
|
});
|
|
575
679
|
cleanups.push(() => {
|
|
@@ -577,9 +681,13 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
577
681
|
if (userCleanup) userCleanup();
|
|
578
682
|
});
|
|
579
683
|
}
|
|
684
|
+
let tornDown = false;
|
|
580
685
|
return () => {
|
|
686
|
+
if (tornDown) return;
|
|
687
|
+
tornDown = true;
|
|
688
|
+
if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
|
|
581
689
|
if (transition?.leave) {
|
|
582
|
-
const result = transition.leave(
|
|
690
|
+
const result = transition.leave(host);
|
|
583
691
|
if (result instanceof Promise) {
|
|
584
692
|
result.then(() => cleanups.forEach((c) => c())).catch(console.error);
|
|
585
693
|
return;
|
|
@@ -591,57 +699,110 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
591
699
|
const island = function(props) {
|
|
592
700
|
return renderToString(props);
|
|
593
701
|
};
|
|
594
|
-
island.toString = (props) =>
|
|
595
|
-
island.mount = (
|
|
702
|
+
island.toString = (props) => renderToString(props, true);
|
|
703
|
+
island.mount = (host, props) => mountIsland(host, props);
|
|
704
|
+
island.hydratable = async (props, opts) => {
|
|
705
|
+
const { name, as: tag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
|
|
706
|
+
const resolvedProps = props ?? {};
|
|
707
|
+
const inner = await renderToString(resolvedProps);
|
|
708
|
+
const encodedProps = escapeHtml(JSON.stringify(resolvedProps));
|
|
709
|
+
let stateAttr = "";
|
|
710
|
+
if (snapshot !== false) {
|
|
711
|
+
const doState = snapshot === true || snapshot.state !== false;
|
|
712
|
+
const doDerived = snapshot === true || snapshot.derived !== false;
|
|
713
|
+
const doSkipOnMount = explicitSkipOnMount ?? (doState || doDerived);
|
|
714
|
+
const snapshotData = {};
|
|
715
|
+
const input = resolveInput(resolvedProps);
|
|
716
|
+
const plainState = buildPlainState(input);
|
|
717
|
+
if (doState) for (const entry of states) snapshotData[entry.key] = plainState[entry.key]();
|
|
718
|
+
if (doDerived) {
|
|
719
|
+
const derivedResults = {};
|
|
720
|
+
for (const entry of deriveds) try {
|
|
721
|
+
const result = await Promise.resolve(entry.fn({
|
|
722
|
+
state: plainState,
|
|
723
|
+
input,
|
|
724
|
+
signal: new AbortController().signal
|
|
725
|
+
}));
|
|
726
|
+
derivedResults[entry.key] = {
|
|
727
|
+
loading: false,
|
|
728
|
+
value: result,
|
|
729
|
+
error: void 0
|
|
730
|
+
};
|
|
731
|
+
} catch (err) {
|
|
732
|
+
derivedResults[entry.key] = {
|
|
733
|
+
loading: false,
|
|
734
|
+
value: void 0,
|
|
735
|
+
error: err instanceof Error ? err.message : String(err)
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
snapshotData["_derived"] = derivedResults;
|
|
739
|
+
}
|
|
740
|
+
if (doSkipOnMount) snapshotData["_skipOnMount"] = true;
|
|
741
|
+
stateAttr = ` ${STATE_ATTR}='${escapeHtml(JSON.stringify(snapshotData))}'`;
|
|
742
|
+
}
|
|
743
|
+
return `<${tag} data-ilha="${escapeHtml(name)}" ${PROPS_ATTR}='${encodedProps}'${stateAttr}>${inner}</${tag}>`;
|
|
744
|
+
};
|
|
596
745
|
return island;
|
|
597
746
|
}
|
|
598
747
|
};
|
|
599
748
|
function ilhaFrom(selector, island, props) {
|
|
600
|
-
const
|
|
601
|
-
if (!
|
|
749
|
+
const host = typeof selector === "string" ? document.querySelector(selector) : selector;
|
|
750
|
+
if (!host) {
|
|
602
751
|
console.warn(`[ilha] from(): element not found: ${selector}`);
|
|
603
752
|
return null;
|
|
604
753
|
}
|
|
605
|
-
return island.mount(
|
|
754
|
+
return island.mount(host, props);
|
|
606
755
|
}
|
|
607
756
|
function mountAll(registry, options = {}) {
|
|
608
757
|
const root = options.root ?? document.body;
|
|
609
758
|
const lazy = options.lazy ?? false;
|
|
610
|
-
const hydrate = options.hydrate ?? false;
|
|
611
759
|
const unmounts = [];
|
|
612
|
-
function activateEl(
|
|
613
|
-
const name =
|
|
760
|
+
function activateEl(host) {
|
|
761
|
+
const name = host.getAttribute("data-ilha");
|
|
614
762
|
if (!name) return;
|
|
615
763
|
const island = registry[name];
|
|
616
|
-
if (!island)
|
|
764
|
+
if (!island) {
|
|
765
|
+
warn(`mount(): no island registered under the name "${name}". Available names: [${Object.keys(registry).join(", ")}]. Check the data-ilha attribute on the element.`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
617
768
|
let props = {};
|
|
618
|
-
const rawProps =
|
|
769
|
+
const rawProps = host.getAttribute(PROPS_ATTR);
|
|
619
770
|
if (rawProps) try {
|
|
620
771
|
props = JSON.parse(rawProps);
|
|
621
772
|
} catch {
|
|
622
|
-
|
|
773
|
+
warn(`Failed to parse ${PROPS_ATTR} on [data-ilha="${name}"] — invalid JSON ignored.`);
|
|
623
774
|
}
|
|
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));
|
|
775
|
+
unmounts.push(island.mount(host, props));
|
|
630
776
|
}
|
|
631
777
|
const els = Array.from(root.querySelectorAll("[data-ilha]"));
|
|
632
778
|
if (lazy && typeof IntersectionObserver !== "undefined") {
|
|
779
|
+
let disposed = false;
|
|
633
780
|
const io = new IntersectionObserver((entries) => {
|
|
781
|
+
if (disposed) return;
|
|
634
782
|
for (const entry of entries) if (entry.isIntersecting) {
|
|
635
783
|
activateEl(entry.target);
|
|
636
784
|
io.unobserve(entry.target);
|
|
637
785
|
}
|
|
638
786
|
});
|
|
639
787
|
els.forEach((el) => io.observe(el));
|
|
640
|
-
unmounts.push(() =>
|
|
788
|
+
unmounts.push(() => {
|
|
789
|
+
disposed = true;
|
|
790
|
+
io.disconnect();
|
|
791
|
+
});
|
|
641
792
|
} else els.forEach(activateEl);
|
|
642
793
|
return { unmount: () => unmounts.forEach((u) => u()) };
|
|
643
794
|
}
|
|
644
|
-
const rootBuilder = new IlhaBuilder(
|
|
795
|
+
const rootBuilder = new IlhaBuilder({
|
|
796
|
+
schema: null,
|
|
797
|
+
states: [],
|
|
798
|
+
deriveds: [],
|
|
799
|
+
ons: [],
|
|
800
|
+
effects: [],
|
|
801
|
+
onMounts: [],
|
|
802
|
+
slots: {},
|
|
803
|
+
transition: null,
|
|
804
|
+
binds: []
|
|
805
|
+
});
|
|
645
806
|
const ilha = Object.assign(rootBuilder, {
|
|
646
807
|
html: ilhaHtml,
|
|
647
808
|
raw: ilhaRaw,
|
|
@@ -649,10 +810,19 @@ const ilha = Object.assign(rootBuilder, {
|
|
|
649
810
|
from: ilhaFrom,
|
|
650
811
|
context: ilhaContext
|
|
651
812
|
});
|
|
813
|
+
function type(coerce) {
|
|
814
|
+
return { "~standard": {
|
|
815
|
+
version: 1,
|
|
816
|
+
vendor: "ilha",
|
|
817
|
+
validate(value) {
|
|
818
|
+
return { value: coerce ? coerce(value) : value };
|
|
819
|
+
}
|
|
820
|
+
} };
|
|
821
|
+
}
|
|
652
822
|
const html = ilhaHtml;
|
|
653
823
|
const raw = ilhaRaw;
|
|
654
824
|
const mount = mountAll;
|
|
655
825
|
const from = ilhaFrom;
|
|
656
826
|
const context = ilhaContext;
|
|
657
827
|
//#endregion
|
|
658
|
-
export { context, ilha as default, from, html, mount, raw };
|
|
828
|
+
export { context, ilha as default, from, html, mount, raw, type };
|