zero-query 0.9.5 → 0.9.7

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.
package/src/http.js CHANGED
@@ -165,6 +165,7 @@ export const http = {
165
165
  put: (url, data, opts) => request('PUT', url, data, opts),
166
166
  patch: (url, data, opts) => request('PATCH', url, data, opts),
167
167
  delete: (url, data, opts) => request('DELETE', url, data, opts),
168
+ head: (url, opts) => request('HEAD', url, undefined, opts),
168
169
 
169
170
  /**
170
171
  * Configure defaults
@@ -175,20 +176,56 @@ export const http = {
175
176
  if (opts.timeout !== undefined) _config.timeout = opts.timeout;
176
177
  },
177
178
 
179
+ /**
180
+ * Read-only snapshot of current configuration
181
+ */
182
+ getConfig() {
183
+ return {
184
+ baseURL: _config.baseURL,
185
+ headers: { ..._config.headers },
186
+ timeout: _config.timeout,
187
+ };
188
+ },
189
+
178
190
  /**
179
191
  * Add request interceptor
180
192
  * @param {Function} fn — (fetchOpts, url) → void | false | { url, options }
193
+ * @returns {Function} unsubscribe function
181
194
  */
182
195
  onRequest(fn) {
183
196
  _interceptors.request.push(fn);
197
+ return () => {
198
+ const idx = _interceptors.request.indexOf(fn);
199
+ if (idx !== -1) _interceptors.request.splice(idx, 1);
200
+ };
184
201
  },
185
202
 
186
203
  /**
187
204
  * Add response interceptor
188
205
  * @param {Function} fn — (result) → void
206
+ * @returns {Function} unsubscribe function
189
207
  */
190
208
  onResponse(fn) {
191
209
  _interceptors.response.push(fn);
210
+ return () => {
211
+ const idx = _interceptors.response.indexOf(fn);
212
+ if (idx !== -1) _interceptors.response.splice(idx, 1);
213
+ };
214
+ },
215
+
216
+ /**
217
+ * Clear interceptors — all, or just 'request' / 'response'
218
+ */
219
+ clearInterceptors(type) {
220
+ if (!type || type === 'request') _interceptors.request.length = 0;
221
+ if (!type || type === 'response') _interceptors.response.length = 0;
222
+ },
223
+
224
+ /**
225
+ * Run multiple requests in parallel
226
+ */
227
+ all(requests) {
228
+ return Promise.all(requests);
192
229
  },
193
230
 
194
231
  /**