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,4 +1,4 @@
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
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -11,14 +11,14 @@
11
11
  */
12
12
 
13
13
  const VERSION_INFO = {
14
- version: '2.0.8',
14
+ version: '2.0.10',
15
15
  name: 'bitwrench',
16
16
  description: 'A library for javascript UI functions.',
17
17
  license: 'BSD-2-Clause',
18
18
  homepage: 'http://deftio.com/bitwrench',
19
19
  repository: 'git+https://github.com/deftio/bitwrench.git',
20
20
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
21
- buildDate: '2026-03-06T17:39:43.301Z'
21
+ buildDate: '2026-03-07T03:14:16.606Z'
22
22
  };
23
23
 
24
24
  /**
@@ -3669,6 +3669,27 @@
3669
3669
  _unmountCallbacks: new Map(),
3670
3670
  _topics: {}, // topic → [{handler, id}] (plain object for IE11 compat)
3671
3671
  _subIdCounter: 0, // monotonic ID for subscriptions
3672
+
3673
+ // ── Node reference cache ──────────────────────────────────────────────
3674
+ // Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
3675
+ //
3676
+ // Populated by bw.createDOM() when elements have:
3677
+ // - data-bw-id attribute (user-declared addressable elements)
3678
+ // - id attribute (standard HTML id)
3679
+ // - bw_uuid (internal, for lifecycle-managed elements)
3680
+ //
3681
+ // Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
3682
+ // On cache miss, falls back to querySelector/getElementById — never fails,
3683
+ // just slower. Stale entries (refs to detached nodes) are removed on miss
3684
+ // via parentNode === null check (IE11-safe, unlike el.isConnected).
3685
+ //
3686
+ // Elements created via bw.createDOM() also get el._bw_refs — a local map of
3687
+ // child bw_id → DOM node ref for fast parent→child access in o.render.
3688
+ // This is the bitwrench equivalent of React's compiled template "holes".
3689
+ //
3690
+ // Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
3691
+ // map entries may linger until the next lookup attempt cleans them.
3692
+ _nodeMap: {},
3672
3693
 
3673
3694
  // Monkey patch for testing (same as v1)
3674
3695
  __monkey_patch_is_nodejs__: {
@@ -3899,6 +3920,108 @@
3899
3920
  return `${tag}${timestamp}_${counter}_${random}`;
3900
3921
  };
3901
3922
 
3923
+ /**
3924
+ * Look up a DOM element by ID string, using the node cache for O(1) access.
3925
+ *
3926
+ * Resolution order:
3927
+ * 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
3928
+ * 2. If cached ref is detached (parentNode === null), remove stale entry
3929
+ * 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
3930
+ * 4. If fallback finds the element, cache it for next time
3931
+ * 5. If not found anywhere, return null
3932
+ *
3933
+ * Accepts a DOM element directly (pass-through) or a string identifier.
3934
+ * String identifiers are tried as: direct map key, getElementById,
3935
+ * querySelector (for CSS selectors starting with . or #), and
3936
+ * data-bw-id attribute selector.
3937
+ *
3938
+ * @param {string|Element} id - Element ID, CSS selector, data-bw-id value, or DOM element
3939
+ * @returns {Element|null} The DOM element, or null if not found
3940
+ * @category Internal
3941
+ */
3942
+ bw._el = function(id) {
3943
+ // Pass-through for DOM elements
3944
+ if (typeof id !== 'string') return id || null;
3945
+ if (!id) return null;
3946
+ if (!bw._isBrowser) return null;
3947
+
3948
+ // 1. Check cache
3949
+ var cached = bw._nodeMap[id];
3950
+ if (cached) {
3951
+ // Verify not detached (parentNode check is IE11-safe)
3952
+ if (cached.parentNode !== null) {
3953
+ return cached;
3954
+ }
3955
+ // Stale — remove and fall through
3956
+ delete bw._nodeMap[id];
3957
+ }
3958
+
3959
+ // 2. DOM fallback: try getElementById first (fastest native lookup)
3960
+ var el = document.getElementById(id);
3961
+
3962
+ // 3. Try querySelector for CSS selectors (starts with # or .)
3963
+ if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
3964
+ el = document.querySelector(id);
3965
+ }
3966
+
3967
+ // 4. Try data-bw-id attribute (for bw.uuid-generated IDs)
3968
+ if (!el) {
3969
+ el = document.querySelector('[data-bw-id="' + id + '"]');
3970
+ }
3971
+
3972
+ // 5. Cache the result for next time
3973
+ if (el) {
3974
+ bw._nodeMap[id] = el;
3975
+ }
3976
+
3977
+ return el;
3978
+ };
3979
+
3980
+ /**
3981
+ * Register a DOM element in the node cache under one or more keys.
3982
+ *
3983
+ * Called internally by `bw.createDOM()`. Registers elements that have
3984
+ * id attributes, data-bw-id attributes, or both.
3985
+ *
3986
+ * @param {Element} el - DOM element to register
3987
+ * @param {string} [bwId] - data-bw-id value to register under
3988
+ * @category Internal
3989
+ */
3990
+ bw._registerNode = function(el, bwId) {
3991
+ if (!el) return;
3992
+ // Register under data-bw-id
3993
+ if (bwId) {
3994
+ bw._nodeMap[bwId] = el;
3995
+ }
3996
+ // Register under id attribute
3997
+ var htmlId = el.getAttribute ? el.getAttribute('id') : null;
3998
+ if (htmlId) {
3999
+ bw._nodeMap[htmlId] = el;
4000
+ }
4001
+ };
4002
+
4003
+ /**
4004
+ * Remove a DOM element from the node cache.
4005
+ *
4006
+ * Called internally by `bw.cleanup()` when elements are destroyed
4007
+ * through bitwrench APIs.
4008
+ *
4009
+ * @param {Element} el - DOM element to deregister
4010
+ * @param {string} [bwId] - data-bw-id value to remove
4011
+ * @category Internal
4012
+ */
4013
+ bw._deregisterNode = function(el, bwId) {
4014
+ // Remove data-bw-id entry
4015
+ if (bwId) {
4016
+ delete bw._nodeMap[bwId];
4017
+ }
4018
+ // Remove id attribute entry
4019
+ var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
4020
+ if (htmlId) {
4021
+ delete bw._nodeMap[htmlId];
4022
+ }
4023
+ };
4024
+
3902
4025
  /**
3903
4026
  * Escape HTML special characters to prevent XSS.
3904
4027
  *
@@ -4123,26 +4246,66 @@
4123
4246
  }
4124
4247
  }
4125
4248
 
4126
- // Add children
4249
+ // Add children, building _bw_refs for fast parent→child access.
4250
+ // Children with data-bw-id or id attributes get local refs on the parent,
4251
+ // so o.render functions can access them without any DOM lookup.
4127
4252
  if (content != null) {
4128
4253
  if (Array.isArray(content)) {
4129
4254
  content.forEach(child => {
4130
4255
  if (child != null) {
4131
- el.appendChild(bw.createDOM(child, options));
4256
+ var childEl = bw.createDOM(child, options);
4257
+ el.appendChild(childEl);
4258
+ // Build local refs for addressable children
4259
+ var childBwId = (child && child.a) ? (child.a['data-bw-id'] || child.a.id) : null;
4260
+ if (childBwId) {
4261
+ if (!el._bw_refs) el._bw_refs = {};
4262
+ el._bw_refs[childBwId] = childEl;
4263
+ }
4264
+ // Bubble up grandchild refs (flatten one level)
4265
+ if (childEl._bw_refs) {
4266
+ if (!el._bw_refs) el._bw_refs = {};
4267
+ for (var rk in childEl._bw_refs) {
4268
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4269
+ el._bw_refs[rk] = childEl._bw_refs[rk];
4270
+ }
4271
+ }
4272
+ }
4132
4273
  }
4133
4274
  });
4134
4275
  } else if (typeof content === 'object' && content.t) {
4135
- el.appendChild(bw.createDOM(content, options));
4276
+ var childEl = bw.createDOM(content, options);
4277
+ el.appendChild(childEl);
4278
+ var childBwId = content.a ? (content.a['data-bw-id'] || content.a.id) : null;
4279
+ if (childBwId) {
4280
+ if (!el._bw_refs) el._bw_refs = {};
4281
+ el._bw_refs[childBwId] = childEl;
4282
+ }
4283
+ if (childEl._bw_refs) {
4284
+ if (!el._bw_refs) el._bw_refs = {};
4285
+ for (var rk in childEl._bw_refs) {
4286
+ if (Object.prototype.hasOwnProperty.call(childEl._bw_refs, rk)) {
4287
+ el._bw_refs[rk] = childEl._bw_refs[rk];
4288
+ }
4289
+ }
4290
+ }
4136
4291
  } else {
4137
4292
  el.textContent = String(content);
4138
4293
  }
4139
4294
  }
4140
-
4295
+
4296
+ // Register element in node cache if it has an id attribute
4297
+ if (attrs.id) {
4298
+ bw._registerNode(el, null);
4299
+ }
4300
+
4141
4301
  // Handle lifecycle hooks and state
4142
4302
  if (opts.mounted || opts.unmount || opts.render || opts.state) {
4143
4303
  const id = attrs['data-bw-id'] || bw.uuid();
4144
4304
  el.setAttribute('data-bw-id', id);
4145
4305
 
4306
+ // Register in node cache under data-bw-id
4307
+ bw._registerNode(el, id);
4308
+
4146
4309
  // Store state
4147
4310
  if (opts.state) {
4148
4311
  el._bw_state = opts.state;
@@ -4185,8 +4348,11 @@
4185
4348
  opts.unmount(el, el._bw_state || {});
4186
4349
  });
4187
4350
  }
4351
+ } else if (attrs['data-bw-id']) {
4352
+ // Element has explicit data-bw-id but no lifecycle hooks — still register it
4353
+ bw._registerNode(el, attrs['data-bw-id']);
4188
4354
  }
4189
-
4355
+
4190
4356
  return el;
4191
4357
  };
4192
4358
 
@@ -4219,10 +4385,8 @@
4219
4385
  throw new Error('bw.DOM requires a DOM environment (document/window). Use bw.html() instead.');
4220
4386
  }
4221
4387
 
4222
- // Get target element
4223
- const targetEl = typeof target === 'string'
4224
- ? document.querySelector(target)
4225
- : target;
4388
+ // Get target element (use cache-backed lookup)
4389
+ const targetEl = bw._el(target);
4226
4390
 
4227
4391
  if (!targetEl) {
4228
4392
  console.error('bw.DOM: Target element not found:', target);
@@ -4245,7 +4409,11 @@
4245
4409
  // Restore the target's own state/render/subs after cleanup
4246
4410
  if (savedState !== undefined) targetEl._bw_state = savedState;
4247
4411
  if (savedRender) targetEl._bw_render = savedRender;
4248
- if (savedBwId) targetEl.setAttribute('data-bw-id', savedBwId);
4412
+ if (savedBwId) {
4413
+ targetEl.setAttribute('data-bw-id', savedBwId);
4414
+ // Re-register mount point in node cache (cleanup deregistered it)
4415
+ bw._registerNode(targetEl, savedBwId);
4416
+ }
4249
4417
  if (savedSubs) targetEl._bw_subs = savedSubs;
4250
4418
 
4251
4419
  // Clear and mount new content
@@ -4508,15 +4676,19 @@
4508
4676
  bw._unmountCallbacks.delete(id);
4509
4677
  }
4510
4678
 
4679
+ // Deregister from node cache
4680
+ bw._deregisterNode(el, id);
4681
+
4511
4682
  // Clean up pub/sub subscriptions tied to this element
4512
4683
  if (el._bw_subs) {
4513
4684
  el._bw_subs.forEach(function(unsub) { unsub(); });
4514
4685
  delete el._bw_subs;
4515
4686
  }
4516
4687
 
4517
- // Clean up state and render
4688
+ // Clean up state, render, and local refs
4518
4689
  delete el._bw_state;
4519
4690
  delete el._bw_render;
4691
+ delete el._bw_refs;
4520
4692
  });
4521
4693
 
4522
4694
  // Check element itself
@@ -4527,6 +4699,10 @@
4527
4699
  callback();
4528
4700
  bw._unmountCallbacks.delete(id);
4529
4701
  }
4702
+
4703
+ // Deregister from node cache
4704
+ bw._deregisterNode(element, id);
4705
+
4530
4706
  // Clean up pub/sub subscriptions tied to element itself
4531
4707
  if (element._bw_subs) {
4532
4708
  element._bw_subs.forEach(function(unsub) { unsub(); });
@@ -4534,6 +4710,7 @@
4534
4710
  }
4535
4711
  delete element._bw_state;
4536
4712
  delete element._bw_render;
4713
+ delete element._bw_refs;
4537
4714
  }
4538
4715
  };
4539
4716
 
@@ -4548,7 +4725,7 @@
4548
4725
  * Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
4549
4726
  * components can react without tight coupling.
4550
4727
  *
4551
- * @param {string|Element} target - CSS selector or DOM element with _bw_render
4728
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element
4552
4729
  * @returns {Element|null} The element, or null if not found / no render function
4553
4730
  * @category State Management
4554
4731
  * @see bw.patch
@@ -4558,7 +4735,7 @@
4558
4735
  * bw.update(el); // re-renders, emits bw:statechange
4559
4736
  */
4560
4737
  bw.update = function(target) {
4561
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4738
+ var el = bw._el(target);
4562
4739
  if (el && el._bw_render) {
4563
4740
  el._bw_render(el, el._bw_state || {});
4564
4741
  bw.emit(el, 'statechange', el._bw_state);
@@ -4573,7 +4750,8 @@
4573
4750
  * Use `bw.patch()` for lightweight value updates (scores, labels, counters)
4574
4751
  * and `bw.update()` for full structural re-renders.
4575
4752
  *
4576
- * @param {string|Element} id - Element ID string or DOM element
4753
+ * @param {string|Element} id - Element ID, data-bw-id, CSS selector, or DOM element.
4754
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4577
4755
  * @param {string|Object} content - New text content, or TACO object to replace children
4578
4756
  * @param {string} [attr] - If provided, sets this attribute instead of content
4579
4757
  * @returns {Element|null} The patched element, or null if not found
@@ -4586,7 +4764,7 @@
4586
4764
  * bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
4587
4765
  */
4588
4766
  bw.patch = function(id, content, attr) {
4589
- var el = typeof id === 'string' ? document.getElementById(id) : id;
4767
+ var el = bw._el(id);
4590
4768
  if (!el) return null;
4591
4769
 
4592
4770
  if (attr) {
@@ -4637,7 +4815,8 @@
4637
4815
  * bubble by default so ancestor elements can listen. Use with `bw.on()` for
4638
4816
  * DOM-scoped communication between components.
4639
4817
  *
4640
- * @param {string|Element} target - CSS selector or DOM element
4818
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
4819
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4641
4820
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4642
4821
  * @param {*} [detail] - Data to pass with the event
4643
4822
  * @category Events (DOM)
@@ -4647,7 +4826,7 @@
4647
4826
  * // Dispatches CustomEvent 'bw:statechange' on the element
4648
4827
  */
4649
4828
  bw.emit = function(target, eventName, detail) {
4650
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4829
+ var el = bw._el(target);
4651
4830
  if (el) {
4652
4831
  el.dispatchEvent(new CustomEvent('bw:' + eventName, {
4653
4832
  bubbles: true,
@@ -4663,7 +4842,8 @@
4663
4842
  * is the first argument so you don't need to destructure `e.detail`.
4664
4843
  * Events bubble, so you can listen on an ancestor element.
4665
4844
  *
4666
- * @param {string|Element} target - CSS selector or DOM element
4845
+ * @param {string|Element} target - Element ID, data-bw-id, CSS selector, or DOM element.
4846
+ * Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
4667
4847
  * @param {string} eventName - Event name (will be prefixed with 'bw:')
4668
4848
  * @param {Function} handler - Called with (detail, event)
4669
4849
  * @returns {Element|null} The element (for chaining), or null if not found
@@ -4675,7 +4855,7 @@
4675
4855
  * });
4676
4856
  */
4677
4857
  bw.on = function(target, eventName, handler) {
4678
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4858
+ var el = bw._el(target);
4679
4859
  if (el) {
4680
4860
  el.addEventListener('bw:' + eventName, function(e) {
4681
4861
  handler(e.detail, e);