dalila 1.9.5 → 1.9.6
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 +19 -2
- package/dist/cli/check.js +1 -1
- package/dist/runtime/bind.d.ts +12 -0
- package/dist/runtime/bind.js +312 -18
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
58
58
|
|
|
59
59
|
### Runtime
|
|
60
60
|
|
|
61
|
-
- [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, text interpolation, events
|
|
61
|
+
- [Template Binding](./docs/runtime/bind.md) — `bind()`, `mount()`, `configure()`, transitions, portal, text interpolation, events
|
|
62
62
|
- [Components](./docs/runtime/component.md) — `defineComponent`, typed props/emits/refs, slots
|
|
63
63
|
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
64
64
|
|
|
@@ -108,7 +108,7 @@ Firefox extension workflows:
|
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
dalila → signal, computed, effect, batch, ...
|
|
111
|
-
dalila/runtime → bind(), mount(), configure(), defineComponent()
|
|
111
|
+
dalila/runtime → bind(), mount(), configure(), createPortalTarget(), defineComponent()
|
|
112
112
|
dalila/context → createContext, provide, inject
|
|
113
113
|
dalila/http → createHttpClient with XSRF protection
|
|
114
114
|
```
|
|
@@ -143,6 +143,23 @@ const dispose = mount('.app', { count: signal(0) });
|
|
|
143
143
|
dispose();
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
### Transitions and Portal
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<div d-when="open" d-transition="fade">Panel</div>
|
|
150
|
+
<div d-portal="showModal ? '#modal-root' : null">Modal content</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { configure, createPortalTarget } from 'dalila/runtime';
|
|
155
|
+
|
|
156
|
+
const modalTarget = createPortalTarget('modal-root');
|
|
157
|
+
|
|
158
|
+
configure({
|
|
159
|
+
transitions: [{ name: 'fade', duration: 250 }],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
146
163
|
### Scopes
|
|
147
164
|
|
|
148
165
|
```ts
|
package/dist/cli/check.js
CHANGED
|
@@ -520,7 +520,7 @@ function extractTemplateIdentifiers(html) {
|
|
|
520
520
|
i++;
|
|
521
521
|
}
|
|
522
522
|
// --- 2. Directive scanning (supports single and double quotes) ---
|
|
523
|
-
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
523
|
+
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-portal|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
524
524
|
DIRECTIVE_RE.lastIndex = 0;
|
|
525
525
|
let match;
|
|
526
526
|
while ((match = DIRECTIVE_RE.exec(html))) {
|
package/dist/runtime/bind.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
+
import { Signal } from '../core/index.js';
|
|
9
10
|
import type { Component } from './component.js';
|
|
10
11
|
export interface BindOptions {
|
|
11
12
|
/**
|
|
@@ -30,6 +31,10 @@ export interface BindOptions {
|
|
|
30
31
|
components?: Record<string, Component> | Component[];
|
|
31
32
|
/** Error policy for component `ctx.onMount()` callbacks. Default: 'log'. */
|
|
32
33
|
onMountError?: 'log' | 'throw';
|
|
34
|
+
/**
|
|
35
|
+
* Optional runtime transition registry used by `d-transition`.
|
|
36
|
+
*/
|
|
37
|
+
transitions?: TransitionConfig[];
|
|
33
38
|
/**
|
|
34
39
|
* Internal flag — set by fromHtml for router/template rendering.
|
|
35
40
|
* Skips HMR context registration but KEEPS d-ready/d-loading lifecycle.
|
|
@@ -59,6 +64,13 @@ export interface BindHandle {
|
|
|
59
64
|
getRef(name: string): Element | null;
|
|
60
65
|
getRefs(): Readonly<Record<string, Element>>;
|
|
61
66
|
}
|
|
67
|
+
export interface TransitionConfig {
|
|
68
|
+
name: string;
|
|
69
|
+
enter?: (el: HTMLElement) => void;
|
|
70
|
+
leave?: (el: HTMLElement) => void;
|
|
71
|
+
duration?: number;
|
|
72
|
+
}
|
|
73
|
+
export declare function createPortalTarget(id: string): Signal<Element | null>;
|
|
62
74
|
/**
|
|
63
75
|
* Set global defaults for all `bind()` / `mount()` calls.
|
|
64
76
|
*
|
package/dist/runtime/bind.js
CHANGED
|
@@ -97,6 +97,7 @@ function warn(message) {
|
|
|
97
97
|
console.warn(`[Dalila] ${message}`);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
const portalSyncByElement = new WeakMap();
|
|
100
101
|
function describeBindRoot(root) {
|
|
101
102
|
const explicit = root.getAttribute('data-component') ||
|
|
102
103
|
root.getAttribute('data-devtools-label') ||
|
|
@@ -1135,13 +1136,231 @@ function bindEmit(root, ctx, cleanups) {
|
|
|
1135
1136
|
}
|
|
1136
1137
|
}
|
|
1137
1138
|
}
|
|
1139
|
+
function createTransitionRegistry(transitions) {
|
|
1140
|
+
const registry = new Map();
|
|
1141
|
+
if (!transitions)
|
|
1142
|
+
return registry;
|
|
1143
|
+
for (const cfg of transitions) {
|
|
1144
|
+
if (!cfg || typeof cfg !== 'object')
|
|
1145
|
+
continue;
|
|
1146
|
+
const name = typeof cfg.name === 'string' ? cfg.name.trim() : '';
|
|
1147
|
+
if (!name) {
|
|
1148
|
+
warn('configure({ transitions }): each transition must have a non-empty "name"');
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
registry.set(name, cfg);
|
|
1152
|
+
}
|
|
1153
|
+
return registry;
|
|
1154
|
+
}
|
|
1155
|
+
function readTransitionNames(el) {
|
|
1156
|
+
const raw = el.getAttribute('d-transition');
|
|
1157
|
+
if (!raw)
|
|
1158
|
+
return [];
|
|
1159
|
+
return raw
|
|
1160
|
+
.split(/\s+/)
|
|
1161
|
+
.map(v => v.trim())
|
|
1162
|
+
.filter(Boolean);
|
|
1163
|
+
}
|
|
1164
|
+
function parseCssTimeToMs(value) {
|
|
1165
|
+
const token = value.trim();
|
|
1166
|
+
if (!token)
|
|
1167
|
+
return 0;
|
|
1168
|
+
if (token.endsWith('ms')) {
|
|
1169
|
+
const ms = Number(token.slice(0, -2));
|
|
1170
|
+
return Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
1171
|
+
}
|
|
1172
|
+
if (token.endsWith('s')) {
|
|
1173
|
+
const seconds = Number(token.slice(0, -1));
|
|
1174
|
+
return Number.isFinite(seconds) ? Math.max(0, seconds * 1000) : 0;
|
|
1175
|
+
}
|
|
1176
|
+
const fallback = Number(token);
|
|
1177
|
+
return Number.isFinite(fallback) ? Math.max(0, fallback) : 0;
|
|
1178
|
+
}
|
|
1179
|
+
function getTransitionDurationMs(el, names, registry) {
|
|
1180
|
+
let durationFromRegistry = 0;
|
|
1181
|
+
for (const name of names) {
|
|
1182
|
+
const cfg = registry.get(name);
|
|
1183
|
+
if (!cfg)
|
|
1184
|
+
continue;
|
|
1185
|
+
if (typeof cfg.duration === 'number' && Number.isFinite(cfg.duration)) {
|
|
1186
|
+
durationFromRegistry = Math.max(durationFromRegistry, Math.max(0, cfg.duration));
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
let durationFromCss = 0;
|
|
1190
|
+
if (typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
|
|
1191
|
+
const style = window.getComputedStyle(el);
|
|
1192
|
+
const durations = style.transitionDuration.split(',');
|
|
1193
|
+
const delays = style.transitionDelay.split(',');
|
|
1194
|
+
const total = Math.max(durations.length, delays.length);
|
|
1195
|
+
for (let i = 0; i < total; i++) {
|
|
1196
|
+
const duration = parseCssTimeToMs(durations[Math.min(i, durations.length - 1)] ?? '0ms');
|
|
1197
|
+
const delay = parseCssTimeToMs(delays[Math.min(i, delays.length - 1)] ?? '0ms');
|
|
1198
|
+
durationFromCss = Math.max(durationFromCss, duration + delay);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return Math.max(durationFromRegistry, durationFromCss);
|
|
1202
|
+
}
|
|
1203
|
+
function runTransitionHook(phase, el, names, registry) {
|
|
1204
|
+
for (const name of names) {
|
|
1205
|
+
const cfg = registry.get(name);
|
|
1206
|
+
const hook = phase === 'enter' ? cfg?.enter : cfg?.leave;
|
|
1207
|
+
if (typeof hook !== 'function')
|
|
1208
|
+
continue;
|
|
1209
|
+
try {
|
|
1210
|
+
hook(el);
|
|
1211
|
+
}
|
|
1212
|
+
catch (err) {
|
|
1213
|
+
warn(`d-transition (${name}): ${phase} hook failed (${err.message || String(err)})`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function syncPortalElement(el) {
|
|
1218
|
+
const sync = portalSyncByElement.get(el);
|
|
1219
|
+
sync?.();
|
|
1220
|
+
}
|
|
1221
|
+
function createTransitionController(el, registry, cleanups) {
|
|
1222
|
+
const names = readTransitionNames(el);
|
|
1223
|
+
const hasTransition = names.length > 0;
|
|
1224
|
+
let token = 0;
|
|
1225
|
+
let timeoutId = null;
|
|
1226
|
+
const cancelPending = () => {
|
|
1227
|
+
token++;
|
|
1228
|
+
if (timeoutId != null) {
|
|
1229
|
+
clearTimeout(timeoutId);
|
|
1230
|
+
timeoutId = null;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
cleanups.push(cancelPending);
|
|
1234
|
+
const enter = () => {
|
|
1235
|
+
cancelPending();
|
|
1236
|
+
if (!hasTransition)
|
|
1237
|
+
return;
|
|
1238
|
+
el.removeAttribute('data-leave');
|
|
1239
|
+
el.setAttribute('data-enter', '');
|
|
1240
|
+
runTransitionHook('enter', el, names, registry);
|
|
1241
|
+
};
|
|
1242
|
+
const leave = (onDone) => {
|
|
1243
|
+
cancelPending();
|
|
1244
|
+
if (!hasTransition) {
|
|
1245
|
+
onDone();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const current = ++token;
|
|
1249
|
+
el.removeAttribute('data-enter');
|
|
1250
|
+
el.setAttribute('data-leave', '');
|
|
1251
|
+
runTransitionHook('leave', el, names, registry);
|
|
1252
|
+
const durationMs = getTransitionDurationMs(el, names, registry);
|
|
1253
|
+
if (durationMs <= 0) {
|
|
1254
|
+
if (current === token)
|
|
1255
|
+
onDone();
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
timeoutId = setTimeout(() => {
|
|
1259
|
+
timeoutId = null;
|
|
1260
|
+
if (current !== token)
|
|
1261
|
+
return;
|
|
1262
|
+
onDone();
|
|
1263
|
+
}, durationMs);
|
|
1264
|
+
};
|
|
1265
|
+
return { hasTransition, enter, leave };
|
|
1266
|
+
}
|
|
1267
|
+
function bindPortal(root, ctx, cleanups) {
|
|
1268
|
+
const elements = qsaIncludingRoot(root, '[d-portal]');
|
|
1269
|
+
for (const el of elements) {
|
|
1270
|
+
const rawExpression = el.getAttribute('d-portal')?.trim();
|
|
1271
|
+
if (!rawExpression)
|
|
1272
|
+
continue;
|
|
1273
|
+
let expressionAst = null;
|
|
1274
|
+
let fallbackSelector = null;
|
|
1275
|
+
try {
|
|
1276
|
+
expressionAst = parseExpression(rawExpression);
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
// Allow selector shorthand: d-portal="#modal-root"
|
|
1280
|
+
fallbackSelector = rawExpression;
|
|
1281
|
+
}
|
|
1282
|
+
const htmlEl = el;
|
|
1283
|
+
const anchor = document.createComment('d-portal');
|
|
1284
|
+
htmlEl.parentNode?.insertBefore(anchor, htmlEl);
|
|
1285
|
+
const coerceTarget = (value) => {
|
|
1286
|
+
const resolved = resolve(value);
|
|
1287
|
+
if (resolved == null || resolved === false)
|
|
1288
|
+
return null;
|
|
1289
|
+
if (typeof resolved === 'string') {
|
|
1290
|
+
const selector = resolved.trim();
|
|
1291
|
+
if (!selector)
|
|
1292
|
+
return null;
|
|
1293
|
+
if (typeof document === 'undefined')
|
|
1294
|
+
return null;
|
|
1295
|
+
const target = document.querySelector(selector);
|
|
1296
|
+
if (!target) {
|
|
1297
|
+
warn(`d-portal: target "${selector}" not found`);
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
return target;
|
|
1301
|
+
}
|
|
1302
|
+
if (typeof Element !== 'undefined' && resolved instanceof Element) {
|
|
1303
|
+
return resolved;
|
|
1304
|
+
}
|
|
1305
|
+
warn('d-portal: expression must resolve to selector string, Element, or null');
|
|
1306
|
+
return null;
|
|
1307
|
+
};
|
|
1308
|
+
const restoreToAnchor = () => {
|
|
1309
|
+
const hostParent = anchor.parentNode;
|
|
1310
|
+
if (!hostParent)
|
|
1311
|
+
return;
|
|
1312
|
+
if (htmlEl.parentNode === hostParent)
|
|
1313
|
+
return;
|
|
1314
|
+
const next = anchor.nextSibling;
|
|
1315
|
+
if (next)
|
|
1316
|
+
hostParent.insertBefore(htmlEl, next);
|
|
1317
|
+
else
|
|
1318
|
+
hostParent.appendChild(htmlEl);
|
|
1319
|
+
};
|
|
1320
|
+
const syncPortal = () => {
|
|
1321
|
+
let target = null;
|
|
1322
|
+
if (expressionAst) {
|
|
1323
|
+
const result = evalExpressionAst(expressionAst, ctx);
|
|
1324
|
+
if (!result.ok) {
|
|
1325
|
+
if (result.reason === 'missing_identifier') {
|
|
1326
|
+
warn(`d-portal: ${result.message}`);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
warn(`d-portal: invalid expression "${rawExpression}"`);
|
|
1330
|
+
}
|
|
1331
|
+
target = null;
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
target = coerceTarget(result.value);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
target = coerceTarget(fallbackSelector);
|
|
1339
|
+
}
|
|
1340
|
+
if (!target) {
|
|
1341
|
+
restoreToAnchor();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (htmlEl.parentNode !== target) {
|
|
1345
|
+
target.appendChild(htmlEl);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
portalSyncByElement.set(htmlEl, syncPortal);
|
|
1349
|
+
bindEffect(htmlEl, syncPortal);
|
|
1350
|
+
cleanups.push(() => {
|
|
1351
|
+
portalSyncByElement.delete(htmlEl);
|
|
1352
|
+
restoreToAnchor();
|
|
1353
|
+
anchor.remove();
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1138
1357
|
// ============================================================================
|
|
1139
1358
|
// d-when Directive
|
|
1140
1359
|
// ============================================================================
|
|
1141
1360
|
/**
|
|
1142
1361
|
* Bind all [d-when] directives within root
|
|
1143
1362
|
*/
|
|
1144
|
-
function bindWhen(root, ctx, cleanups) {
|
|
1363
|
+
function bindWhen(root, ctx, cleanups, transitionRegistry) {
|
|
1145
1364
|
const elements = qsaIncludingRoot(root, '[when], [d-when]');
|
|
1146
1365
|
for (const el of elements) {
|
|
1147
1366
|
const attrName = el.hasAttribute('when') ? 'when' : 'd-when';
|
|
@@ -1154,13 +1373,32 @@ function bindWhen(root, ctx, cleanups) {
|
|
|
1154
1373
|
continue;
|
|
1155
1374
|
}
|
|
1156
1375
|
const htmlEl = el;
|
|
1376
|
+
const transitions = createTransitionController(htmlEl, transitionRegistry, cleanups);
|
|
1157
1377
|
// Apply initial state synchronously to avoid FOUC (flash of unstyled content)
|
|
1158
1378
|
const initialValue = !!resolve(binding);
|
|
1159
|
-
|
|
1379
|
+
if (initialValue) {
|
|
1380
|
+
htmlEl.style.display = '';
|
|
1381
|
+
if (transitions.hasTransition) {
|
|
1382
|
+
htmlEl.removeAttribute('data-leave');
|
|
1383
|
+
htmlEl.setAttribute('data-enter', '');
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
htmlEl.style.display = 'none';
|
|
1388
|
+
htmlEl.removeAttribute('data-enter');
|
|
1389
|
+
htmlEl.removeAttribute('data-leave');
|
|
1390
|
+
}
|
|
1160
1391
|
// Then create reactive effect to keep it updated
|
|
1161
1392
|
bindEffect(htmlEl, () => {
|
|
1162
1393
|
const value = !!resolve(binding);
|
|
1163
|
-
|
|
1394
|
+
if (value) {
|
|
1395
|
+
htmlEl.style.display = '';
|
|
1396
|
+
transitions.enter();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
transitions.leave(() => {
|
|
1400
|
+
htmlEl.style.display = 'none';
|
|
1401
|
+
});
|
|
1164
1402
|
});
|
|
1165
1403
|
}
|
|
1166
1404
|
}
|
|
@@ -1851,7 +2089,7 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1851
2089
|
* Unlike [d-when] which toggles display, d-if adds/removes the element from
|
|
1852
2090
|
* the DOM entirely. A comment node is left as placeholder for insertion position.
|
|
1853
2091
|
*/
|
|
1854
|
-
function bindIf(root, ctx, cleanups) {
|
|
2092
|
+
function bindIf(root, ctx, cleanups, transitionRegistry) {
|
|
1855
2093
|
const elements = qsaIncludingRoot(root, '[d-if]');
|
|
1856
2094
|
const processedElse = new Set();
|
|
1857
2095
|
for (const el of elements) {
|
|
@@ -1869,23 +2107,36 @@ function bindIf(root, ctx, cleanups) {
|
|
|
1869
2107
|
el.parentNode?.replaceChild(comment, el);
|
|
1870
2108
|
el.removeAttribute('d-if');
|
|
1871
2109
|
const htmlEl = el;
|
|
2110
|
+
const transitions = createTransitionController(htmlEl, transitionRegistry, cleanups);
|
|
1872
2111
|
// Handle d-else branch
|
|
1873
2112
|
let elseHtmlEl = null;
|
|
1874
2113
|
let elseComment = null;
|
|
2114
|
+
let elseTransitions = null;
|
|
1875
2115
|
if (elseEl) {
|
|
1876
2116
|
processedElse.add(elseEl);
|
|
1877
2117
|
elseComment = document.createComment('d-else');
|
|
1878
2118
|
elseEl.parentNode?.replaceChild(elseComment, elseEl);
|
|
1879
2119
|
elseEl.removeAttribute('d-else');
|
|
1880
2120
|
elseHtmlEl = elseEl;
|
|
2121
|
+
elseTransitions = createTransitionController(elseHtmlEl, transitionRegistry, cleanups);
|
|
1881
2122
|
}
|
|
1882
2123
|
// Apply initial state synchronously to avoid FOUC
|
|
1883
2124
|
const initialValue = !!resolve(binding);
|
|
1884
2125
|
if (initialValue) {
|
|
1885
2126
|
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
2127
|
+
syncPortalElement(htmlEl);
|
|
2128
|
+
if (transitions.hasTransition) {
|
|
2129
|
+
htmlEl.removeAttribute('data-leave');
|
|
2130
|
+
htmlEl.setAttribute('data-enter', '');
|
|
2131
|
+
}
|
|
1886
2132
|
}
|
|
1887
2133
|
else if (elseHtmlEl && elseComment) {
|
|
1888
2134
|
elseComment.parentNode?.insertBefore(elseHtmlEl, elseComment);
|
|
2135
|
+
syncPortalElement(elseHtmlEl);
|
|
2136
|
+
if (elseTransitions?.hasTransition) {
|
|
2137
|
+
elseHtmlEl.removeAttribute('data-leave');
|
|
2138
|
+
elseHtmlEl.setAttribute('data-enter', '');
|
|
2139
|
+
}
|
|
1889
2140
|
}
|
|
1890
2141
|
// Then create reactive effect to keep it updated
|
|
1891
2142
|
if (elseHtmlEl && elseComment) {
|
|
@@ -1896,18 +2147,26 @@ function bindIf(root, ctx, cleanups) {
|
|
|
1896
2147
|
if (value) {
|
|
1897
2148
|
if (!htmlEl.parentNode) {
|
|
1898
2149
|
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
2150
|
+
syncPortalElement(htmlEl);
|
|
1899
2151
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2152
|
+
transitions.enter();
|
|
2153
|
+
elseTransitions?.leave(() => {
|
|
2154
|
+
if (capturedElseEl.parentNode) {
|
|
2155
|
+
capturedElseEl.parentNode.removeChild(capturedElseEl);
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
1903
2158
|
}
|
|
1904
2159
|
else {
|
|
1905
|
-
|
|
1906
|
-
htmlEl.parentNode
|
|
1907
|
-
|
|
2160
|
+
transitions.leave(() => {
|
|
2161
|
+
if (htmlEl.parentNode) {
|
|
2162
|
+
htmlEl.parentNode.removeChild(htmlEl);
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
1908
2165
|
if (!capturedElseEl.parentNode) {
|
|
1909
2166
|
capturedElseComment.parentNode?.insertBefore(capturedElseEl, capturedElseComment);
|
|
2167
|
+
syncPortalElement(capturedElseEl);
|
|
1910
2168
|
}
|
|
2169
|
+
elseTransitions?.enter();
|
|
1911
2170
|
}
|
|
1912
2171
|
});
|
|
1913
2172
|
}
|
|
@@ -1917,12 +2176,16 @@ function bindIf(root, ctx, cleanups) {
|
|
|
1917
2176
|
if (value) {
|
|
1918
2177
|
if (!htmlEl.parentNode) {
|
|
1919
2178
|
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
2179
|
+
syncPortalElement(htmlEl);
|
|
1920
2180
|
}
|
|
2181
|
+
transitions.enter();
|
|
1921
2182
|
}
|
|
1922
2183
|
else {
|
|
1923
|
-
|
|
1924
|
-
htmlEl.parentNode
|
|
1925
|
-
|
|
2184
|
+
transitions.leave(() => {
|
|
2185
|
+
if (htmlEl.parentNode) {
|
|
2186
|
+
htmlEl.parentNode.removeChild(htmlEl);
|
|
2187
|
+
}
|
|
2188
|
+
});
|
|
1926
2189
|
}
|
|
1927
2190
|
});
|
|
1928
2191
|
}
|
|
@@ -3064,6 +3327,20 @@ function bindComponents(root, ctx, events, cleanups, onMountError) {
|
|
|
3064
3327
|
// Global Configuration
|
|
3065
3328
|
// ============================================================================
|
|
3066
3329
|
let globalConfig = {};
|
|
3330
|
+
export function createPortalTarget(id) {
|
|
3331
|
+
const targetSignal = signal(null);
|
|
3332
|
+
if (typeof document === 'undefined') {
|
|
3333
|
+
return targetSignal;
|
|
3334
|
+
}
|
|
3335
|
+
let target = document.getElementById(id);
|
|
3336
|
+
if (!target) {
|
|
3337
|
+
target = document.createElement('div');
|
|
3338
|
+
target.id = id;
|
|
3339
|
+
document.body.appendChild(target);
|
|
3340
|
+
}
|
|
3341
|
+
targetSignal.set(target);
|
|
3342
|
+
return targetSignal;
|
|
3343
|
+
}
|
|
3067
3344
|
/**
|
|
3068
3345
|
* Set global defaults for all `bind()` / `mount()` calls.
|
|
3069
3346
|
*
|
|
@@ -3116,8 +3393,8 @@ export function configure(config) {
|
|
|
3116
3393
|
export function bind(root, ctx, options = {}) {
|
|
3117
3394
|
// ── Merge global config with per-call options ──
|
|
3118
3395
|
if (Object.keys(globalConfig).length > 0) {
|
|
3119
|
-
const { components: globalComponents, ...globalRest } = globalConfig;
|
|
3120
|
-
const { components: localComponents, ...localRest } = options;
|
|
3396
|
+
const { components: globalComponents, transitions: globalTransitions, ...globalRest } = globalConfig;
|
|
3397
|
+
const { components: localComponents, transitions: localTransitions, ...localRest } = options;
|
|
3121
3398
|
const mergedOpts = { ...globalRest, ...localRest };
|
|
3122
3399
|
// Combine component registries: local takes precedence over global
|
|
3123
3400
|
if (globalComponents || localComponents) {
|
|
@@ -3142,6 +3419,20 @@ export function bind(root, ctx, options = {}) {
|
|
|
3142
3419
|
mergeComponents(localComponents); // local wins
|
|
3143
3420
|
mergedOpts.components = combined;
|
|
3144
3421
|
}
|
|
3422
|
+
if (globalTransitions || localTransitions) {
|
|
3423
|
+
const byName = new Map();
|
|
3424
|
+
for (const item of globalTransitions ?? []) {
|
|
3425
|
+
if (!item || typeof item.name !== 'string')
|
|
3426
|
+
continue;
|
|
3427
|
+
byName.set(item.name, item);
|
|
3428
|
+
}
|
|
3429
|
+
for (const item of localTransitions ?? []) {
|
|
3430
|
+
if (!item || typeof item.name !== 'string')
|
|
3431
|
+
continue;
|
|
3432
|
+
byName.set(item.name, item);
|
|
3433
|
+
}
|
|
3434
|
+
mergedOpts.transitions = Array.from(byName.values());
|
|
3435
|
+
}
|
|
3145
3436
|
options = mergedOpts;
|
|
3146
3437
|
}
|
|
3147
3438
|
// ── Resolve string selector ──
|
|
@@ -3186,6 +3477,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
3186
3477
|
const onMountError = options.onMountError ?? 'log';
|
|
3187
3478
|
const rawTextSelectors = options.rawTextSelectors ?? DEFAULT_RAW_TEXT_SELECTORS;
|
|
3188
3479
|
const templatePlanCacheConfig = resolveTemplatePlanCacheConfig(options);
|
|
3480
|
+
const transitionRegistry = createTransitionRegistry(options.transitions);
|
|
3189
3481
|
const benchSession = createBindBenchSession();
|
|
3190
3482
|
const htmlRoot = root;
|
|
3191
3483
|
// HMR support: Register binding context globally in dev mode.
|
|
@@ -3229,14 +3521,16 @@ export function bind(root, ctx, options = {}) {
|
|
|
3229
3521
|
// 13. d-emit-* bindings (component template → parent)
|
|
3230
3522
|
bindEmit(root, ctx, cleanups);
|
|
3231
3523
|
// 14. d-when directive
|
|
3232
|
-
bindWhen(root, ctx, cleanups);
|
|
3524
|
+
bindWhen(root, ctx, cleanups, transitionRegistry);
|
|
3233
3525
|
// 15. d-match directive
|
|
3234
3526
|
bindMatch(root, ctx, cleanups);
|
|
3235
3527
|
// 16. Form error displays — BEFORE d-if to bind errors in conditionally rendered sections
|
|
3236
3528
|
bindError(root, ctx, cleanups);
|
|
3237
3529
|
bindFormError(root, ctx, cleanups);
|
|
3238
|
-
// 17. d-
|
|
3239
|
-
|
|
3530
|
+
// 17. d-portal — move already-bound elements to external targets
|
|
3531
|
+
bindPortal(root, ctx, cleanups);
|
|
3532
|
+
// 18. d-if — must run last: elements are fully bound before conditional removal
|
|
3533
|
+
bindIf(root, ctx, cleanups, transitionRegistry);
|
|
3240
3534
|
});
|
|
3241
3535
|
// Bindings complete: remove loading state and mark as ready.
|
|
3242
3536
|
// Only the top-level bind owns this lifecycle — d-each clones skip it.
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
-
export { bind, autoBind, mount, configure } from './bind.js';
|
|
10
|
-
export type { BindOptions, BindContext, BindData, DisposeFunction, BindHandle } from './bind.js';
|
|
9
|
+
export { bind, autoBind, mount, configure, createPortalTarget } from './bind.js';
|
|
10
|
+
export type { BindOptions, BindContext, BindData, DisposeFunction, BindHandle, TransitionConfig } from './bind.js';
|
|
11
11
|
export { fromHtml } from './fromHtml.js';
|
|
12
12
|
export type { FromHtmlOptions } from './fromHtml.js';
|
|
13
13
|
export { defineComponent } from './component.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
-
export { bind, autoBind, mount, configure } from './bind.js';
|
|
9
|
+
export { bind, autoBind, mount, configure, createPortalTarget } from './bind.js';
|
|
10
10
|
export { fromHtml } from './fromHtml.js';
|
|
11
11
|
export { defineComponent } from './component.js';
|