bitwrench 2.0.8 → 2.0.10

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/src/bitwrench.js CHANGED
@@ -44,6 +44,27 @@ const bw = {
44
44
  _unmountCallbacks: new Map(),
45
45
  _topics: {}, // topic → [{handler, id}] (plain object for IE11 compat)
46
46
  _subIdCounter: 0, // monotonic ID for subscriptions
47
+
48
+ // ── Node reference cache ──────────────────────────────────────────────
49
+ // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
50
+ //
51
+ // Populated by bw.createDOM() when elements have:
52
+ // - data-bw-id attribute (user-declared addressable elements)
53
+ // - id attribute (standard HTML id)
54
+ // - bw_uuid (internal, for lifecycle-managed elements)
55
+ //
56
+ // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
57
+ // On cache miss, falls back to querySelector/getElementById — never fails,
58
+ // just slower. Stale entries (refs to detached nodes) are removed on miss
59
+ // via parentNode === null check (IE11-safe, unlike el.isConnected).
60
+ //
61
+ // Elements created via bw.createDOM() also get el._bw_refs — a local map of
62
+ // child bw_id → DOM node ref for fast parent→child access in o.render.
63
+ // This is the bitwrench equivalent of React's compiled template "holes".
64
+ //
65
+ // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
66
+ // map entries may linger until the next lookup attempt cleans them.
67
+ _nodeMap: {},
47
68
 
48
69
  // Monkey patch for testing (same as v1)
49
70
  __monkey_patch_is_nodejs__: {
@@ -274,6 +295,108 @@ bw.uuid = function(prefix) {
274
295
  return `${tag}${timestamp}_${counter}_${random}`;
275
296
  };
276
297
 
298
+ /**
299
+ * Look up a DOM element by ID string, using the node cache for O(1) access.
300
+ *
301
+ * Resolution order:
302
+ * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
303
+ * 2. If cached ref is detached (parentNode === null), remove stale entry
304
+ * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
305
+ * 4. If fallback finds the element, cache it for next time
306
+ * 5. If not found anywhere, return null
307
+ *
308
+ * Accepts a DOM element directly (pass-through) or a string identifier.
309
+ * String identifiers are tried as: direct map key, getElementById,
310
+ * querySelector (for CSS selectors starting with . or #), and
311
+ * data-bw-id attribute selector.
312
+ *
313
+ * @param {string|Element} id - Element ID, CSS selector, data-bw-id value, or DOM element
314
+ * @returns {Element|null} The DOM element, or null if not found
315
+ * @category Internal
316
+ */
317
+ bw._el = function(id) {
318
+ // Pass-through for DOM elements
319
+ if (typeof id !== 'string') return id || null;
320
+ if (!id) return null;
321
+ if (!bw._isBrowser) return null;
322
+
323
+ // 1. Check cache
324
+ var cached = bw._nodeMap[id];
325
+ if (cached) {
326
+ // Verify not detached (parentNode check is IE11-safe)
327
+ if (cached.parentNode !== null) {
328
+ return cached;
329
+ }
330
+ // Stale — remove and fall through
331
+ delete bw._nodeMap[id];
332
+ }
333
+
334
+ // 2. DOM fallback: try getElementById first (fastest native lookup)
335
+ var el = document.getElementById(id);
336
+
337
+ // 3. Try querySelector for CSS selectors (starts with # or .)
338
+ if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
339
+ el = document.querySelector(id);
340
+ }
341
+
342
+ // 4. Try data-bw-id attribute (for bw.uuid-generated IDs)
343
+ if (!el) {
344
+ el = document.querySelector('[data-bw-id="' + id + '"]');
345
+ }
346
+
347
+ // 5. Cache the result for next time
348
+ if (el) {
349
+ bw._nodeMap[id] = el;
350
+ }
351
+
352
+ return el;
353
+ };
354
+
355
+ /**
356
+ * Register a DOM element in the node cache under one or more keys.
357
+ *
358
+ * Called internally by `bw.createDOM()`. Registers elements that have
359
+ * id attributes, data-bw-id attributes, or both.
360
+ *
361
+ * @param {Element} el - DOM element to register
362
+ * @param {string} [bwId] - data-bw-id value to register under
363
+ * @category Internal
364
+ */
365
+ bw._registerNode = function(el, bwId) {
366
+ if (!el) return;
367
+ // Register under data-bw-id
368
+ if (bwId) {
369
+ bw._nodeMap[bwId] = el;
370
+ }
371
+ // Register under id attribute
372
+ var htmlId = el.getAttribute ? el.getAttribute('id') : null;
373
+ if (htmlId) {
374
+ bw._nodeMap[htmlId] = el;
375
+ }
376
+ };
377
+
378
+ /**
379
+ * Remove a DOM element from the node cache.
380
+ *
381
+ * Called internally by `bw.cleanup()` when elements are destroyed
382
+ * through bitwrench APIs.
383
+ *
384
+ * @param {Element} el - DOM element to deregister
385
+ * @param {string} [bwId] - data-bw-id value to remove
386
+ * @category Internal
387
+ */
388
+ bw._deregisterNode = function(el, bwId) {
389
+ // Remove data-bw-id entry
390
+ if (bwId) {
391
+ delete bw._nodeMap[bwId];
392
+ }
393
+ // Remove id attribute entry
394
+ var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
395
+ if (htmlId) {
396
+ delete bw._nodeMap[htmlId];
397
+ }
398
+ };
399
+
277
400
  /**
278
401
  * Escape HTML special characters to prevent XSS.
279
402
  *
@@ -498,26 +621,66 @@ bw.createDOM = function(taco, options = {}) {
498
621
  }
499
622
  }
500
623
 
501
- // Add children
624
+ // Add children, building _bw_refs for fast parent→child access.
625
+ // Children with data-bw-id or id attributes get local refs on the parent,
626
+ // so o.render functions can access them without any DOM lookup.
502
627
  if (content != null) {
503
628
  if (Array.isArray(content)) {
504
629
  content.forEach(child => {
505
630
  if (child != null) {
506
- el.appendChild(bw.createDOM(child, options));
631
+ var childEl = bw.createDOM(child, options);
632
+ el.appendChild(childEl);
633
+ // Build local refs for addressable children
634
+ var childBwId = (child && child.a) ? (child.a['data-bw-id'] || child.a.id) : null;
635
+ if (childBwId) {
636
+ if (!el._bw_refs) el._bw_refs = {};
637
+ el._bw_refs[childBwId] = childEl;
638
+ }
639
+ // Bubble up grandchild refs (flatten one level)
640
+ if (childEl._bw_refs) {
641
+ if (!el._bw_refs) el._bw_refs = {};
642
+ for (var rk in childEl._bw_refs) {
643
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
644
+ el._bw_refs[rk] = childEl._bw_refs[rk];
645
+ }
646
+ }
647
+ }
507
648
  }
508
649
  });
509
650
  } else if (typeof content === 'object' && content.t) {
510
- el.appendChild(bw.createDOM(content, options));
651
+ var childEl = bw.createDOM(content, options);
652
+ el.appendChild(childEl);
653
+ var childBwId = content.a ? (content.a['data-bw-id'] || content.a.id) : null;
654
+ if (childBwId) {
655
+ if (!el._bw_refs) el._bw_refs = {};
656
+ el._bw_refs[childBwId] = childEl;
657
+ }
658
+ if (childEl._bw_refs) {
659
+ if (!el._bw_refs) el._bw_refs = {};
660
+ for (var rk in childEl._bw_refs) {
661
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
662
+ el._bw_refs[rk] = childEl._bw_refs[rk];
663
+ }
664
+ }
665
+ }
511
666
  } else {
512
667
  el.textContent = String(content);
513
668
  }
514
669
  }
515
-
670
+
671
+ // Register element in node cache if it has an id attribute
672
+ if (attrs.id) {
673
+ bw._registerNode(el, null);
674
+ }
675
+
516
676
  // Handle lifecycle hooks and state
517
677
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
518
678
  const id = attrs['data-bw-id'] || bw.uuid();
519
679
  el.setAttribute('data-bw-id', id);
520
680
 
681
+ // Register in node cache under data-bw-id
682
+ bw._registerNode(el, id);
683
+
521
684
  // Store state
522
685
  if (opts.state) {
523
686
  el._bw_state = opts.state;
@@ -560,8 +723,11 @@ bw.createDOM = function(taco, options = {}) {
560
723
  opts.unmount(el, el._bw_state || {});
561
724
  });
562
725
  }
726
+ } else if (attrs['data-bw-id']) {
727
+ // Element has explicit data-bw-id but no lifecycle hooks — still register it
728
+ bw._registerNode(el, attrs['data-bw-id']);
563
729
  }
564
-
730
+
565
731
  return el;
566
732
  };
567
733
 
@@ -594,10 +760,8 @@ bw.DOM = function(target, taco, options = {}) {
594
760
  throw new Error('bw.DOM requires a DOM environment (document/window). Use bw.html() instead.');
595
761
  }
596
762
 
597
- // Get target element
598
- const targetEl = typeof target === 'string'
599
- ? document.querySelector(target)
600
- : target;
763
+ // Get target element (use cache-backed lookup)
764
+ const targetEl = bw._el(target);
601
765
 
602
766
  if (!targetEl) {
603
767
  console.error('bw.DOM: Target element not found:', target);
@@ -620,7 +784,11 @@ bw.DOM = function(target, taco, options = {}) {
620
784
  // Restore the target's own state/render/subs after cleanup
621
785
  if (savedState !== undefined) targetEl._bw_state = savedState;
622
786
  if (savedRender) targetEl._bw_render = savedRender;
623
- if (savedBwId) targetEl.setAttribute('data-bw-id', savedBwId);
787
+ if (savedBwId) {
788
+ targetEl.setAttribute('data-bw-id', savedBwId);
789
+ // Re-register mount point in node cache (cleanup deregistered it)
790
+ bw._registerNode(targetEl, savedBwId);
791
+ }
624
792
  if (savedSubs) targetEl._bw_subs = savedSubs;
625
793
 
626
794
  // Clear and mount new content
@@ -883,15 +1051,19 @@ bw.cleanup = function(element) {
883
1051
  bw._unmountCallbacks.delete(id);
884
1052
  }
885
1053
 
1054
+ // Deregister from node cache
1055
+ bw._deregisterNode(el, id);
1056
+
886
1057
  // Clean up pub/sub subscriptions tied to this element
887
1058
  if (el._bw_subs) {
888
1059
  el._bw_subs.forEach(function(unsub) { unsub(); });
889
1060
  delete el._bw_subs;
890
1061
  }
891
1062
 
892
- // Clean up state and render
1063
+ // Clean up state, render, and local refs
893
1064
  delete el._bw_state;
894
1065
  delete el._bw_render;
1066
+ delete el._bw_refs;
895
1067
  });
896
1068
 
897
1069
  // Check element itself
@@ -902,6 +1074,10 @@ bw.cleanup = function(element) {
902
1074
  callback();
903
1075
  bw._unmountCallbacks.delete(id);
904
1076
  }
1077
+
1078
+ // Deregister from node cache
1079
+ bw._deregisterNode(element, id);
1080
+
905
1081
  // Clean up pub/sub subscriptions tied to element itself
906
1082
  if (element._bw_subs) {
907
1083
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -909,6 +1085,7 @@ bw.cleanup = function(element) {
909
1085
  }
910
1086
  delete element._bw_state;
911
1087
  delete element._bw_render;
1088
+ delete element._bw_refs;
912
1089
  }
913
1090
  };
914
1091
 
@@ -923,7 +1100,7 @@ bw.cleanup = function(element) {
923
1100
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
924
1101
  * components can react without tight coupling.
925
1102
  *
926
- * @param {string|Element} target - CSS selector or DOM element with _bw_render
1103
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element
927
1104
  * @returns {Element|null} The element, or null if not found / no render function
928
1105
  * @category State Management
929
1106
  * @see bw.patch
@@ -933,7 +1110,7 @@ bw.cleanup = function(element) {
933
1110
  * bw.update(el); // re-renders, emits bw:statechange
934
1111
  */
935
1112
  bw.update = function(target) {
936
- var el = typeof target === 'string' ? document.querySelector(target) : target;
1113
+ var el = bw._el(target);
937
1114
  if (el && el._bw_render) {
938
1115
  el._bw_render(el, el._bw_state || {});
939
1116
  bw.emit(el, 'statechange', el._bw_state);
@@ -948,7 +1125,8 @@ bw.update = function(target) {
948
1125
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
949
1126
  * and `bw.update()` for full structural re-renders.
950
1127
  *
951
- * @param {string|Element} id - Element ID string or DOM element
1128
+ * @param {string|Element} id - Element ID, data-bw-id, CSS selector, or DOM element.
1129
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
952
1130
  * @param {string|Object} content - New text content, or TACO object to replace children
953
1131
  * @param {string} [attr] - If provided, sets this attribute instead of content
954
1132
  * @returns {Element|null} The patched element, or null if not found
@@ -961,7 +1139,7 @@ bw.update = function(target) {
961
1139
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
962
1140
  */
963
1141
  bw.patch = function(id, content, attr) {
964
- var el = typeof id === 'string' ? document.getElementById(id) : id;
1142
+ var el = bw._el(id);
965
1143
  if (!el) return null;
966
1144
 
967
1145
  if (attr) {
@@ -1012,7 +1190,8 @@ bw.patchAll = function(patches) {
1012
1190
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
1013
1191
  * DOM-scoped communication between components.
1014
1192
  *
1015
- * @param {string|Element} target - CSS selector or DOM element
1193
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
1194
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1016
1195
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1017
1196
  * @param {*} [detail] - Data to pass with the event
1018
1197
  * @category Events (DOM)
@@ -1022,7 +1201,7 @@ bw.patchAll = function(patches) {
1022
1201
  * // Dispatches CustomEvent 'bw:statechange' on the element
1023
1202
  */
1024
1203
  bw.emit = function(target, eventName, detail) {
1025
- var el = typeof target === 'string' ? document.querySelector(target) : target;
1204
+ var el = bw._el(target);
1026
1205
  if (el) {
1027
1206
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
1028
1207
  bubbles: true,
@@ -1038,7 +1217,8 @@ bw.emit = function(target, eventName, detail) {
1038
1217
  * is the first argument so you don't need to destructure `e.detail`.
1039
1218
  * Events bubble, so you can listen on an ancestor element.
1040
1219
  *
1041
- * @param {string|Element} target - CSS selector or DOM element
1220
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
1221
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
1042
1222
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
1043
1223
  * @param {Function} handler - Called with (detail, event)
1044
1224
  * @returns {Element|null} The element (for chaining), or null if not found
@@ -1050,7 +1230,7 @@ bw.emit = function(target, eventName, detail) {
1050
1230
  * });
1051
1231
  */
1052
1232
  bw.on = function(target, eventName, handler) {
1053
- var el = typeof target === 'string' ? document.querySelector(target) : target;
1233
+ var el = bw._el(target);
1054
1234
  if (el) {
1055
1235
  el.addEventListener('bw:' + eventName, function(e) {
1056
1236
  handler(e.detail, e);
package/src/version.js CHANGED
@@ -3,14 +3,14 @@
3
3
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
4
4
  */
5
5
 
6
- export const VERSION = '2.0.8';
6
+ export const VERSION = '2.0.10';
7
7
  export const VERSION_INFO = {
8
- version: '2.0.8',
8
+ version: '2.0.10',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'http://deftio.com/bitwrench',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-06T17:39:43.301Z'
15
+ buildDate: '2026-03-07T03:14:16.606Z'
16
16
  };