lightview 2.0.7 → 2.0.8
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 +1 -1
- package/docs/about.html +1 -7
- package/docs/index.html +0 -4
- package/lightview-router.js +94 -299
- package/lightview-x.js +174 -504
- package/lightview.js +123 -107
- package/package.json +1 -1
package/lightview.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
(() => {
|
|
2
|
+
/**
|
|
3
|
+
* LIGHTVIEW CORE
|
|
4
|
+
* A minimalist library for signals-based reactivity and functional UI components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
2
7
|
// ============= SIGNALS =============
|
|
3
8
|
|
|
4
9
|
let currentEffect = null;
|
|
5
10
|
|
|
6
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Helper to get a value from a Map or create and set it if it doesn't exist.
|
|
14
|
+
*/
|
|
7
15
|
const getOrSet = (map, key, factory) => {
|
|
8
16
|
let v = map.get(key);
|
|
9
17
|
if (!v) {
|
|
@@ -18,6 +26,11 @@
|
|
|
18
26
|
|
|
19
27
|
const signalRegistry = new Map();
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates a reactive signal.
|
|
31
|
+
* @param {*} initialValue - The initial value of the signal.
|
|
32
|
+
* @param {Object|string} [optionsOrName] - Optional name (for registry) or options object.
|
|
33
|
+
*/
|
|
21
34
|
const signal = (initialValue, optionsOrName) => {
|
|
22
35
|
let name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
|
|
23
36
|
const storage = optionsOrName?.storage;
|
|
@@ -80,19 +93,29 @@
|
|
|
80
93
|
return signalRegistry.get(name);
|
|
81
94
|
};
|
|
82
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Creates a side-effect that automatically tracks and re-runs when its signal dependencies change.
|
|
98
|
+
* @param {Function} fn - The function to execute as an effect.
|
|
99
|
+
*/
|
|
83
100
|
const effect = (fn) => {
|
|
84
101
|
const execute = () => {
|
|
85
|
-
if (!execute.active) return;
|
|
102
|
+
if (!execute.active || execute.running) return;
|
|
86
103
|
// Cleanup old dependencies
|
|
87
104
|
execute.dependencies.forEach(dep => dep.delete(execute));
|
|
88
105
|
execute.dependencies.clear();
|
|
89
106
|
|
|
107
|
+
execute.running = true;
|
|
90
108
|
currentEffect = execute;
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
try {
|
|
110
|
+
fn();
|
|
111
|
+
} finally {
|
|
112
|
+
currentEffect = null;
|
|
113
|
+
execute.running = false;
|
|
114
|
+
}
|
|
93
115
|
};
|
|
94
116
|
|
|
95
117
|
execute.active = true;
|
|
118
|
+
execute.running = false;
|
|
96
119
|
execute.dependencies = new Set();
|
|
97
120
|
execute.stop = () => {
|
|
98
121
|
execute.dependencies.forEach(dep => dep.delete(execute));
|
|
@@ -103,12 +126,18 @@
|
|
|
103
126
|
return execute;
|
|
104
127
|
};
|
|
105
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Assocates an effect with a DOM node for automatic cleanup when the node is removed.
|
|
131
|
+
*/
|
|
106
132
|
const trackEffect = (node, effectFn) => {
|
|
107
133
|
const state = getOrSet(nodeState, node, nodeStateFactory);
|
|
108
134
|
if (!state.effects) state.effects = [];
|
|
109
135
|
state.effects.push(effectFn);
|
|
110
136
|
};
|
|
111
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Creates a read-only signal derived from other signals.
|
|
140
|
+
*/
|
|
112
141
|
const computed = (fn) => {
|
|
113
142
|
const sig = signal(undefined);
|
|
114
143
|
effect(() => {
|
|
@@ -194,20 +223,13 @@
|
|
|
194
223
|
};
|
|
195
224
|
|
|
196
225
|
// ============= REACTIVE UI =============
|
|
197
|
-
|
|
198
|
-
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'marker',
|
|
199
|
-
'pattern', 'mask', 'image', 'text', 'tspan', 'foreignObject', 'use', 'symbol', 'clipPath',
|
|
200
|
-
'linearGradient', 'radialGradient', 'stop', 'filter', 'animate', 'animateMotion',
|
|
201
|
-
'animateTransform', 'mpath', 'desc', 'metadata', 'title', 'feBlend', 'feColorMatrix',
|
|
202
|
-
'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
|
|
203
|
-
'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB',
|
|
204
|
-
'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
|
|
205
|
-
'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight',
|
|
206
|
-
'feTile', 'feTurbulence', 'view'
|
|
207
|
-
]);
|
|
226
|
+
let inSVG = false;
|
|
208
227
|
|
|
209
228
|
const domToElement = new WeakMap();
|
|
210
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Wraps a native DOM element in a Lightview reactive proxy.
|
|
232
|
+
*/
|
|
211
233
|
const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
|
|
212
234
|
const el = {
|
|
213
235
|
tag,
|
|
@@ -220,6 +242,10 @@
|
|
|
220
242
|
return proxy;
|
|
221
243
|
};
|
|
222
244
|
|
|
245
|
+
/**
|
|
246
|
+
* The core virtual-DOM-to-real-DOM factory.
|
|
247
|
+
* Handles tag functions (components), shadow DOM directives, and SVG namespaces.
|
|
248
|
+
*/
|
|
223
249
|
const element = (tag, attributes = {}, children = []) => {
|
|
224
250
|
if (customTags[tag]) tag = customTags[tag];
|
|
225
251
|
// If tag is a function (component), call it and process the result
|
|
@@ -233,13 +259,19 @@
|
|
|
233
259
|
return createShadowDOMMarker(attributes, children);
|
|
234
260
|
}
|
|
235
261
|
|
|
236
|
-
const
|
|
237
|
-
const
|
|
262
|
+
const isSVG = tag.toLowerCase() === 'svg';
|
|
263
|
+
const wasInSVG = inSVG;
|
|
264
|
+
if (isSVG) inSVG = true;
|
|
265
|
+
|
|
266
|
+
const domNode = inSVG
|
|
238
267
|
? document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
239
268
|
: document.createElement(tag);
|
|
269
|
+
|
|
240
270
|
const proxy = wrapDomElement(domNode, tag, attributes, children);
|
|
241
271
|
proxy.attributes = attributes;
|
|
242
272
|
proxy.children = children;
|
|
273
|
+
|
|
274
|
+
if (isSVG) inSVG = wasInSVG;
|
|
243
275
|
return proxy;
|
|
244
276
|
};
|
|
245
277
|
|
|
@@ -292,6 +324,9 @@
|
|
|
292
324
|
return null;
|
|
293
325
|
};
|
|
294
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Internal proxy to intercept 'attributes' and 'children' updates on an element.
|
|
329
|
+
*/
|
|
295
330
|
const makeReactive = (el) => {
|
|
296
331
|
const domNode = el.domEl;
|
|
297
332
|
|
|
@@ -309,31 +344,25 @@
|
|
|
309
344
|
});
|
|
310
345
|
};
|
|
311
346
|
|
|
312
|
-
//
|
|
313
|
-
const
|
|
314
|
-
'disabled', 'checked', 'readonly', 'required', 'hidden', 'autofocus',
|
|
315
|
-
'autoplay', 'controls', 'loop', 'muted', 'default', 'defer', 'async',
|
|
316
|
-
'novalidate', 'formnovalidate', 'open', 'selected', 'multiple', 'reversed',
|
|
317
|
-
'ismap', 'nomodule', 'playsinline', 'allowfullscreen', 'inert'
|
|
318
|
-
]);
|
|
347
|
+
// Properties that should be set directly on the DOM node object rather than as attributes
|
|
348
|
+
const NODE_PROPERTIES = new Set(['value', 'checked', 'selected', 'selectedIndex', 'className', 'innerHTML', 'innerText']);
|
|
319
349
|
|
|
320
350
|
// Set attribute with proper handling of boolean attributes and undefined/null values
|
|
321
351
|
const setAttributeValue = (domNode, key, value) => {
|
|
322
|
-
const
|
|
352
|
+
const isBool = typeof domNode[key] === 'boolean';
|
|
323
353
|
|
|
324
|
-
if (
|
|
354
|
+
if (NODE_PROPERTIES.has(key) || isBool) {
|
|
355
|
+
domNode[key] = isBool ? (value !== null && value !== undefined && value !== false && value !== 'false') : value;
|
|
356
|
+
} else if (value === null || value === undefined) {
|
|
325
357
|
domNode.removeAttribute(key);
|
|
326
|
-
} else if (isBooleanAttr) {
|
|
327
|
-
if (value && value !== 'false') {
|
|
328
|
-
domNode.setAttribute(key, '');
|
|
329
|
-
} else {
|
|
330
|
-
domNode.removeAttribute(key);
|
|
331
|
-
}
|
|
332
358
|
} else {
|
|
333
359
|
domNode.setAttribute(key, value);
|
|
334
360
|
}
|
|
335
361
|
};
|
|
336
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Processes attributes, handling event listeners, reactive bindings, and special 'onmount' hooks.
|
|
365
|
+
*/
|
|
337
366
|
const makeReactiveAttributes = (attributes, domNode) => {
|
|
338
367
|
const reactiveAttrs = {};
|
|
339
368
|
|
|
@@ -399,6 +428,10 @@
|
|
|
399
428
|
* @param {boolean} clearExisting - Whether to clear existing content
|
|
400
429
|
* @returns {Array} - Processed child elements
|
|
401
430
|
*/
|
|
431
|
+
/**
|
|
432
|
+
* Core child processing logic. Recursively handles strings, arrays,
|
|
433
|
+
* reactive functions, vDOM objects, and Shadow DOM markers.
|
|
434
|
+
*/
|
|
402
435
|
const processChildren = (children, targetNode, clearExisting = true) => {
|
|
403
436
|
if (clearExisting && targetNode.innerHTML !== undefined) {
|
|
404
437
|
targetNode.innerHTML = ''; // Clear existing
|
|
@@ -410,19 +443,15 @@
|
|
|
410
443
|
const isSpecialElement = targetNode.tagName &&
|
|
411
444
|
(targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
|
|
412
445
|
|
|
413
|
-
|
|
446
|
+
const flatChildren = children.flat(Infinity);
|
|
447
|
+
|
|
448
|
+
for (let child of flatChildren) {
|
|
414
449
|
// Allow extensions to transform children (e.g., template literals)
|
|
415
450
|
// BUT skip for script/style elements which need raw content
|
|
416
451
|
if (Lightview.hooks.processChild && !isSpecialElement) {
|
|
417
452
|
child = Lightview.hooks.processChild(child) ?? child;
|
|
418
453
|
}
|
|
419
454
|
|
|
420
|
-
// Handle nested arrays (flattening)
|
|
421
|
-
if (Array.isArray(child)) {
|
|
422
|
-
childElements.push(...processChildren(child, targetNode, false));
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
455
|
// Handle shadowDOM markers - attach shadow to parent and process shadow children
|
|
427
456
|
if (isShadowDOMMarker(child)) {
|
|
428
457
|
// targetNode is the parent element that should get the shadow root
|
|
@@ -437,45 +466,40 @@
|
|
|
437
466
|
|
|
438
467
|
const type = typeof child;
|
|
439
468
|
if (type === 'function') {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
textNode.textContent = val !== undefined ? val : '';
|
|
475
|
-
});
|
|
476
|
-
trackEffect(textNode, runner);
|
|
477
|
-
childElements.push(child);
|
|
478
|
-
}
|
|
469
|
+
const startMarker = document.createComment('lv:s');
|
|
470
|
+
const endMarker = document.createComment('lv:e');
|
|
471
|
+
targetNode.appendChild(startMarker);
|
|
472
|
+
targetNode.appendChild(endMarker);
|
|
473
|
+
|
|
474
|
+
let runner;
|
|
475
|
+
const update = () => {
|
|
476
|
+
// 1. Cleanup: Remove everything between markers
|
|
477
|
+
while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
|
|
478
|
+
startMarker.nextSibling.remove();
|
|
479
|
+
// Note: MutationObserver handles cleanupNode(removedNode)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 2. Execution: Get new value and process it
|
|
483
|
+
const val = child();
|
|
484
|
+
if (val === undefined || val === null) return;
|
|
485
|
+
|
|
486
|
+
// 3. Render: Process children into a fragment and insert before endMarker
|
|
487
|
+
const fragment = document.createDocumentFragment();
|
|
488
|
+
const childrenToProcess = Array.isArray(val) ? val : [val];
|
|
489
|
+
|
|
490
|
+
// Stop the runner if the markers are no longer in the DOM
|
|
491
|
+
if (runner && !startMarker.isConnected) {
|
|
492
|
+
runner.stop();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
processChildren(childrenToProcess, fragment, false);
|
|
497
|
+
endMarker.parentNode.insertBefore(fragment, endMarker);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
runner = effect(update);
|
|
501
|
+
trackEffect(startMarker, runner);
|
|
502
|
+
childElements.push(child);
|
|
479
503
|
} else if (['string', 'number', 'boolean', 'symbol'].includes(type)) {
|
|
480
504
|
// Static text
|
|
481
505
|
targetNode.appendChild(document.createTextNode(child));
|
|
@@ -507,6 +531,9 @@
|
|
|
507
531
|
};
|
|
508
532
|
|
|
509
533
|
// ============= EXPORTS =============
|
|
534
|
+
/**
|
|
535
|
+
* Enhances an existing DOM element with Lightview reactivity.
|
|
536
|
+
*/
|
|
510
537
|
const enhance = (selectorOrNode, options = {}) => {
|
|
511
538
|
const domNode = typeof selectorOrNode === 'string'
|
|
512
539
|
? document.querySelector(selectorOrNode)
|
|
@@ -549,6 +576,9 @@
|
|
|
549
576
|
return el;
|
|
550
577
|
};
|
|
551
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Query selector helper that adds a .content() method for easy DOM manipulation.
|
|
581
|
+
*/
|
|
552
582
|
const $ = (cssSelectorOrElement, startingDomEl = document.body) => {
|
|
553
583
|
const el = typeof cssSelectorOrElement === 'string' ? startingDomEl.querySelector(cssSelectorOrElement) : cssSelectorOrElement;
|
|
554
584
|
if (!el) return null;
|
|
@@ -574,37 +604,19 @@
|
|
|
574
604
|
}
|
|
575
605
|
});
|
|
576
606
|
|
|
577
|
-
|
|
578
|
-
let shadow = el.shadowRoot;
|
|
579
|
-
if (!shadow) {
|
|
580
|
-
shadow = el.attachShadow({ mode: 'open' });
|
|
581
|
-
}
|
|
582
|
-
shadow.innerHTML = '';
|
|
583
|
-
array.forEach(item => {
|
|
584
|
-
shadow.appendChild(item);
|
|
585
|
-
});
|
|
586
|
-
return el;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (location === 'inner') {
|
|
590
|
-
el.innerHTML = '';
|
|
591
|
-
array.forEach(item => {
|
|
592
|
-
el.appendChild(item);
|
|
593
|
-
});
|
|
594
|
-
return el;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (location === 'outer') {
|
|
598
|
-
el.replaceWith(...array);
|
|
599
|
-
return el;
|
|
600
|
-
}
|
|
607
|
+
const target = location === 'shadow' ? (el.shadowRoot || el.attachShadow({ mode: 'open' })) : el;
|
|
601
608
|
|
|
602
|
-
if (location === '
|
|
603
|
-
|
|
609
|
+
if (location === 'inner' || location === 'shadow') {
|
|
610
|
+
target.replaceChildren(...array);
|
|
611
|
+
} else if (location === 'outer') {
|
|
612
|
+
target.replaceWith(...array);
|
|
613
|
+
} else if (location === 'afterbegin') {
|
|
614
|
+
target.prepend(...array);
|
|
615
|
+
} else if (location === 'beforeend') {
|
|
616
|
+
target.append(...array);
|
|
617
|
+
} else {
|
|
618
|
+
array.forEach(item => el.insertAdjacentElement(location, item));
|
|
604
619
|
}
|
|
605
|
-
array.forEach(item => {
|
|
606
|
-
el.insertAdjacentElement(location, item);
|
|
607
|
-
});
|
|
608
620
|
return el;
|
|
609
621
|
},
|
|
610
622
|
configurable: true,
|
|
@@ -614,6 +626,10 @@
|
|
|
614
626
|
};
|
|
615
627
|
|
|
616
628
|
const customTags = {}
|
|
629
|
+
/**
|
|
630
|
+
* Proxy for accessing or registering tags/components.
|
|
631
|
+
* e.g., Lightview.tags.div(...) or Lightview.tags.MyComponent = ...
|
|
632
|
+
*/
|
|
617
633
|
const tags = new Proxy({}, {
|
|
618
634
|
get(_, tag) {
|
|
619
635
|
if (tag === "_customTags") return { ...customTags };
|
|
@@ -626,7 +642,7 @@
|
|
|
626
642
|
attributes = arg0;
|
|
627
643
|
children = args.slice(1);
|
|
628
644
|
}
|
|
629
|
-
return element(customTags[tag] || tag, attributes, children
|
|
645
|
+
return element(customTags[tag] || tag, attributes, children);
|
|
630
646
|
};
|
|
631
647
|
|
|
632
648
|
// Lift static methods/properties from the component onto the wrapper
|