zero-query 0.1.0
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/LICENSE +21 -0
- package/README.md +1271 -0
- package/index.js +173 -0
- package/package.json +48 -0
- package/src/component.js +800 -0
- package/src/core.js +508 -0
- package/src/http.js +177 -0
- package/src/reactive.js +125 -0
- package/src/router.js +334 -0
- package/src/store.js +169 -0
- package/src/utils.js +271 -0
package/src/core.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zQuery Core — Selector engine & chainable DOM collection
|
|
3
|
+
*
|
|
4
|
+
* Extends the quick-ref pattern (Id, Class, Classes, Children)
|
|
5
|
+
* into a full jQuery-like chainable wrapper with modern APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// ZQueryCollection — wraps an array of elements with chainable methods
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export class ZQueryCollection {
|
|
12
|
+
constructor(elements) {
|
|
13
|
+
this.elements = Array.isArray(elements) ? elements : [elements];
|
|
14
|
+
this.length = this.elements.length;
|
|
15
|
+
this.elements.forEach((el, i) => { this[i] = el; });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Iteration -----------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
each(fn) {
|
|
21
|
+
this.elements.forEach((el, i) => fn.call(el, i, el));
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
map(fn) {
|
|
26
|
+
return this.elements.map((el, i) => fn.call(el, i, el));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
first() { return this.elements[0] || null; }
|
|
30
|
+
last() { return this.elements[this.length - 1] || null; }
|
|
31
|
+
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
32
|
+
toArray(){ return [...this.elements]; }
|
|
33
|
+
|
|
34
|
+
[Symbol.iterator]() { return this.elements[Symbol.iterator](); }
|
|
35
|
+
|
|
36
|
+
// --- Traversal -----------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
find(selector) {
|
|
39
|
+
const found = [];
|
|
40
|
+
this.elements.forEach(el => found.push(...el.querySelectorAll(selector)));
|
|
41
|
+
return new ZQueryCollection(found);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
parent() {
|
|
45
|
+
const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))];
|
|
46
|
+
return new ZQueryCollection(parents);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
closest(selector) {
|
|
50
|
+
return new ZQueryCollection(
|
|
51
|
+
this.elements.map(el => el.closest(selector)).filter(Boolean)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
children(selector) {
|
|
56
|
+
const kids = [];
|
|
57
|
+
this.elements.forEach(el => {
|
|
58
|
+
kids.push(...(selector
|
|
59
|
+
? el.querySelectorAll(`:scope > ${selector}`)
|
|
60
|
+
: el.children));
|
|
61
|
+
});
|
|
62
|
+
return new ZQueryCollection([...kids]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
siblings() {
|
|
66
|
+
const sibs = [];
|
|
67
|
+
this.elements.forEach(el => {
|
|
68
|
+
sibs.push(...[...el.parentElement.children].filter(c => c !== el));
|
|
69
|
+
});
|
|
70
|
+
return new ZQueryCollection(sibs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); }
|
|
74
|
+
prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); }
|
|
75
|
+
|
|
76
|
+
filter(selector) {
|
|
77
|
+
if (typeof selector === 'function') {
|
|
78
|
+
return new ZQueryCollection(this.elements.filter(selector));
|
|
79
|
+
}
|
|
80
|
+
return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
not(selector) {
|
|
84
|
+
if (typeof selector === 'function') {
|
|
85
|
+
return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el)));
|
|
86
|
+
}
|
|
87
|
+
return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
has(selector) {
|
|
91
|
+
return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Classes -------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
addClass(...names) {
|
|
97
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
98
|
+
return this.each((_, el) => el.classList.add(...classes));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
removeClass(...names) {
|
|
102
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
103
|
+
return this.each((_, el) => el.classList.remove(...classes));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toggleClass(name, force) {
|
|
107
|
+
return this.each((_, el) => el.classList.toggle(name, force));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
hasClass(name) {
|
|
111
|
+
return this.first()?.classList.contains(name) || false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Attributes ----------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
attr(name, value) {
|
|
117
|
+
if (value === undefined) return this.first()?.getAttribute(name);
|
|
118
|
+
return this.each((_, el) => el.setAttribute(name, value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
removeAttr(name) {
|
|
122
|
+
return this.each((_, el) => el.removeAttribute(name));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
prop(name, value) {
|
|
126
|
+
if (value === undefined) return this.first()?.[name];
|
|
127
|
+
return this.each((_, el) => { el[name] = value; });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
data(key, value) {
|
|
131
|
+
if (value === undefined) {
|
|
132
|
+
if (key === undefined) return this.first()?.dataset;
|
|
133
|
+
const raw = this.first()?.dataset[key];
|
|
134
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
135
|
+
}
|
|
136
|
+
return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- CSS / Dimensions ----------------------------------------------------
|
|
140
|
+
|
|
141
|
+
css(props) {
|
|
142
|
+
if (typeof props === 'string') {
|
|
143
|
+
return getComputedStyle(this.first())[props];
|
|
144
|
+
}
|
|
145
|
+
return this.each((_, el) => Object.assign(el.style, props));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
width() { return this.first()?.getBoundingClientRect().width; }
|
|
149
|
+
height() { return this.first()?.getBoundingClientRect().height; }
|
|
150
|
+
|
|
151
|
+
offset() {
|
|
152
|
+
const r = this.first()?.getBoundingClientRect();
|
|
153
|
+
return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
position() {
|
|
157
|
+
const el = this.first();
|
|
158
|
+
return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Content -------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
html(content) {
|
|
164
|
+
if (content === undefined) return this.first()?.innerHTML;
|
|
165
|
+
return this.each((_, el) => { el.innerHTML = content; });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
text(content) {
|
|
169
|
+
if (content === undefined) return this.first()?.textContent;
|
|
170
|
+
return this.each((_, el) => { el.textContent = content; });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
val(value) {
|
|
174
|
+
if (value === undefined) return this.first()?.value;
|
|
175
|
+
return this.each((_, el) => { el.value = value; });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- DOM Manipulation ----------------------------------------------------
|
|
179
|
+
|
|
180
|
+
append(content) {
|
|
181
|
+
return this.each((_, el) => {
|
|
182
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content);
|
|
183
|
+
else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c));
|
|
184
|
+
else if (content instanceof Node) el.appendChild(content);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
prepend(content) {
|
|
189
|
+
return this.each((_, el) => {
|
|
190
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content);
|
|
191
|
+
else if (content instanceof Node) el.insertBefore(content, el.firstChild);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
after(content) {
|
|
196
|
+
return this.each((_, el) => {
|
|
197
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterend', content);
|
|
198
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
before(content) {
|
|
203
|
+
return this.each((_, el) => {
|
|
204
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content);
|
|
205
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
wrap(wrapper) {
|
|
210
|
+
return this.each((_, el) => {
|
|
211
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
212
|
+
el.parentNode.insertBefore(w, el);
|
|
213
|
+
w.appendChild(el);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
remove() {
|
|
218
|
+
return this.each((_, el) => el.remove());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
empty() {
|
|
222
|
+
return this.each((_, el) => { el.innerHTML = ''; });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
clone(deep = true) {
|
|
226
|
+
return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep)));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
replaceWith(content) {
|
|
230
|
+
return this.each((_, el) => {
|
|
231
|
+
if (typeof content === 'string') {
|
|
232
|
+
el.insertAdjacentHTML('afterend', content);
|
|
233
|
+
el.remove();
|
|
234
|
+
} else if (content instanceof Node) {
|
|
235
|
+
el.parentNode.replaceChild(content, el);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Visibility ----------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
show(display = '') {
|
|
243
|
+
return this.each((_, el) => { el.style.display = display; });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
hide() {
|
|
247
|
+
return this.each((_, el) => { el.style.display = 'none'; });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
toggle(display = '') {
|
|
251
|
+
return this.each((_, el) => {
|
|
252
|
+
el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none';
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- Events --------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
on(event, selectorOrHandler, handler) {
|
|
259
|
+
// Support multiple events: "click mouseenter"
|
|
260
|
+
const events = event.split(/\s+/);
|
|
261
|
+
return this.each((_, el) => {
|
|
262
|
+
events.forEach(evt => {
|
|
263
|
+
if (typeof selectorOrHandler === 'function') {
|
|
264
|
+
el.addEventListener(evt, selectorOrHandler);
|
|
265
|
+
} else {
|
|
266
|
+
// Delegated event
|
|
267
|
+
el.addEventListener(evt, (e) => {
|
|
268
|
+
const target = e.target.closest(selectorOrHandler);
|
|
269
|
+
if (target && el.contains(target)) handler.call(target, e);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
off(event, handler) {
|
|
277
|
+
const events = event.split(/\s+/);
|
|
278
|
+
return this.each((_, el) => {
|
|
279
|
+
events.forEach(evt => el.removeEventListener(evt, handler));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
one(event, handler) {
|
|
284
|
+
return this.each((_, el) => {
|
|
285
|
+
el.addEventListener(event, handler, { once: true });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
trigger(event, detail) {
|
|
290
|
+
return this.each((_, el) => {
|
|
291
|
+
el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Convenience event shorthands
|
|
296
|
+
click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); }
|
|
297
|
+
submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
|
|
298
|
+
focus() { this.first()?.focus(); return this; }
|
|
299
|
+
blur() { this.first()?.blur(); return this; }
|
|
300
|
+
|
|
301
|
+
// --- Animation -----------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
animate(props, duration = 300, easing = 'ease') {
|
|
304
|
+
return new Promise(resolve => {
|
|
305
|
+
const count = { done: 0 };
|
|
306
|
+
this.each((_, el) => {
|
|
307
|
+
el.style.transition = `all ${duration}ms ${easing}`;
|
|
308
|
+
requestAnimationFrame(() => {
|
|
309
|
+
Object.assign(el.style, props);
|
|
310
|
+
const onEnd = () => {
|
|
311
|
+
el.removeEventListener('transitionend', onEnd);
|
|
312
|
+
el.style.transition = '';
|
|
313
|
+
if (++count.done >= this.length) resolve(this);
|
|
314
|
+
};
|
|
315
|
+
el.addEventListener('transitionend', onEnd);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
// Fallback in case transitionend doesn't fire
|
|
319
|
+
setTimeout(() => resolve(this), duration + 50);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fadeIn(duration = 300) {
|
|
324
|
+
return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fadeOut(duration = 300) {
|
|
328
|
+
return this.animate({ opacity: '0' }, duration).then(col => col.hide());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
slideToggle(duration = 300) {
|
|
332
|
+
return this.each((_, el) => {
|
|
333
|
+
if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
|
|
334
|
+
el.style.display = '';
|
|
335
|
+
el.style.overflow = 'hidden';
|
|
336
|
+
const h = el.scrollHeight + 'px';
|
|
337
|
+
el.style.maxHeight = '0';
|
|
338
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
339
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
340
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
341
|
+
} else {
|
|
342
|
+
el.style.overflow = 'hidden';
|
|
343
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
344
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
345
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
346
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Form helpers --------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
serialize() {
|
|
354
|
+
const form = this.first();
|
|
355
|
+
if (!form || form.tagName !== 'FORM') return '';
|
|
356
|
+
return new URLSearchParams(new FormData(form)).toString();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
serializeObject() {
|
|
360
|
+
const form = this.first();
|
|
361
|
+
if (!form || form.tagName !== 'FORM') return {};
|
|
362
|
+
const obj = {};
|
|
363
|
+
new FormData(form).forEach((v, k) => {
|
|
364
|
+
if (obj[k] !== undefined) {
|
|
365
|
+
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
366
|
+
obj[k].push(v);
|
|
367
|
+
} else {
|
|
368
|
+
obj[k] = v;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return obj;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Helper — create document fragment from HTML string
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
function createFragment(html) {
|
|
380
|
+
const tpl = document.createElement('template');
|
|
381
|
+
tpl.innerHTML = html.trim();
|
|
382
|
+
return tpl.content;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// $() — main selector / creator function (returns single element for CSS selectors)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
export function query(selector, context) {
|
|
390
|
+
// null / undefined
|
|
391
|
+
if (!selector) return null;
|
|
392
|
+
|
|
393
|
+
// Already a collection — return first element
|
|
394
|
+
if (selector instanceof ZQueryCollection) return selector.first();
|
|
395
|
+
|
|
396
|
+
// DOM element or Window — return as-is
|
|
397
|
+
if (selector instanceof Node || selector === window) {
|
|
398
|
+
return selector;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// NodeList / HTMLCollection / Array — return first element
|
|
402
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
403
|
+
const arr = Array.from(selector);
|
|
404
|
+
return arr[0] || null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// HTML string → create elements, return first
|
|
408
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
409
|
+
const fragment = createFragment(selector);
|
|
410
|
+
const els = [...fragment.childNodes].filter(n => n.nodeType === 1);
|
|
411
|
+
return els[0] || null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// CSS selector string → querySelector (single element)
|
|
415
|
+
if (typeof selector === 'string') {
|
|
416
|
+
const root = context
|
|
417
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
418
|
+
: document;
|
|
419
|
+
return root.querySelector(selector);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// $.all() — collection selector (returns ZQueryCollection for CSS selectors)
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
export function queryAll(selector, context) {
|
|
430
|
+
// null / undefined
|
|
431
|
+
if (!selector) return new ZQueryCollection([]);
|
|
432
|
+
|
|
433
|
+
// Already a collection
|
|
434
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
435
|
+
|
|
436
|
+
// DOM element or Window
|
|
437
|
+
if (selector instanceof Node || selector === window) {
|
|
438
|
+
return new ZQueryCollection([selector]);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// NodeList / HTMLCollection / Array
|
|
442
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
443
|
+
return new ZQueryCollection(Array.from(selector));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// HTML string → create elements
|
|
447
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
448
|
+
const fragment = createFragment(selector);
|
|
449
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// CSS selector string → querySelectorAll (collection)
|
|
453
|
+
if (typeof selector === 'string') {
|
|
454
|
+
const root = context
|
|
455
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
456
|
+
: document;
|
|
457
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return new ZQueryCollection([]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Quick-ref shortcuts, on $ namespace)
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
query.id = (id) => document.getElementById(id);
|
|
468
|
+
query.class = (name) => document.querySelector(`.${name}`);
|
|
469
|
+
query.classes = (name) => Array.from(document.getElementsByClassName(name));
|
|
470
|
+
query.tag = (name) => Array.from(document.getElementsByTagName(name));
|
|
471
|
+
query.children = (parentId) => {
|
|
472
|
+
const p = document.getElementById(parentId);
|
|
473
|
+
return p ? Array.from(p.children) : [];
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Create element shorthand
|
|
477
|
+
query.create = (tag, attrs = {}, ...children) => {
|
|
478
|
+
const el = document.createElement(tag);
|
|
479
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
480
|
+
if (k === 'class') el.className = v;
|
|
481
|
+
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
482
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
483
|
+
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
484
|
+
else el.setAttribute(k, v);
|
|
485
|
+
}
|
|
486
|
+
children.flat().forEach(child => {
|
|
487
|
+
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
488
|
+
else if (child instanceof Node) el.appendChild(child);
|
|
489
|
+
});
|
|
490
|
+
return el;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// DOM ready
|
|
494
|
+
query.ready = (fn) => {
|
|
495
|
+
if (document.readyState !== 'loading') fn();
|
|
496
|
+
else document.addEventListener('DOMContentLoaded', fn);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// Global event delegation
|
|
500
|
+
query.on = (event, selector, handler) => {
|
|
501
|
+
document.addEventListener(event, (e) => {
|
|
502
|
+
const target = e.target.closest(selector);
|
|
503
|
+
if (target) handler.call(target, e);
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Extend collection prototype (like $.fn in jQuery)
|
|
508
|
+
query.fn = ZQueryCollection.prototype;
|
package/src/http.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zQuery HTTP — Lightweight fetch wrapper
|
|
3
|
+
*
|
|
4
|
+
* Clean API for GET/POST/PUT/PATCH/DELETE with:
|
|
5
|
+
* - Auto JSON serialization/deserialization
|
|
6
|
+
* - Request/response interceptors
|
|
7
|
+
* - Timeout support
|
|
8
|
+
* - Base URL configuration
|
|
9
|
+
* - Abort controller integration
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* $.http.get('/api/users');
|
|
13
|
+
* $.http.post('/api/users', { name: 'Tony' });
|
|
14
|
+
* $.http.configure({ baseURL: 'https://api.example.com', headers: { Authorization: 'Bearer ...' } });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const _config = {
|
|
18
|
+
baseURL: '',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
timeout: 30000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const _interceptors = {
|
|
24
|
+
request: [],
|
|
25
|
+
response: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Core request function
|
|
31
|
+
*/
|
|
32
|
+
async function request(method, url, data, options = {}) {
|
|
33
|
+
let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
|
|
34
|
+
let headers = { ..._config.headers, ...options.headers };
|
|
35
|
+
let body = undefined;
|
|
36
|
+
|
|
37
|
+
// Build fetch options
|
|
38
|
+
const fetchOpts = {
|
|
39
|
+
method: method.toUpperCase(),
|
|
40
|
+
headers,
|
|
41
|
+
...options,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Handle body
|
|
45
|
+
if (data !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
46
|
+
if (data instanceof FormData) {
|
|
47
|
+
body = data;
|
|
48
|
+
delete fetchOpts.headers['Content-Type']; // Let browser set multipart boundary
|
|
49
|
+
} else if (typeof data === 'object') {
|
|
50
|
+
body = JSON.stringify(data);
|
|
51
|
+
} else {
|
|
52
|
+
body = data;
|
|
53
|
+
}
|
|
54
|
+
fetchOpts.body = body;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Query params for GET
|
|
58
|
+
if (data && (method === 'GET' || method === 'HEAD') && typeof data === 'object') {
|
|
59
|
+
const params = new URLSearchParams(data).toString();
|
|
60
|
+
fullURL += (fullURL.includes('?') ? '&' : '?') + params;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Timeout via AbortController
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
fetchOpts.signal = options.signal || controller.signal;
|
|
66
|
+
const timeout = options.timeout ?? _config.timeout;
|
|
67
|
+
let timer;
|
|
68
|
+
if (timeout > 0) {
|
|
69
|
+
timer = setTimeout(() => controller.abort(), timeout);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Run request interceptors
|
|
73
|
+
for (const interceptor of _interceptors.request) {
|
|
74
|
+
const result = await interceptor(fetchOpts, fullURL);
|
|
75
|
+
if (result === false) throw new Error('Request blocked by interceptor');
|
|
76
|
+
if (result?.url) fullURL = result.url;
|
|
77
|
+
if (result?.options) Object.assign(fetchOpts, result.options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(fullURL, fetchOpts);
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
|
|
84
|
+
// Parse response
|
|
85
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
86
|
+
let responseData;
|
|
87
|
+
|
|
88
|
+
if (contentType.includes('application/json')) {
|
|
89
|
+
responseData = await response.json();
|
|
90
|
+
} else if (contentType.includes('text/')) {
|
|
91
|
+
responseData = await response.text();
|
|
92
|
+
} else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
|
|
93
|
+
responseData = await response.blob();
|
|
94
|
+
} else {
|
|
95
|
+
// Try JSON first, fall back to text
|
|
96
|
+
const text = await response.text();
|
|
97
|
+
try { responseData = JSON.parse(text); } catch { responseData = text; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = {
|
|
101
|
+
ok: response.ok,
|
|
102
|
+
status: response.status,
|
|
103
|
+
statusText: response.statusText,
|
|
104
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
105
|
+
data: responseData,
|
|
106
|
+
response,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Run response interceptors
|
|
110
|
+
for (const interceptor of _interceptors.response) {
|
|
111
|
+
await interceptor(result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
116
|
+
err.response = result;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (timer) clearTimeout(timer);
|
|
123
|
+
if (err.name === 'AbortError') {
|
|
124
|
+
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Public API
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
export const http = {
|
|
135
|
+
get: (url, params, opts) => request('GET', url, params, opts),
|
|
136
|
+
post: (url, data, opts) => request('POST', url, data, opts),
|
|
137
|
+
put: (url, data, opts) => request('PUT', url, data, opts),
|
|
138
|
+
patch: (url, data, opts) => request('PATCH', url, data, opts),
|
|
139
|
+
delete: (url, data, opts) => request('DELETE', url, data, opts),
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Configure defaults
|
|
143
|
+
*/
|
|
144
|
+
configure(opts) {
|
|
145
|
+
if (opts.baseURL !== undefined) _config.baseURL = opts.baseURL;
|
|
146
|
+
if (opts.headers) Object.assign(_config.headers, opts.headers);
|
|
147
|
+
if (opts.timeout !== undefined) _config.timeout = opts.timeout;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Add request interceptor
|
|
152
|
+
* @param {Function} fn — (fetchOpts, url) → void | false | { url, options }
|
|
153
|
+
*/
|
|
154
|
+
onRequest(fn) {
|
|
155
|
+
_interceptors.request.push(fn);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Add response interceptor
|
|
160
|
+
* @param {Function} fn — (result) → void
|
|
161
|
+
*/
|
|
162
|
+
onResponse(fn) {
|
|
163
|
+
_interceptors.response.push(fn);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create a standalone AbortController for manual cancellation
|
|
168
|
+
*/
|
|
169
|
+
createAbort() {
|
|
170
|
+
return new AbortController();
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Raw fetch pass-through (for edge cases)
|
|
175
|
+
*/
|
|
176
|
+
raw: (url, opts) => fetch(url, opts),
|
|
177
|
+
};
|