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/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
- fn();
92
- currentEffect = null;
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
- const SVG_TAGS = new Set([
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 isSvg = SVG_TAGS.has(tag.toLowerCase());
237
- const domNode = isSvg
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
- // Boolean attributes that should be present/absent rather than having a value
313
- const BOOLEAN_ATTRIBUTES = new Set([
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 isBooleanAttr = BOOLEAN_ATTRIBUTES.has(key.toLowerCase());
352
+ const isBool = typeof domNode[key] === 'boolean';
323
353
 
324
- if (value === null || value === undefined) {
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
- for (let child of children) {
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 result = child();
441
- // Determine if the result implies complex content (DOM/vDOM/Array)
442
- // Treat as complex if it's an object (including arrays) but not null
443
- const isComplex = result && (typeof result === 'object' || Array.isArray(result));
444
-
445
- if (isComplex) {
446
- // Reactive element, vDOM object, or list of items
447
- // Use a stable wrapper div to hold the reactive content
448
- const wrapper = document.createElement('span');
449
- wrapper.style.display = 'contents';
450
- targetNode.appendChild(wrapper);
451
-
452
- let runner;
453
- const update = () => {
454
- const val = child();
455
- // Check if wrapper is still in the DOM (skip check on first run)
456
- if (runner && !wrapper.parentNode) {
457
- runner.stop();
458
- return;
459
- }
460
- const childrenToProcess = Array.isArray(val) ? val : [val];
461
- // processChildren handles clearing existing content via 3rd arg=true
462
- processChildren(childrenToProcess, wrapper, true);
463
- };
464
-
465
- runner = effect(update);
466
- trackEffect(wrapper, runner);
467
- childElements.push(child);
468
- } else {
469
- // Reactive text node for primitives
470
- const textNode = document.createTextNode('');
471
- targetNode.appendChild(textNode);
472
- const runner = effect(() => {
473
- const val = child();
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
- if (location === 'shadow') {
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 === 'afterbegin' || location === 'afterend') {
603
- array.reverse();
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.flat());
645
+ return element(customTags[tag] || tag, attributes, children);
630
646
  };
631
647
 
632
648
  // Lift static methods/properties from the component onto the wrapper
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "A lightweight reactive UI library with features of Bau, Juris, and HTMX",
5
5
  "main": "lightview.js",
6
6
  "directories": {