pulse-js-framework 1.7.3 → 1.7.5
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/cli/analyze.js +127 -46
- package/cli/build.js +148 -34
- package/cli/dev.js +20 -5
- package/cli/format.js +64 -8
- package/cli/lint.js +112 -27
- package/cli/utils/cli-ui.js +452 -0
- package/compiler/parser.js +19 -2
- package/core/errors.js +281 -6
- package/package.json +7 -2
- package/runtime/async.js +282 -14
- package/runtime/dom-adapter.js +920 -0
- package/runtime/dom.js +331 -162
- package/runtime/logger.js +144 -69
- package/runtime/logger.prod.js +43 -18
- package/runtime/pulse.js +202 -80
- package/runtime/router.js +27 -39
- package/runtime/store.js +10 -7
- package/runtime/utils.js +279 -18
package/runtime/dom.js
CHANGED
|
@@ -2,15 +2,42 @@
|
|
|
2
2
|
* Pulse DOM - Declarative DOM manipulation
|
|
3
3
|
*
|
|
4
4
|
* Creates DOM elements using CSS selector-like syntax
|
|
5
|
-
* and provides reactive bindings
|
|
5
|
+
* and provides reactive bindings.
|
|
6
|
+
*
|
|
7
|
+
* DOM Abstraction:
|
|
8
|
+
* This module uses a DOM adapter for all DOM operations, enabling:
|
|
9
|
+
* - Server-Side Rendering (SSR) with virtual DOM implementations
|
|
10
|
+
* - Simplified testing without browser environment
|
|
11
|
+
* - Platform-specific optimizations
|
|
12
|
+
*
|
|
13
|
+
* @see ./dom-adapter.js for adapter configuration
|
|
6
14
|
*/
|
|
7
15
|
|
|
8
16
|
import { effect, pulse, batch, onCleanup } from './pulse.js';
|
|
9
17
|
import { loggers } from './logger.js';
|
|
10
18
|
import { LRUCache } from './lru-cache.js';
|
|
19
|
+
import { safeSetAttribute, sanitizeUrl, safeSetStyle } from './utils.js';
|
|
20
|
+
import { getAdapter } from './dom-adapter.js';
|
|
21
|
+
import { Errors } from '../core/errors.js';
|
|
11
22
|
|
|
12
23
|
const log = loggers.dom;
|
|
13
24
|
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// CONFIGURATION
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} DomConfig
|
|
31
|
+
* @property {number} [selectorCacheCapacity=500] - Max selectors to cache (0 = disabled)
|
|
32
|
+
* @property {boolean} [trackCacheMetrics=true] - Enable cache hit/miss tracking
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {DomConfig} */
|
|
36
|
+
const config = {
|
|
37
|
+
selectorCacheCapacity: 500,
|
|
38
|
+
trackCacheMetrics: true
|
|
39
|
+
};
|
|
40
|
+
|
|
14
41
|
// =============================================================================
|
|
15
42
|
// SELECTOR CACHE
|
|
16
43
|
// =============================================================================
|
|
@@ -21,13 +48,84 @@ const log = loggers.dom;
|
|
|
21
48
|
// - Without eviction, memory grows unbounded in long-running apps
|
|
22
49
|
// - LRU keeps frequently-used selectors hot while evicting rare ones
|
|
23
50
|
//
|
|
24
|
-
//
|
|
51
|
+
// Default capacity: 500 selectors (configurable via configureDom())
|
|
25
52
|
// - Most apps use 50-200 unique selectors
|
|
26
53
|
// - 500 provides headroom for dynamic selectors without excessive memory
|
|
27
54
|
//
|
|
28
55
|
// Cache hit returns a shallow copy to prevent mutation of cached config
|
|
29
56
|
// Metrics tracking enabled for performance monitoring
|
|
30
|
-
|
|
57
|
+
let selectorCache = new LRUCache(config.selectorCacheCapacity, { trackMetrics: config.trackCacheMetrics });
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configure DOM module settings.
|
|
61
|
+
*
|
|
62
|
+
* Call this before using any DOM functions if you need non-default settings.
|
|
63
|
+
* Changing configuration clears the selector cache.
|
|
64
|
+
*
|
|
65
|
+
* @param {Partial<DomConfig>} options - Configuration options
|
|
66
|
+
* @returns {DomConfig} Current configuration after applying changes
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Increase cache for large apps
|
|
70
|
+
* configureDom({ selectorCacheCapacity: 1000 });
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Disable caching for debugging
|
|
74
|
+
* configureDom({ selectorCacheCapacity: 0 });
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Disable metrics tracking in production
|
|
78
|
+
* configureDom({ trackCacheMetrics: false });
|
|
79
|
+
*/
|
|
80
|
+
export function configureDom(options = {}) {
|
|
81
|
+
let cacheChanged = false;
|
|
82
|
+
|
|
83
|
+
if (options.selectorCacheCapacity !== undefined) {
|
|
84
|
+
config.selectorCacheCapacity = options.selectorCacheCapacity;
|
|
85
|
+
cacheChanged = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.trackCacheMetrics !== undefined) {
|
|
89
|
+
config.trackCacheMetrics = options.trackCacheMetrics;
|
|
90
|
+
cacheChanged = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Recreate cache if settings changed
|
|
94
|
+
if (cacheChanged) {
|
|
95
|
+
if (config.selectorCacheCapacity > 0) {
|
|
96
|
+
selectorCache = new LRUCache(config.selectorCacheCapacity, {
|
|
97
|
+
trackMetrics: config.trackCacheMetrics
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
// Disable caching with a null-like cache
|
|
101
|
+
selectorCache = {
|
|
102
|
+
get: () => undefined,
|
|
103
|
+
set: () => {},
|
|
104
|
+
clear: () => {},
|
|
105
|
+
getMetrics: () => ({ hits: 0, misses: 0, evictions: 0, hitRate: 0, size: 0, capacity: 0 }),
|
|
106
|
+
resetMetrics: () => {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { ...config };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get current DOM configuration.
|
|
116
|
+
* @returns {DomConfig} Current configuration (copy)
|
|
117
|
+
*/
|
|
118
|
+
export function getDomConfig() {
|
|
119
|
+
return { ...config };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clear the selector cache.
|
|
124
|
+
* Useful for memory management or after significant DOM structure changes.
|
|
125
|
+
*/
|
|
126
|
+
export function clearSelectorCache() {
|
|
127
|
+
selectorCache.clear();
|
|
128
|
+
}
|
|
31
129
|
|
|
32
130
|
/**
|
|
33
131
|
* Get selector cache performance metrics.
|
|
@@ -51,23 +149,6 @@ export function resetCacheMetrics() {
|
|
|
51
149
|
selectorCache.resetMetrics();
|
|
52
150
|
}
|
|
53
151
|
|
|
54
|
-
/**
|
|
55
|
-
* Safely insert a node before a reference node
|
|
56
|
-
* Returns false if the parent is detached (no parentNode)
|
|
57
|
-
* @private
|
|
58
|
-
* @param {Node} newNode - Node to insert
|
|
59
|
-
* @param {Node} refNode - Reference node (insert before this)
|
|
60
|
-
* @returns {boolean} True if insertion succeeded
|
|
61
|
-
*/
|
|
62
|
-
function safeInsertBefore(newNode, refNode) {
|
|
63
|
-
if (!refNode.parentNode) {
|
|
64
|
-
log.warn('Cannot insert node: reference node has no parent (may be detached)');
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
refNode.parentNode.insertBefore(newNode, refNode);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
152
|
/**
|
|
72
153
|
* Resolve a selector string or element to a DOM element
|
|
73
154
|
* @private
|
|
@@ -77,7 +158,8 @@ function safeInsertBefore(newNode, refNode) {
|
|
|
77
158
|
*/
|
|
78
159
|
function resolveSelector(target, context = 'target') {
|
|
79
160
|
if (typeof target === 'string') {
|
|
80
|
-
const
|
|
161
|
+
const dom = getAdapter();
|
|
162
|
+
const element = dom.querySelector(target);
|
|
81
163
|
return { element, selector: target };
|
|
82
164
|
}
|
|
83
165
|
return { element: target, selector: '(element)' };
|
|
@@ -96,7 +178,8 @@ export function onMount(fn) {
|
|
|
96
178
|
currentMountContext.mountCallbacks.push(fn);
|
|
97
179
|
} else {
|
|
98
180
|
// Defer to next microtask if no context
|
|
99
|
-
|
|
181
|
+
const dom = getAdapter();
|
|
182
|
+
dom.queueMicrotask(fn);
|
|
100
183
|
}
|
|
101
184
|
}
|
|
102
185
|
|
|
@@ -218,19 +301,21 @@ export function parseSelector(selector) {
|
|
|
218
301
|
* Create a DOM element from a selector
|
|
219
302
|
*/
|
|
220
303
|
export function el(selector, ...children) {
|
|
304
|
+
const dom = getAdapter();
|
|
221
305
|
const config = parseSelector(selector);
|
|
222
|
-
const element =
|
|
306
|
+
const element = dom.createElement(config.tag);
|
|
223
307
|
|
|
224
308
|
if (config.id) {
|
|
225
|
-
element
|
|
309
|
+
dom.setProperty(element, 'id', config.id);
|
|
226
310
|
}
|
|
227
311
|
|
|
228
312
|
if (config.classes.length > 0) {
|
|
229
|
-
element
|
|
313
|
+
dom.setProperty(element, 'className', config.classes.join(' '));
|
|
230
314
|
}
|
|
231
315
|
|
|
232
316
|
for (const [key, value] of Object.entries(config.attrs)) {
|
|
233
|
-
|
|
317
|
+
// Use safeSetAttribute to prevent XSS via event handlers and javascript: URLs
|
|
318
|
+
safeSetAttribute(element, key, value, {}, dom);
|
|
234
319
|
}
|
|
235
320
|
|
|
236
321
|
// Process children
|
|
@@ -245,20 +330,22 @@ export function el(selector, ...children) {
|
|
|
245
330
|
* Append a child to an element, handling various types
|
|
246
331
|
*/
|
|
247
332
|
function appendChild(parent, child) {
|
|
333
|
+
const dom = getAdapter();
|
|
334
|
+
|
|
248
335
|
if (child == null || child === false) return;
|
|
249
336
|
|
|
250
337
|
if (typeof child === 'string' || typeof child === 'number') {
|
|
251
|
-
|
|
252
|
-
} else if (child
|
|
253
|
-
|
|
338
|
+
dom.appendChild(parent, dom.createTextNode(String(child)));
|
|
339
|
+
} else if (dom.isNode(child)) {
|
|
340
|
+
dom.appendChild(parent, child);
|
|
254
341
|
} else if (Array.isArray(child)) {
|
|
255
342
|
for (const c of child) {
|
|
256
343
|
appendChild(parent, c);
|
|
257
344
|
}
|
|
258
345
|
} else if (typeof child === 'function') {
|
|
259
346
|
// Reactive child - create a placeholder and update it
|
|
260
|
-
const placeholder =
|
|
261
|
-
|
|
347
|
+
const placeholder = dom.createComment('pulse');
|
|
348
|
+
dom.appendChild(parent, placeholder);
|
|
262
349
|
let currentNodes = [];
|
|
263
350
|
|
|
264
351
|
effect(() => {
|
|
@@ -266,34 +353,35 @@ function appendChild(parent, child) {
|
|
|
266
353
|
|
|
267
354
|
// Remove old nodes
|
|
268
355
|
for (const node of currentNodes) {
|
|
269
|
-
|
|
356
|
+
dom.removeNode(node);
|
|
270
357
|
}
|
|
271
358
|
currentNodes = [];
|
|
272
359
|
|
|
273
360
|
// Add new nodes
|
|
274
361
|
if (result != null && result !== false) {
|
|
275
|
-
const fragment =
|
|
362
|
+
const fragment = dom.createDocumentFragment();
|
|
276
363
|
if (typeof result === 'string' || typeof result === 'number') {
|
|
277
|
-
const textNode =
|
|
278
|
-
|
|
364
|
+
const textNode = dom.createTextNode(String(result));
|
|
365
|
+
dom.appendChild(fragment, textNode);
|
|
279
366
|
currentNodes.push(textNode);
|
|
280
|
-
} else if (result
|
|
281
|
-
|
|
367
|
+
} else if (dom.isNode(result)) {
|
|
368
|
+
dom.appendChild(fragment, result);
|
|
282
369
|
currentNodes.push(result);
|
|
283
370
|
} else if (Array.isArray(result)) {
|
|
284
371
|
for (const r of result) {
|
|
285
|
-
if (r
|
|
286
|
-
|
|
372
|
+
if (dom.isNode(r)) {
|
|
373
|
+
dom.appendChild(fragment, r);
|
|
287
374
|
currentNodes.push(r);
|
|
288
375
|
} else if (r != null && r !== false) {
|
|
289
|
-
const textNode =
|
|
290
|
-
|
|
376
|
+
const textNode = dom.createTextNode(String(r));
|
|
377
|
+
dom.appendChild(fragment, textNode);
|
|
291
378
|
currentNodes.push(textNode);
|
|
292
379
|
}
|
|
293
380
|
}
|
|
294
381
|
}
|
|
295
|
-
|
|
296
|
-
|
|
382
|
+
const placeholderParent = dom.getParentNode(placeholder);
|
|
383
|
+
if (placeholderParent) {
|
|
384
|
+
dom.insertBefore(placeholderParent, fragment, dom.getNextSibling(placeholder));
|
|
297
385
|
} else {
|
|
298
386
|
log.warn('Cannot insert reactive children: placeholder has no parent node');
|
|
299
387
|
}
|
|
@@ -306,33 +394,74 @@ function appendChild(parent, child) {
|
|
|
306
394
|
* Create a reactive text node
|
|
307
395
|
*/
|
|
308
396
|
export function text(getValue) {
|
|
397
|
+
const dom = getAdapter();
|
|
309
398
|
if (typeof getValue === 'function') {
|
|
310
|
-
const node =
|
|
399
|
+
const node = dom.createTextNode('');
|
|
311
400
|
effect(() => {
|
|
312
|
-
node
|
|
401
|
+
dom.setTextContent(node, String(getValue()));
|
|
313
402
|
});
|
|
314
403
|
return node;
|
|
315
404
|
}
|
|
316
|
-
return
|
|
405
|
+
return dom.createTextNode(String(getValue));
|
|
317
406
|
}
|
|
318
407
|
|
|
319
408
|
/**
|
|
320
|
-
*
|
|
409
|
+
* URL attributes that need sanitization in bind()
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
const BIND_URL_ATTRIBUTES = new Set([
|
|
413
|
+
'href', 'src', 'action', 'formaction', 'data', 'poster',
|
|
414
|
+
'cite', 'codebase', 'background', 'profile', 'usemap', 'longdesc'
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Bind an attribute reactively with XSS protection
|
|
419
|
+
*
|
|
420
|
+
* Security: URL attributes (href, src, etc.) are sanitized to prevent javascript: XSS
|
|
321
421
|
*/
|
|
322
422
|
export function bind(element, attr, getValue) {
|
|
423
|
+
const dom = getAdapter();
|
|
424
|
+
const lowerAttr = attr.toLowerCase();
|
|
425
|
+
const isUrlAttr = BIND_URL_ATTRIBUTES.has(lowerAttr);
|
|
426
|
+
|
|
323
427
|
if (typeof getValue === 'function') {
|
|
324
428
|
effect(() => {
|
|
325
429
|
const value = getValue();
|
|
326
430
|
if (value == null || value === false) {
|
|
327
|
-
|
|
431
|
+
dom.removeAttribute(element, attr);
|
|
328
432
|
} else if (value === true) {
|
|
329
|
-
|
|
433
|
+
dom.setAttribute(element, attr, '');
|
|
330
434
|
} else {
|
|
331
|
-
|
|
435
|
+
// Sanitize URL attributes to prevent javascript: XSS
|
|
436
|
+
if (isUrlAttr) {
|
|
437
|
+
const sanitized = sanitizeUrl(String(value));
|
|
438
|
+
if (sanitized === null) {
|
|
439
|
+
console.warn(
|
|
440
|
+
`[Pulse Security] Dangerous URL blocked in bind() for ${attr}: "${String(value).slice(0, 50)}"`
|
|
441
|
+
);
|
|
442
|
+
dom.removeAttribute(element, attr);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
dom.setAttribute(element, attr, sanitized);
|
|
446
|
+
} else {
|
|
447
|
+
dom.setAttribute(element, attr, String(value));
|
|
448
|
+
}
|
|
332
449
|
}
|
|
333
450
|
});
|
|
334
451
|
} else {
|
|
335
|
-
|
|
452
|
+
// Sanitize URL attributes for static values too
|
|
453
|
+
if (isUrlAttr) {
|
|
454
|
+
const sanitized = sanitizeUrl(String(getValue));
|
|
455
|
+
if (sanitized === null) {
|
|
456
|
+
console.warn(
|
|
457
|
+
`[Pulse Security] Dangerous URL blocked in bind() for ${attr}: "${String(getValue).slice(0, 50)}"`
|
|
458
|
+
);
|
|
459
|
+
return element;
|
|
460
|
+
}
|
|
461
|
+
dom.setAttribute(element, attr, sanitized);
|
|
462
|
+
} else {
|
|
463
|
+
dom.setAttribute(element, attr, String(getValue));
|
|
464
|
+
}
|
|
336
465
|
}
|
|
337
466
|
return element;
|
|
338
467
|
}
|
|
@@ -341,12 +470,13 @@ export function bind(element, attr, getValue) {
|
|
|
341
470
|
* Bind a property reactively
|
|
342
471
|
*/
|
|
343
472
|
export function prop(element, propName, getValue) {
|
|
473
|
+
const dom = getAdapter();
|
|
344
474
|
if (typeof getValue === 'function') {
|
|
345
475
|
effect(() => {
|
|
346
|
-
element
|
|
476
|
+
dom.setProperty(element, propName, getValue());
|
|
347
477
|
});
|
|
348
478
|
} else {
|
|
349
|
-
element
|
|
479
|
+
dom.setProperty(element, propName, getValue);
|
|
350
480
|
}
|
|
351
481
|
return element;
|
|
352
482
|
}
|
|
@@ -355,30 +485,43 @@ export function prop(element, propName, getValue) {
|
|
|
355
485
|
* Bind CSS class reactively
|
|
356
486
|
*/
|
|
357
487
|
export function cls(element, className, condition) {
|
|
488
|
+
const dom = getAdapter();
|
|
358
489
|
if (typeof condition === 'function') {
|
|
359
490
|
effect(() => {
|
|
360
491
|
if (condition()) {
|
|
361
|
-
|
|
492
|
+
dom.addClass(element, className);
|
|
362
493
|
} else {
|
|
363
|
-
|
|
494
|
+
dom.removeClass(element, className);
|
|
364
495
|
}
|
|
365
496
|
});
|
|
366
497
|
} else if (condition) {
|
|
367
|
-
|
|
498
|
+
dom.addClass(element, className);
|
|
368
499
|
}
|
|
369
500
|
return element;
|
|
370
501
|
}
|
|
371
502
|
|
|
372
503
|
/**
|
|
373
|
-
* Bind style property reactively
|
|
504
|
+
* Bind style property reactively with CSS injection protection
|
|
505
|
+
*
|
|
506
|
+
* Security: CSS values are sanitized to prevent injection attacks via:
|
|
507
|
+
* - Semicolons (property injection: 'red; position: fixed')
|
|
508
|
+
* - url() (data exfiltration)
|
|
509
|
+
* - expression() (IE script execution)
|
|
510
|
+
*
|
|
511
|
+
* @param {HTMLElement} element - Target element
|
|
512
|
+
* @param {string} prop - CSS property name
|
|
513
|
+
* @param {*} getValue - Value or function returning value
|
|
514
|
+
* @param {Object} [options] - Options passed to safeSetStyle
|
|
515
|
+
* @returns {HTMLElement} The element for chaining
|
|
374
516
|
*/
|
|
375
|
-
export function style(element, prop, getValue) {
|
|
517
|
+
export function style(element, prop, getValue, options = {}) {
|
|
518
|
+
const dom = getAdapter();
|
|
376
519
|
if (typeof getValue === 'function') {
|
|
377
520
|
effect(() => {
|
|
378
|
-
element
|
|
521
|
+
safeSetStyle(element, prop, getValue(), options, dom);
|
|
379
522
|
});
|
|
380
523
|
} else {
|
|
381
|
-
element
|
|
524
|
+
safeSetStyle(element, prop, getValue, options, dom);
|
|
382
525
|
}
|
|
383
526
|
return element;
|
|
384
527
|
}
|
|
@@ -387,11 +530,12 @@ export function style(element, prop, getValue) {
|
|
|
387
530
|
* Attach an event listener
|
|
388
531
|
*/
|
|
389
532
|
export function on(element, event, handler, options) {
|
|
390
|
-
|
|
533
|
+
const dom = getAdapter();
|
|
534
|
+
dom.addEventListener(element, event, handler, options);
|
|
391
535
|
|
|
392
536
|
// Auto-cleanup: remove listener when effect is disposed (HMR support)
|
|
393
537
|
onCleanup(() => {
|
|
394
|
-
|
|
538
|
+
dom.removeEventListener(element, event, handler, options);
|
|
395
539
|
});
|
|
396
540
|
|
|
397
541
|
return element;
|
|
@@ -483,12 +627,13 @@ function computeLIS(arr) {
|
|
|
483
627
|
* @returns {DocumentFragment} Container fragment with reactive list
|
|
484
628
|
*/
|
|
485
629
|
export function list(getItems, template, keyFn = (item, i) => i) {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
const
|
|
630
|
+
const dom = getAdapter();
|
|
631
|
+
const container = dom.createDocumentFragment();
|
|
632
|
+
const startMarker = dom.createComment('list-start');
|
|
633
|
+
const endMarker = dom.createComment('list-end');
|
|
489
634
|
|
|
490
|
-
|
|
491
|
-
|
|
635
|
+
dom.appendChild(container, startMarker);
|
|
636
|
+
dom.appendChild(container, endMarker);
|
|
492
637
|
|
|
493
638
|
// Map: key -> { nodes: Node[], cleanup: Function, item: any }
|
|
494
639
|
let itemNodes = new Map();
|
|
@@ -529,7 +674,7 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
529
674
|
for (const [key, entry] of itemNodes) {
|
|
530
675
|
if (!newItemNodes.has(key)) {
|
|
531
676
|
for (const node of entry.nodes) {
|
|
532
|
-
|
|
677
|
+
dom.removeNode(node);
|
|
533
678
|
}
|
|
534
679
|
if (entry.cleanup) entry.cleanup();
|
|
535
680
|
}
|
|
@@ -561,17 +706,17 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
561
706
|
});
|
|
562
707
|
|
|
563
708
|
// Phase 5: Position nodes with minimal DOM operations
|
|
564
|
-
const parent = startMarker
|
|
709
|
+
const parent = dom.getParentNode(startMarker);
|
|
565
710
|
if (!parent) {
|
|
566
711
|
// Not yet in DOM, use simple append with DocumentFragment batch
|
|
567
|
-
const fragment =
|
|
712
|
+
const fragment = dom.createDocumentFragment();
|
|
568
713
|
for (const key of newKeys) {
|
|
569
714
|
const entry = newItemNodes.get(key);
|
|
570
715
|
for (const node of entry.nodes) {
|
|
571
|
-
|
|
716
|
+
dom.appendChild(fragment, node);
|
|
572
717
|
}
|
|
573
718
|
}
|
|
574
|
-
|
|
719
|
+
dom.insertBefore(container, fragment, endMarker);
|
|
575
720
|
} else {
|
|
576
721
|
// Optimized reordering: batch consecutive inserts using DocumentFragment
|
|
577
722
|
let prevNode = startMarker;
|
|
@@ -585,14 +730,14 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
585
730
|
const isStable = stableKeys.has(key);
|
|
586
731
|
|
|
587
732
|
// Check if node is already in correct position
|
|
588
|
-
const inPosition = prevNode
|
|
733
|
+
const inPosition = dom.getNextSibling(prevNode) === firstNode;
|
|
589
734
|
|
|
590
735
|
if (inPosition && (isStable || isNew)) {
|
|
591
736
|
// Already in correct position, just advance
|
|
592
737
|
prevNode = entry.nodes[entry.nodes.length - 1];
|
|
593
738
|
} else {
|
|
594
739
|
// Collect consecutive items that need to be inserted at this position
|
|
595
|
-
const fragment =
|
|
740
|
+
const fragment = dom.createDocumentFragment();
|
|
596
741
|
let j = i;
|
|
597
742
|
|
|
598
743
|
while (j < newKeys.length) {
|
|
@@ -603,14 +748,14 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
603
748
|
const s = stableKeys.has(k);
|
|
604
749
|
|
|
605
750
|
// If this item is already in position after prevNode, stop batching
|
|
606
|
-
if (j > i && prevNode
|
|
751
|
+
if (j > i && dom.getNextSibling(prevNode) === f) {
|
|
607
752
|
break;
|
|
608
753
|
}
|
|
609
754
|
|
|
610
755
|
// Add to batch if it's new or needs to move
|
|
611
|
-
if (n || !s || prevNode
|
|
756
|
+
if (n || !s || dom.getNextSibling(prevNode) !== f) {
|
|
612
757
|
for (const node of e.nodes) {
|
|
613
|
-
|
|
758
|
+
dom.appendChild(fragment, node);
|
|
614
759
|
}
|
|
615
760
|
j++;
|
|
616
761
|
} else {
|
|
@@ -619,8 +764,8 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
619
764
|
}
|
|
620
765
|
|
|
621
766
|
// Insert the batch
|
|
622
|
-
if (fragment
|
|
623
|
-
|
|
767
|
+
if (dom.getFirstChild(fragment)) {
|
|
768
|
+
dom.insertBefore(parent, fragment, dom.getNextSibling(prevNode));
|
|
624
769
|
}
|
|
625
770
|
|
|
626
771
|
// Update prevNode to last inserted node
|
|
@@ -644,9 +789,10 @@ export function list(getItems, template, keyFn = (item, i) => i) {
|
|
|
644
789
|
* Conditional rendering
|
|
645
790
|
*/
|
|
646
791
|
export function when(condition, thenTemplate, elseTemplate = null) {
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
792
|
+
const dom = getAdapter();
|
|
793
|
+
const container = dom.createDocumentFragment();
|
|
794
|
+
const marker = dom.createComment('when');
|
|
795
|
+
dom.appendChild(container, marker);
|
|
650
796
|
|
|
651
797
|
let currentNodes = [];
|
|
652
798
|
let currentCleanup = null;
|
|
@@ -656,7 +802,7 @@ export function when(condition, thenTemplate, elseTemplate = null) {
|
|
|
656
802
|
|
|
657
803
|
// Cleanup previous
|
|
658
804
|
for (const node of currentNodes) {
|
|
659
|
-
|
|
805
|
+
dom.removeNode(node);
|
|
660
806
|
}
|
|
661
807
|
if (currentCleanup) currentCleanup();
|
|
662
808
|
currentNodes = [];
|
|
@@ -668,14 +814,17 @@ export function when(condition, thenTemplate, elseTemplate = null) {
|
|
|
668
814
|
const result = typeof template === 'function' ? template() : template;
|
|
669
815
|
if (result) {
|
|
670
816
|
const nodes = Array.isArray(result) ? result : [result];
|
|
671
|
-
const fragment =
|
|
817
|
+
const fragment = dom.createDocumentFragment();
|
|
672
818
|
for (const node of nodes) {
|
|
673
|
-
if (node
|
|
674
|
-
|
|
819
|
+
if (dom.isNode(node)) {
|
|
820
|
+
dom.appendChild(fragment, node);
|
|
675
821
|
currentNodes.push(node);
|
|
676
822
|
}
|
|
677
823
|
}
|
|
678
|
-
|
|
824
|
+
const markerParent = dom.getParentNode(marker);
|
|
825
|
+
if (markerParent) {
|
|
826
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
827
|
+
}
|
|
679
828
|
}
|
|
680
829
|
}
|
|
681
830
|
});
|
|
@@ -687,7 +836,8 @@ export function when(condition, thenTemplate, elseTemplate = null) {
|
|
|
687
836
|
* Switch/case rendering
|
|
688
837
|
*/
|
|
689
838
|
export function match(getValue, cases) {
|
|
690
|
-
const
|
|
839
|
+
const dom = getAdapter();
|
|
840
|
+
const marker = dom.createComment('match');
|
|
691
841
|
let currentNodes = [];
|
|
692
842
|
|
|
693
843
|
effect(() => {
|
|
@@ -695,7 +845,7 @@ export function match(getValue, cases) {
|
|
|
695
845
|
|
|
696
846
|
// Remove old nodes
|
|
697
847
|
for (const node of currentNodes) {
|
|
698
|
-
|
|
848
|
+
dom.removeNode(node);
|
|
699
849
|
}
|
|
700
850
|
currentNodes = [];
|
|
701
851
|
|
|
@@ -705,14 +855,17 @@ export function match(getValue, cases) {
|
|
|
705
855
|
const result = typeof template === 'function' ? template() : template;
|
|
706
856
|
if (result) {
|
|
707
857
|
const nodes = Array.isArray(result) ? result : [result];
|
|
708
|
-
const fragment =
|
|
858
|
+
const fragment = dom.createDocumentFragment();
|
|
709
859
|
for (const node of nodes) {
|
|
710
|
-
if (node
|
|
711
|
-
|
|
860
|
+
if (dom.isNode(node)) {
|
|
861
|
+
dom.appendChild(fragment, node);
|
|
712
862
|
currentNodes.push(node);
|
|
713
863
|
}
|
|
714
864
|
}
|
|
715
|
-
|
|
865
|
+
const markerParent = dom.getParentNode(marker);
|
|
866
|
+
if (markerParent) {
|
|
867
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
868
|
+
}
|
|
716
869
|
}
|
|
717
870
|
}
|
|
718
871
|
});
|
|
@@ -727,35 +880,36 @@ export function match(getValue, cases) {
|
|
|
727
880
|
* to prevent memory leaks when the element is removed from the DOM.
|
|
728
881
|
*/
|
|
729
882
|
export function model(element, pulseValue) {
|
|
730
|
-
const
|
|
731
|
-
const
|
|
883
|
+
const dom = getAdapter();
|
|
884
|
+
const tagName = dom.getTagName(element);
|
|
885
|
+
const type = dom.getInputType(element);
|
|
732
886
|
|
|
733
887
|
if (tagName === 'input' && (type === 'checkbox' || type === 'radio')) {
|
|
734
888
|
// Checkbox/Radio
|
|
735
889
|
effect(() => {
|
|
736
|
-
element
|
|
890
|
+
dom.setProperty(element, 'checked', pulseValue.get());
|
|
737
891
|
});
|
|
738
|
-
const handler = () => pulseValue.set(element
|
|
739
|
-
|
|
740
|
-
onCleanup(() =>
|
|
892
|
+
const handler = () => pulseValue.set(dom.getProperty(element, 'checked'));
|
|
893
|
+
dom.addEventListener(element, 'change', handler);
|
|
894
|
+
onCleanup(() => dom.removeEventListener(element, 'change', handler));
|
|
741
895
|
} else if (tagName === 'select') {
|
|
742
896
|
// Select
|
|
743
897
|
effect(() => {
|
|
744
|
-
element
|
|
898
|
+
dom.setProperty(element, 'value', pulseValue.get());
|
|
745
899
|
});
|
|
746
|
-
const handler = () => pulseValue.set(element
|
|
747
|
-
|
|
748
|
-
onCleanup(() =>
|
|
900
|
+
const handler = () => pulseValue.set(dom.getProperty(element, 'value'));
|
|
901
|
+
dom.addEventListener(element, 'change', handler);
|
|
902
|
+
onCleanup(() => dom.removeEventListener(element, 'change', handler));
|
|
749
903
|
} else {
|
|
750
904
|
// Text input, textarea, etc.
|
|
751
905
|
effect(() => {
|
|
752
|
-
if (element
|
|
753
|
-
element
|
|
906
|
+
if (dom.getProperty(element, 'value') !== pulseValue.get()) {
|
|
907
|
+
dom.setProperty(element, 'value', pulseValue.get());
|
|
754
908
|
}
|
|
755
909
|
});
|
|
756
|
-
const handler = () => pulseValue.set(element
|
|
757
|
-
|
|
758
|
-
onCleanup(() =>
|
|
910
|
+
const handler = () => pulseValue.set(dom.getProperty(element, 'value'));
|
|
911
|
+
dom.addEventListener(element, 'input', handler);
|
|
912
|
+
onCleanup(() => dom.removeEventListener(element, 'input', handler));
|
|
759
913
|
}
|
|
760
914
|
|
|
761
915
|
return element;
|
|
@@ -769,18 +923,14 @@ export function model(element, pulseValue) {
|
|
|
769
923
|
* @throws {Error} If target element is not found
|
|
770
924
|
*/
|
|
771
925
|
export function mount(target, element) {
|
|
926
|
+
const dom = getAdapter();
|
|
772
927
|
const { element: resolved, selector } = resolveSelector(target, 'mount');
|
|
773
928
|
if (!resolved) {
|
|
774
|
-
throw
|
|
775
|
-
`[Pulse] Mount target not found: "${selector}". ` +
|
|
776
|
-
`Ensure the element exists in the DOM before mounting. ` +
|
|
777
|
-
`Tip: Use document.addEventListener('DOMContentLoaded', () => mount(...)) ` +
|
|
778
|
-
`or place your script at the end of <body>.`
|
|
779
|
-
);
|
|
929
|
+
throw Errors.mountNotFound(selector);
|
|
780
930
|
}
|
|
781
|
-
|
|
931
|
+
dom.appendChild(resolved, element);
|
|
782
932
|
return () => {
|
|
783
|
-
|
|
933
|
+
dom.removeNode(element);
|
|
784
934
|
};
|
|
785
935
|
}
|
|
786
936
|
|
|
@@ -789,6 +939,7 @@ export function mount(target, element) {
|
|
|
789
939
|
*/
|
|
790
940
|
export function component(setup) {
|
|
791
941
|
return (props = {}) => {
|
|
942
|
+
const dom = getAdapter();
|
|
792
943
|
const state = {};
|
|
793
944
|
const methods = {};
|
|
794
945
|
|
|
@@ -826,7 +977,7 @@ export function component(setup) {
|
|
|
826
977
|
|
|
827
978
|
// Schedule mount callbacks after DOM insertion
|
|
828
979
|
if (mountContext.mountCallbacks.length > 0) {
|
|
829
|
-
queueMicrotask(() => {
|
|
980
|
+
dom.queueMicrotask(() => {
|
|
830
981
|
for (const cb of mountContext.mountCallbacks) {
|
|
831
982
|
try {
|
|
832
983
|
cb();
|
|
@@ -838,7 +989,7 @@ export function component(setup) {
|
|
|
838
989
|
}
|
|
839
990
|
|
|
840
991
|
// Store unmount callbacks on the element for later cleanup
|
|
841
|
-
if (result
|
|
992
|
+
if (dom.isNode(result) && mountContext.unmountCallbacks.length > 0) {
|
|
842
993
|
result._pulseUnmount = mountContext.unmountCallbacks;
|
|
843
994
|
}
|
|
844
995
|
|
|
@@ -851,9 +1002,10 @@ export function component(setup) {
|
|
|
851
1002
|
* Unlike when(), this keeps the element in the DOM but hides it
|
|
852
1003
|
*/
|
|
853
1004
|
export function show(condition, element) {
|
|
1005
|
+
const dom = getAdapter();
|
|
854
1006
|
effect(() => {
|
|
855
1007
|
const shouldShow = typeof condition === 'function' ? condition() : condition.get();
|
|
856
|
-
element
|
|
1008
|
+
dom.setStyle(element, 'display', shouldShow ? '' : 'none');
|
|
857
1009
|
});
|
|
858
1010
|
return element;
|
|
859
1011
|
}
|
|
@@ -862,14 +1014,15 @@ export function show(condition, element) {
|
|
|
862
1014
|
* Portal - render children into a different DOM location
|
|
863
1015
|
*/
|
|
864
1016
|
export function portal(children, target) {
|
|
1017
|
+
const dom = getAdapter();
|
|
865
1018
|
const { element: resolvedTarget, selector } = resolveSelector(target, 'portal');
|
|
866
1019
|
|
|
867
1020
|
if (!resolvedTarget) {
|
|
868
1021
|
log.warn(`Portal target not found: "${selector}"`);
|
|
869
|
-
return
|
|
1022
|
+
return dom.createComment('portal-target-not-found');
|
|
870
1023
|
}
|
|
871
1024
|
|
|
872
|
-
const marker =
|
|
1025
|
+
const marker = dom.createComment('portal');
|
|
873
1026
|
let mountedNodes = [];
|
|
874
1027
|
|
|
875
1028
|
// Handle reactive children
|
|
@@ -877,7 +1030,7 @@ export function portal(children, target) {
|
|
|
877
1030
|
effect(() => {
|
|
878
1031
|
// Cleanup previous nodes
|
|
879
1032
|
for (const node of mountedNodes) {
|
|
880
|
-
|
|
1033
|
+
dom.removeNode(node);
|
|
881
1034
|
if (node._pulseUnmount) {
|
|
882
1035
|
for (const cb of node._pulseUnmount) cb();
|
|
883
1036
|
}
|
|
@@ -888,8 +1041,8 @@ export function portal(children, target) {
|
|
|
888
1041
|
if (result) {
|
|
889
1042
|
const nodes = Array.isArray(result) ? result : [result];
|
|
890
1043
|
for (const node of nodes) {
|
|
891
|
-
if (node
|
|
892
|
-
|
|
1044
|
+
if (dom.isNode(node)) {
|
|
1045
|
+
dom.appendChild(resolvedTarget, node);
|
|
893
1046
|
mountedNodes.push(node);
|
|
894
1047
|
}
|
|
895
1048
|
}
|
|
@@ -899,8 +1052,8 @@ export function portal(children, target) {
|
|
|
899
1052
|
// Static children
|
|
900
1053
|
const nodes = Array.isArray(children) ? children : [children];
|
|
901
1054
|
for (const node of nodes) {
|
|
902
|
-
if (node
|
|
903
|
-
|
|
1055
|
+
if (dom.isNode(node)) {
|
|
1056
|
+
dom.appendChild(resolvedTarget, node);
|
|
904
1057
|
mountedNodes.push(node);
|
|
905
1058
|
}
|
|
906
1059
|
}
|
|
@@ -909,7 +1062,7 @@ export function portal(children, target) {
|
|
|
909
1062
|
// Return marker for position tracking, attach cleanup
|
|
910
1063
|
marker._pulseUnmount = [() => {
|
|
911
1064
|
for (const node of mountedNodes) {
|
|
912
|
-
|
|
1065
|
+
dom.removeNode(node);
|
|
913
1066
|
if (node._pulseUnmount) {
|
|
914
1067
|
for (const cb of node._pulseUnmount) cb();
|
|
915
1068
|
}
|
|
@@ -923,9 +1076,10 @@ export function portal(children, target) {
|
|
|
923
1076
|
* Error boundary - catch errors in child components
|
|
924
1077
|
*/
|
|
925
1078
|
export function errorBoundary(children, fallback) {
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
|
|
1079
|
+
const dom = getAdapter();
|
|
1080
|
+
const container = dom.createDocumentFragment();
|
|
1081
|
+
const marker = dom.createComment('error-boundary');
|
|
1082
|
+
dom.appendChild(container, marker);
|
|
929
1083
|
|
|
930
1084
|
const error = pulse(null);
|
|
931
1085
|
let currentNodes = [];
|
|
@@ -933,7 +1087,7 @@ export function errorBoundary(children, fallback) {
|
|
|
933
1087
|
const renderContent = () => {
|
|
934
1088
|
// Cleanup previous
|
|
935
1089
|
for (const node of currentNodes) {
|
|
936
|
-
|
|
1090
|
+
dom.removeNode(node);
|
|
937
1091
|
}
|
|
938
1092
|
currentNodes = [];
|
|
939
1093
|
|
|
@@ -949,21 +1103,24 @@ export function errorBoundary(children, fallback) {
|
|
|
949
1103
|
|
|
950
1104
|
if (result) {
|
|
951
1105
|
const nodes = Array.isArray(result) ? result : [result];
|
|
952
|
-
const fragment =
|
|
1106
|
+
const fragment = dom.createDocumentFragment();
|
|
953
1107
|
for (const node of nodes) {
|
|
954
|
-
if (node
|
|
955
|
-
|
|
1108
|
+
if (dom.isNode(node)) {
|
|
1109
|
+
dom.appendChild(fragment, node);
|
|
956
1110
|
currentNodes.push(node);
|
|
957
1111
|
}
|
|
958
1112
|
}
|
|
959
|
-
|
|
1113
|
+
const markerParent = dom.getParentNode(marker);
|
|
1114
|
+
if (markerParent) {
|
|
1115
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
1116
|
+
}
|
|
960
1117
|
}
|
|
961
1118
|
} catch (e) {
|
|
962
1119
|
log.error('Error in component:', e);
|
|
963
1120
|
error.set(e);
|
|
964
1121
|
// Re-render with error
|
|
965
1122
|
if (!hasError) {
|
|
966
|
-
queueMicrotask(renderContent);
|
|
1123
|
+
dom.queueMicrotask(renderContent);
|
|
967
1124
|
}
|
|
968
1125
|
}
|
|
969
1126
|
};
|
|
@@ -983,6 +1140,7 @@ export function errorBoundary(children, fallback) {
|
|
|
983
1140
|
* to prevent callbacks executing on removed elements.
|
|
984
1141
|
*/
|
|
985
1142
|
export function transition(element, options = {}) {
|
|
1143
|
+
const dom = getAdapter();
|
|
986
1144
|
const {
|
|
987
1145
|
enter = 'fade-in',
|
|
988
1146
|
exit = 'fade-out',
|
|
@@ -995,7 +1153,7 @@ export function transition(element, options = {}) {
|
|
|
995
1153
|
const activeTimers = new Set();
|
|
996
1154
|
|
|
997
1155
|
const safeTimeout = (fn, delay) => {
|
|
998
|
-
const timerId = setTimeout(() => {
|
|
1156
|
+
const timerId = dom.setTimeout(() => {
|
|
999
1157
|
activeTimers.delete(timerId);
|
|
1000
1158
|
fn();
|
|
1001
1159
|
}, delay);
|
|
@@ -1005,34 +1163,34 @@ export function transition(element, options = {}) {
|
|
|
1005
1163
|
|
|
1006
1164
|
const clearAllTimers = () => {
|
|
1007
1165
|
for (const timerId of activeTimers) {
|
|
1008
|
-
clearTimeout(timerId);
|
|
1166
|
+
dom.clearTimeout(timerId);
|
|
1009
1167
|
}
|
|
1010
1168
|
activeTimers.clear();
|
|
1011
1169
|
};
|
|
1012
1170
|
|
|
1013
1171
|
// Apply enter animation
|
|
1014
1172
|
const applyEnter = () => {
|
|
1015
|
-
|
|
1173
|
+
dom.addClass(element, enter);
|
|
1016
1174
|
if (onEnter) onEnter(element);
|
|
1017
1175
|
safeTimeout(() => {
|
|
1018
|
-
|
|
1176
|
+
dom.removeClass(element, enter);
|
|
1019
1177
|
}, duration);
|
|
1020
1178
|
};
|
|
1021
1179
|
|
|
1022
1180
|
// Apply exit animation and return promise
|
|
1023
1181
|
const applyExit = () => {
|
|
1024
1182
|
return new Promise(resolve => {
|
|
1025
|
-
|
|
1183
|
+
dom.addClass(element, exit);
|
|
1026
1184
|
if (onExit) onExit(element);
|
|
1027
1185
|
safeTimeout(() => {
|
|
1028
|
-
|
|
1186
|
+
dom.removeClass(element, exit);
|
|
1029
1187
|
resolve();
|
|
1030
1188
|
}, duration);
|
|
1031
1189
|
});
|
|
1032
1190
|
};
|
|
1033
1191
|
|
|
1034
1192
|
// Apply enter on mount
|
|
1035
|
-
queueMicrotask(applyEnter);
|
|
1193
|
+
dom.queueMicrotask(applyEnter);
|
|
1036
1194
|
|
|
1037
1195
|
// Attach exit method
|
|
1038
1196
|
element._pulseTransitionExit = applyExit;
|
|
@@ -1050,9 +1208,10 @@ export function transition(element, options = {}) {
|
|
|
1050
1208
|
* to prevent callbacks executing on removed elements.
|
|
1051
1209
|
*/
|
|
1052
1210
|
export function whenTransition(condition, thenTemplate, elseTemplate = null, options = {}) {
|
|
1053
|
-
const
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1211
|
+
const dom = getAdapter();
|
|
1212
|
+
const container = dom.createDocumentFragment();
|
|
1213
|
+
const marker = dom.createComment('when-transition');
|
|
1214
|
+
dom.appendChild(container, marker);
|
|
1056
1215
|
|
|
1057
1216
|
const { duration = 300, enterClass = 'fade-in', exitClass = 'fade-out' } = options;
|
|
1058
1217
|
|
|
@@ -1063,7 +1222,7 @@ export function whenTransition(condition, thenTemplate, elseTemplate = null, opt
|
|
|
1063
1222
|
const activeTimers = new Set();
|
|
1064
1223
|
|
|
1065
1224
|
const safeTimeout = (fn, delay) => {
|
|
1066
|
-
const timerId = setTimeout(() => {
|
|
1225
|
+
const timerId = dom.setTimeout(() => {
|
|
1067
1226
|
activeTimers.delete(timerId);
|
|
1068
1227
|
fn();
|
|
1069
1228
|
}, delay);
|
|
@@ -1073,7 +1232,7 @@ export function whenTransition(condition, thenTemplate, elseTemplate = null, opt
|
|
|
1073
1232
|
|
|
1074
1233
|
const clearAllTimers = () => {
|
|
1075
1234
|
for (const timerId of activeTimers) {
|
|
1076
|
-
clearTimeout(timerId);
|
|
1235
|
+
dom.clearTimeout(timerId);
|
|
1077
1236
|
}
|
|
1078
1237
|
activeTimers.clear();
|
|
1079
1238
|
};
|
|
@@ -1095,12 +1254,12 @@ export function whenTransition(condition, thenTemplate, elseTemplate = null, opt
|
|
|
1095
1254
|
currentNodes = [];
|
|
1096
1255
|
|
|
1097
1256
|
for (const node of nodesToRemove) {
|
|
1098
|
-
|
|
1257
|
+
dom.addClass(node, exitClass);
|
|
1099
1258
|
}
|
|
1100
1259
|
|
|
1101
1260
|
safeTimeout(() => {
|
|
1102
1261
|
for (const node of nodesToRemove) {
|
|
1103
|
-
|
|
1262
|
+
dom.removeNode(node);
|
|
1104
1263
|
}
|
|
1105
1264
|
isTransitioning = false;
|
|
1106
1265
|
|
|
@@ -1109,16 +1268,19 @@ export function whenTransition(condition, thenTemplate, elseTemplate = null, opt
|
|
|
1109
1268
|
const result = typeof template === 'function' ? template() : template;
|
|
1110
1269
|
if (result) {
|
|
1111
1270
|
const nodes = Array.isArray(result) ? result : [result];
|
|
1112
|
-
const fragment =
|
|
1271
|
+
const fragment = dom.createDocumentFragment();
|
|
1113
1272
|
for (const node of nodes) {
|
|
1114
|
-
if (node
|
|
1115
|
-
|
|
1116
|
-
|
|
1273
|
+
if (dom.isNode(node)) {
|
|
1274
|
+
dom.addClass(node, enterClass);
|
|
1275
|
+
dom.appendChild(fragment, node);
|
|
1117
1276
|
currentNodes.push(node);
|
|
1118
|
-
safeTimeout(() =>
|
|
1277
|
+
safeTimeout(() => dom.removeClass(node, enterClass), duration);
|
|
1119
1278
|
}
|
|
1120
1279
|
}
|
|
1121
|
-
|
|
1280
|
+
const markerParent = dom.getParentNode(marker);
|
|
1281
|
+
if (markerParent) {
|
|
1282
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
1283
|
+
}
|
|
1122
1284
|
}
|
|
1123
1285
|
}
|
|
1124
1286
|
}, duration);
|
|
@@ -1127,16 +1289,19 @@ export function whenTransition(condition, thenTemplate, elseTemplate = null, opt
|
|
|
1127
1289
|
const result = typeof template === 'function' ? template() : template;
|
|
1128
1290
|
if (result) {
|
|
1129
1291
|
const nodes = Array.isArray(result) ? result : [result];
|
|
1130
|
-
const fragment =
|
|
1292
|
+
const fragment = dom.createDocumentFragment();
|
|
1131
1293
|
for (const node of nodes) {
|
|
1132
|
-
if (node
|
|
1133
|
-
|
|
1134
|
-
|
|
1294
|
+
if (dom.isNode(node)) {
|
|
1295
|
+
dom.addClass(node, enterClass);
|
|
1296
|
+
dom.appendChild(fragment, node);
|
|
1135
1297
|
currentNodes.push(node);
|
|
1136
|
-
safeTimeout(() =>
|
|
1298
|
+
safeTimeout(() => dom.removeClass(node, enterClass), duration);
|
|
1137
1299
|
}
|
|
1138
1300
|
}
|
|
1139
|
-
|
|
1301
|
+
const markerParent = dom.getParentNode(marker);
|
|
1302
|
+
if (markerParent) {
|
|
1303
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
1304
|
+
}
|
|
1140
1305
|
}
|
|
1141
1306
|
}
|
|
1142
1307
|
});
|
|
@@ -1167,6 +1332,10 @@ export default {
|
|
|
1167
1332
|
errorBoundary,
|
|
1168
1333
|
transition,
|
|
1169
1334
|
whenTransition,
|
|
1335
|
+
// Configuration
|
|
1336
|
+
configureDom,
|
|
1337
|
+
getDomConfig,
|
|
1338
|
+
clearSelectorCache,
|
|
1170
1339
|
// Diagnostics
|
|
1171
1340
|
getCacheMetrics,
|
|
1172
1341
|
resetCacheMetrics
|