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.
@@ -1,18 +1,18 @@
1
- /*! bitwrench v2.0.8 | BSD-2-Clause | http://deftio.com/bitwrench */
1
+ /*! bitwrench v2.0.10 | BSD-2-Clause | http://deftio.com/bitwrench */
2
2
  /**
3
3
  * Auto-generated version file from package.json
4
4
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
5
5
  */
6
6
 
7
7
  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
  };
17
17
 
18
18
  /**
@@ -3663,6 +3663,27 @@ const bw = {
3663
3663
  _unmountCallbacks: new Map(),
3664
3664
  _topics: {}, // topic → [{handler, id}] (plain object for IE11 compat)
3665
3665
  _subIdCounter: 0, // monotonic ID for subscriptions
3666
+
3667
+ // ── Node reference cache ──────────────────────────────────────────────
3668
+ // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
3669
+ //
3670
+ // Populated by bw.createDOM() when elements have:
3671
+ // - data-bw-id attribute (user-declared addressable elements)
3672
+ // - id attribute (standard HTML id)
3673
+ // - bw_uuid (internal, for lifecycle-managed elements)
3674
+ //
3675
+ // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
3676
+ // On cache miss, falls back to querySelector/getElementById — never fails,
3677
+ // just slower. Stale entries (refs to detached nodes) are removed on miss
3678
+ // via parentNode === null check (IE11-safe, unlike el.isConnected).
3679
+ //
3680
+ // Elements created via bw.createDOM() also get el._bw_refs — a local map of
3681
+ // child bw_id → DOM node ref for fast parent→child access in o.render.
3682
+ // This is the bitwrench equivalent of React's compiled template "holes".
3683
+ //
3684
+ // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
3685
+ // map entries may linger until the next lookup attempt cleans them.
3686
+ _nodeMap: {},
3666
3687
 
3667
3688
  // Monkey patch for testing (same as v1)
3668
3689
  __monkey_patch_is_nodejs__: {
@@ -3893,6 +3914,108 @@ bw.uuid = function(prefix) {
3893
3914
  return `${tag}${timestamp}_${counter}_${random}`;
3894
3915
  };
3895
3916
 
3917
+ /**
3918
+ * Look up a DOM element by ID string, using the node cache for O(1) access.
3919
+ *
3920
+ * Resolution order:
3921
+ * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
3922
+ * 2. If cached ref is detached (parentNode === null), remove stale entry
3923
+ * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
3924
+ * 4. If fallback finds the element, cache it for next time
3925
+ * 5. If not found anywhere, return null
3926
+ *
3927
+ * Accepts a DOM element directly (pass-through) or a string identifier.
3928
+ * String identifiers are tried as: direct map key, getElementById,
3929
+ * querySelector (for CSS selectors starting with . or #), and
3930
+ * data-bw-id attribute selector.
3931
+ *
3932
+ * @param {string|Element} id - Element ID, CSS selector, data-bw-id value, or DOM element
3933
+ * @returns {Element|null} The DOM element, or null if not found
3934
+ * @category Internal
3935
+ */
3936
+ bw._el = function(id) {
3937
+ // Pass-through for DOM elements
3938
+ if (typeof id !== 'string') return id || null;
3939
+ if (!id) return null;
3940
+ if (!bw._isBrowser) return null;
3941
+
3942
+ // 1. Check cache
3943
+ var cached = bw._nodeMap[id];
3944
+ if (cached) {
3945
+ // Verify not detached (parentNode check is IE11-safe)
3946
+ if (cached.parentNode !== null) {
3947
+ return cached;
3948
+ }
3949
+ // Stale — remove and fall through
3950
+ delete bw._nodeMap[id];
3951
+ }
3952
+
3953
+ // 2. DOM fallback: try getElementById first (fastest native lookup)
3954
+ var el = document.getElementById(id);
3955
+
3956
+ // 3. Try querySelector for CSS selectors (starts with # or .)
3957
+ if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
3958
+ el = document.querySelector(id);
3959
+ }
3960
+
3961
+ // 4. Try data-bw-id attribute (for bw.uuid-generated IDs)
3962
+ if (!el) {
3963
+ el = document.querySelector('[data-bw-id="' + id + '"]');
3964
+ }
3965
+
3966
+ // 5. Cache the result for next time
3967
+ if (el) {
3968
+ bw._nodeMap[id] = el;
3969
+ }
3970
+
3971
+ return el;
3972
+ };
3973
+
3974
+ /**
3975
+ * Register a DOM element in the node cache under one or more keys.
3976
+ *
3977
+ * Called internally by `bw.createDOM()`. Registers elements that have
3978
+ * id attributes, data-bw-id attributes, or both.
3979
+ *
3980
+ * @param {Element} el - DOM element to register
3981
+ * @param {string} [bwId] - data-bw-id value to register under
3982
+ * @category Internal
3983
+ */
3984
+ bw._registerNode = function(el, bwId) {
3985
+ if (!el) return;
3986
+ // Register under data-bw-id
3987
+ if (bwId) {
3988
+ bw._nodeMap[bwId] = el;
3989
+ }
3990
+ // Register under id attribute
3991
+ var htmlId = el.getAttribute ? el.getAttribute('id') : null;
3992
+ if (htmlId) {
3993
+ bw._nodeMap[htmlId] = el;
3994
+ }
3995
+ };
3996
+
3997
+ /**
3998
+ * Remove a DOM element from the node cache.
3999
+ *
4000
+ * Called internally by `bw.cleanup()` when elements are destroyed
4001
+ * through bitwrench APIs.
4002
+ *
4003
+ * @param {Element} el - DOM element to deregister
4004
+ * @param {string} [bwId] - data-bw-id value to remove
4005
+ * @category Internal
4006
+ */
4007
+ bw._deregisterNode = function(el, bwId) {
4008
+ // Remove data-bw-id entry
4009
+ if (bwId) {
4010
+ delete bw._nodeMap[bwId];
4011
+ }
4012
+ // Remove id attribute entry
4013
+ var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
4014
+ if (htmlId) {
4015
+ delete bw._nodeMap[htmlId];
4016
+ }
4017
+ };
4018
+
3896
4019
  /**
3897
4020
  * Escape HTML special characters to prevent XSS.
3898
4021
  *
@@ -4117,26 +4240,66 @@ bw.createDOM = function(taco, options = {}) {
4117
4240
  }
4118
4241
  }
4119
4242
 
4120
- // Add children
4243
+ // Add children, building _bw_refs for fast parent→child access.
4244
+ // Children with data-bw-id or id attributes get local refs on the parent,
4245
+ // so o.render functions can access them without any DOM lookup.
4121
4246
  if (content != null) {
4122
4247
  if (Array.isArray(content)) {
4123
4248
  content.forEach(child => {
4124
4249
  if (child != null) {
4125
- el.appendChild(bw.createDOM(child, options));
4250
+ var childEl = bw.createDOM(child, options);
4251
+ el.appendChild(childEl);
4252
+ // Build local refs for addressable children
4253
+ var childBwId = (child && child.a) ? (child.a['data-bw-id'] || child.a.id) : null;
4254
+ if (childBwId) {
4255
+ if (!el._bw_refs) el._bw_refs = {};
4256
+ el._bw_refs[childBwId] = childEl;
4257
+ }
4258
+ // Bubble up grandchild refs (flatten one level)
4259
+ if (childEl._bw_refs) {
4260
+ if (!el._bw_refs) el._bw_refs = {};
4261
+ for (var rk in childEl._bw_refs) {
4262
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4263
+ el._bw_refs[rk] = childEl._bw_refs[rk];
4264
+ }
4265
+ }
4266
+ }
4126
4267
  }
4127
4268
  });
4128
4269
  } else if (typeof content === 'object' && content.t) {
4129
- el.appendChild(bw.createDOM(content, options));
4270
+ var childEl = bw.createDOM(content, options);
4271
+ el.appendChild(childEl);
4272
+ var childBwId = content.a ? (content.a['data-bw-id'] || content.a.id) : null;
4273
+ if (childBwId) {
4274
+ if (!el._bw_refs) el._bw_refs = {};
4275
+ el._bw_refs[childBwId] = childEl;
4276
+ }
4277
+ if (childEl._bw_refs) {
4278
+ if (!el._bw_refs) el._bw_refs = {};
4279
+ for (var rk in childEl._bw_refs) {
4280
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4281
+ el._bw_refs[rk] = childEl._bw_refs[rk];
4282
+ }
4283
+ }
4284
+ }
4130
4285
  } else {
4131
4286
  el.textContent = String(content);
4132
4287
  }
4133
4288
  }
4134
-
4289
+
4290
+ // Register element in node cache if it has an id attribute
4291
+ if (attrs.id) {
4292
+ bw._registerNode(el, null);
4293
+ }
4294
+
4135
4295
  // Handle lifecycle hooks and state
4136
4296
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4137
4297
  const id = attrs['data-bw-id'] || bw.uuid();
4138
4298
  el.setAttribute('data-bw-id', id);
4139
4299
 
4300
+ // Register in node cache under data-bw-id
4301
+ bw._registerNode(el, id);
4302
+
4140
4303
  // Store state
4141
4304
  if (opts.state) {
4142
4305
  el._bw_state = opts.state;
@@ -4179,8 +4342,11 @@ bw.createDOM = function(taco, options = {}) {
4179
4342
  opts.unmount(el, el._bw_state || {});
4180
4343
  });
4181
4344
  }
4345
+ } else if (attrs['data-bw-id']) {
4346
+ // Element has explicit data-bw-id but no lifecycle hooks — still register it
4347
+ bw._registerNode(el, attrs['data-bw-id']);
4182
4348
  }
4183
-
4349
+
4184
4350
  return el;
4185
4351
  };
4186
4352
 
@@ -4213,10 +4379,8 @@ bw.DOM = function(target, taco, options = {}) {
4213
4379
  throw new Error('bw.DOM requires a DOM environment (document/window). Use bw.html() instead.');
4214
4380
  }
4215
4381
 
4216
- // Get target element
4217
- const targetEl = typeof target === 'string'
4218
- ? document.querySelector(target)
4219
- : target;
4382
+ // Get target element (use cache-backed lookup)
4383
+ const targetEl = bw._el(target);
4220
4384
 
4221
4385
  if (!targetEl) {
4222
4386
  console.error('bw.DOM: Target element not found:', target);
@@ -4239,7 +4403,11 @@ bw.DOM = function(target, taco, options = {}) {
4239
4403
  // Restore the target's own state/render/subs after cleanup
4240
4404
  if (savedState !== undefined) targetEl._bw_state = savedState;
4241
4405
  if (savedRender) targetEl._bw_render = savedRender;
4242
- if (savedBwId) targetEl.setAttribute('data-bw-id', savedBwId);
4406
+ if (savedBwId) {
4407
+ targetEl.setAttribute('data-bw-id', savedBwId);
4408
+ // Re-register mount point in node cache (cleanup deregistered it)
4409
+ bw._registerNode(targetEl, savedBwId);
4410
+ }
4243
4411
  if (savedSubs) targetEl._bw_subs = savedSubs;
4244
4412
 
4245
4413
  // Clear and mount new content
@@ -4502,15 +4670,19 @@ bw.cleanup = function(element) {
4502
4670
  bw._unmountCallbacks.delete(id);
4503
4671
  }
4504
4672
 
4673
+ // Deregister from node cache
4674
+ bw._deregisterNode(el, id);
4675
+
4505
4676
  // Clean up pub/sub subscriptions tied to this element
4506
4677
  if (el._bw_subs) {
4507
4678
  el._bw_subs.forEach(function(unsub) { unsub(); });
4508
4679
  delete el._bw_subs;
4509
4680
  }
4510
4681
 
4511
- // Clean up state and render
4682
+ // Clean up state, render, and local refs
4512
4683
  delete el._bw_state;
4513
4684
  delete el._bw_render;
4685
+ delete el._bw_refs;
4514
4686
  });
4515
4687
 
4516
4688
  // Check element itself
@@ -4521,6 +4693,10 @@ bw.cleanup = function(element) {
4521
4693
  callback();
4522
4694
  bw._unmountCallbacks.delete(id);
4523
4695
  }
4696
+
4697
+ // Deregister from node cache
4698
+ bw._deregisterNode(element, id);
4699
+
4524
4700
  // Clean up pub/sub subscriptions tied to element itself
4525
4701
  if (element._bw_subs) {
4526
4702
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4528,6 +4704,7 @@ bw.cleanup = function(element) {
4528
4704
  }
4529
4705
  delete element._bw_state;
4530
4706
  delete element._bw_render;
4707
+ delete element._bw_refs;
4531
4708
  }
4532
4709
  };
4533
4710
 
@@ -4542,7 +4719,7 @@ bw.cleanup = function(element) {
4542
4719
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
4543
4720
  * components can react without tight coupling.
4544
4721
  *
4545
- * @param {string|Element} target - CSS selector or DOM element with _bw_render
4722
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element
4546
4723
  * @returns {Element|null} The element, or null if not found / no render function
4547
4724
  * @category State Management
4548
4725
  * @see bw.patch
@@ -4552,7 +4729,7 @@ bw.cleanup = function(element) {
4552
4729
  * bw.update(el); // re-renders, emits bw:statechange
4553
4730
  */
4554
4731
  bw.update = function(target) {
4555
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4732
+ var el = bw._el(target);
4556
4733
  if (el && el._bw_render) {
4557
4734
  el._bw_render(el, el._bw_state || {});
4558
4735
  bw.emit(el, 'statechange', el._bw_state);
@@ -4567,7 +4744,8 @@ bw.update = function(target) {
4567
4744
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
4568
4745
  * and `bw.update()` for full structural re-renders.
4569
4746
  *
4570
- * @param {string|Element} id - Element ID string or DOM element
4747
+ * @param {string|Element} id - Element ID, data-bw-id, CSS selector, or DOM element.
4748
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4571
4749
  * @param {string|Object} content - New text content, or TACO object to replace children
4572
4750
  * @param {string} [attr] - If provided, sets this attribute instead of content
4573
4751
  * @returns {Element|null} The patched element, or null if not found
@@ -4580,7 +4758,7 @@ bw.update = function(target) {
4580
4758
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
4581
4759
  */
4582
4760
  bw.patch = function(id, content, attr) {
4583
- var el = typeof id === 'string' ? document.getElementById(id) : id;
4761
+ var el = bw._el(id);
4584
4762
  if (!el) return null;
4585
4763
 
4586
4764
  if (attr) {
@@ -4631,7 +4809,8 @@ bw.patchAll = function(patches) {
4631
4809
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
4632
4810
  * DOM-scoped communication between components.
4633
4811
  *
4634
- * @param {string|Element} target - CSS selector or DOM element
4812
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
4813
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4635
4814
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4636
4815
  * @param {*} [detail] - Data to pass with the event
4637
4816
  * @category Events (DOM)
@@ -4641,7 +4820,7 @@ bw.patchAll = function(patches) {
4641
4820
  * // Dispatches CustomEvent 'bw:statechange' on the element
4642
4821
  */
4643
4822
  bw.emit = function(target, eventName, detail) {
4644
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4823
+ var el = bw._el(target);
4645
4824
  if (el) {
4646
4825
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
4647
4826
  bubbles: true,
@@ -4657,7 +4836,8 @@ bw.emit = function(target, eventName, detail) {
4657
4836
  * is the first argument so you don't need to destructure `e.detail`.
4658
4837
  * Events bubble, so you can listen on an ancestor element.
4659
4838
  *
4660
- * @param {string|Element} target - CSS selector or DOM element
4839
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
4840
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4661
4841
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4662
4842
  * @param {Function} handler - Called with (detail, event)
4663
4843
  * @returns {Element|null} The element (for chaining), or null if not found
@@ -4669,7 +4849,7 @@ bw.emit = function(target, eventName, detail) {
4669
4849
  * });
4670
4850
  */
4671
4851
  bw.on = function(target, eventName, handler) {
4672
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4852
+ var el = bw._el(target);
4673
4853
  if (el) {
4674
4854
  el.addEventListener('bw:' + eventName, function(e) {
4675
4855
  handler(e.detail, e);