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/README.md +6 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +107 -8
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +1 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/component.js +66 -5
- package/src/http.js +37 -0
- package/tests/component.test.js +1185 -0
- package/tests/http.test.js +200 -0
- package/types/http.d.ts +15 -4
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
|
|
740
|
-
// z-trim
|
|
741
|
-
// z-number
|
|
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
|
-
|
|
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
|
/**
|