zero-query 0.9.5 → 0.9.6

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/component.js CHANGED
@@ -198,7 +198,7 @@ class Component {
198
198
  const defaultSlotNodes = [];
199
199
  [...el.childNodes].forEach(node => {
200
200
  if (node.nodeType === 1 && node.hasAttribute('slot')) {
201
- const slotName = node.getAttribute('slot');
201
+ const slotName = node.getAttribute('slot') || 'default';
202
202
  if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
203
203
  this._slotContent[slotName] += node.outerHTML;
204
204
  } else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
@@ -607,6 +607,24 @@ class Component {
607
607
  for (const [event, bindings] of eventMap) {
608
608
  this._attachDelegatedEvent(event, bindings);
609
609
  }
610
+
611
+ // .outside — attach a document-level listener for bindings that need
612
+ // to detect clicks/events outside their element.
613
+ this._outsideListeners = this._outsideListeners || [];
614
+ for (const [event, bindings] of eventMap) {
615
+ for (const binding of bindings) {
616
+ if (!binding.modifiers.includes('outside')) continue;
617
+ const outsideHandler = (e) => {
618
+ if (binding.el.contains(e.target)) return;
619
+ const match = binding.methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
620
+ if (!match) return;
621
+ const fn = this[match[1]];
622
+ if (typeof fn === 'function') fn.call(this, e);
623
+ };
624
+ document.addEventListener(event, outsideHandler, true);
625
+ this._outsideListeners.push({ event, handler: outsideHandler });
626
+ }
627
+ }
610
628
  }
611
629
 
612
630
  // Attach a single delegated listener for an event type
@@ -651,6 +669,28 @@ class Component {
651
669
  // .self — only fire if target is the element itself
652
670
  if (modifiers.includes('self') && e.target !== el) continue;
653
671
 
672
+ // .outside — only fire if event target is OUTSIDE the element
673
+ if (modifiers.includes('outside')) {
674
+ if (el.contains(e.target)) continue;
675
+ }
676
+
677
+ // Key modifiers — filter keyboard events by key
678
+ const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
679
+ let keyFiltered = false;
680
+ for (const mod of modifiers) {
681
+ if (_keyMap[mod]) {
682
+ const keys = _keyMap[mod].split('|');
683
+ if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
684
+ }
685
+ }
686
+ if (keyFiltered) continue;
687
+
688
+ // System key modifiers — require modifier keys to be held
689
+ if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
690
+ if (modifiers.includes('shift') && !e.shiftKey) continue;
691
+ if (modifiers.includes('alt') && !e.altKey) continue;
692
+ if (modifiers.includes('meta') && !e.metaKey) continue;
693
+
654
694
  // Handle modifiers
655
695
  if (modifiers.includes('prevent')) e.preventDefault();
656
696
  if (modifiers.includes('stop')) {
@@ -736,9 +776,12 @@ class Component {
736
776
  // textarea, select (single & multiple), contenteditable
737
777
  // Nested state keys: z-model="user.name" → this.state.user.name
738
778
  // Modifiers (boolean attributes on the same element):
739
- // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
740
- // z-trim — trim whitespace before writing to state
741
- // z-number — force Number() conversion regardless of input type
779
+ // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
780
+ // z-trim — trim whitespace before writing to state
781
+ // z-number — force Number() conversion regardless of input type
782
+ // z-debounce — debounce state writes (default 250ms, or z-debounce="300")
783
+ // z-uppercase — convert string to uppercase before writing to state
784
+ // z-lowercase — convert string to lowercase before writing to state
742
785
  //
743
786
  // Writes to reactive state so the rest of the UI stays in sync.
744
787
  // Focus and cursor position are preserved in _render() via focusInfo.
@@ -754,6 +797,10 @@ class Component {
754
797
  const isLazy = el.hasAttribute('z-lazy');
755
798
  const isTrim = el.hasAttribute('z-trim');
756
799
  const isNum = el.hasAttribute('z-number');
800
+ const isUpper = el.hasAttribute('z-uppercase');
801
+ const isLower = el.hasAttribute('z-lowercase');
802
+ const hasDebounce = el.hasAttribute('z-debounce');
803
+ const debounceMs = hasDebounce ? (parseInt(el.getAttribute('z-debounce'), 10) || 250) : 0;
757
804
 
758
805
  // Read current state value (supports dot-path keys)
759
806
  const currentVal = _getPath(this.state, key);
@@ -794,6 +841,8 @@ class Component {
794
841
 
795
842
  // Apply modifiers
796
843
  if (isTrim && typeof val === 'string') val = val.trim();
844
+ if (isUpper && typeof val === 'string') val = val.toUpperCase();
845
+ if (isLower && typeof val === 'string') val = val.toLowerCase();
797
846
  if (isNum || type === 'number' || type === 'range') val = Number(val);
798
847
 
799
848
  // Write through the reactive proxy (triggers re-render).
@@ -801,7 +850,15 @@ class Component {
801
850
  _setPath(this.state, key, val);
802
851
  };
803
852
 
804
- el.addEventListener(event, handler);
853
+ if (hasDebounce) {
854
+ let timer = null;
855
+ el.addEventListener(event, () => {
856
+ clearTimeout(timer);
857
+ timer = setTimeout(handler, debounceMs);
858
+ });
859
+ } else {
860
+ el.addEventListener(event, handler);
861
+ }
805
862
  });
806
863
  }
807
864
 
@@ -1087,6 +1144,10 @@ class Component {
1087
1144
  }
1088
1145
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
1089
1146
  this._listeners = [];
1147
+ if (this._outsideListeners) {
1148
+ this._outsideListeners.forEach(({ event, handler }) => document.removeEventListener(event, handler, true));
1149
+ this._outsideListeners = [];
1150
+ }
1090
1151
  this._delegatedEvents = null;
1091
1152
  this._eventBindings = null;
1092
1153
  // Clear any pending debounce/throttle timers to prevent stale closures.