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 CHANGED
@@ -15,19 +15,19 @@
15
15
 
16
16
  </p>
17
17
 
18
- > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~91 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
18
+ > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~100 KB minified browser bundle. Works out of the box with ES modules. An optional CLI bundler is available for single-file production builds.**
19
19
 
20
20
  ## Features
21
21
 
22
22
  | Module | Highlights |
23
23
  | --- | --- |
24
- | **Components** | Reactive state, template literals, `@event` delegation (8 modifiers), `z-model` two-way binding, computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
24
+ | **Components** | Reactive state, template literals, `@event` delegation (22 modifiers — key filters, system keys, `.outside`, timing, and more), `z-model` two-way binding (with `z-trim`, `z-number`, `z-lazy`, `z-debounce`, `z-uppercase`, `z-lowercase`), computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
25
25
  | **Router** | History & hash mode, route params (`:id`), wildcards, guards (`beforeEach`/`afterEach`), lazy loading, `z-link` navigation, `z-to-top` scroll modifier (`instant`/`smooth`), sub-route history substates (`pushSubstate`/`onSubstate`) |
26
26
  | **Directives** | `z-if`, `z-for`, `z-model`, `z-show`, `z-bind`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip` &mdash; 17 built-in template directives |
27
27
  | **Reactive** | Deep proxy reactivity, Signals (`.value`, `.peek()`), computed values, effects (auto-tracked with dispose) |
28
28
  | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions, action history, snapshots |
29
29
  | **Selectors & DOM** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
30
- | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
30
+ | **HTTP** | Fetch wrapper with auto-JSON, interceptors (with unsubscribe & clear), HEAD requests, parallel requests (`http.all`), config inspection (`getConfig`), timeout/abort, base URL |
31
31
  | **Utils** | debounce, throttle, pipe, once, sleep, memoize, escapeHtml, stripHtml, uuid, capitalize, truncate, range, chunk, groupBy, unique, pick, omit, getPath/setPath, isEmpty, clamp, retry, timeout, deepClone, deepMerge, storage/session wrappers, event bus |
32
32
  | **Dev Tools** | CLI dev server with live-reload, CSS hot-swap, full-screen error overlay, floating toolbar, dark-themed inspector panel (Router view, DOM tree, network log, component viewer, performance dashboard), fetch interceptor, render instrumentation, CLI bundler for single-file production builds |
33
33
 
@@ -75,7 +75,7 @@ If you prefer **zero tooling**, download `dist/zquery.min.js` from the [dist/ fo
75
75
  git clone https://github.com/tonywied17/zero-query.git
76
76
  cd zero-query
77
77
  npx zquery build
78
- # → dist/zquery.min.js (~91 KB)
78
+ # → dist/zquery.min.js (~100 KB)
79
79
  ```
80
80
 
81
81
  ### Include in HTML
@@ -261,7 +261,7 @@ location / {
261
261
  | `$.style` | Dynamically load global stylesheet file(s) at runtime |
262
262
  | `$.router` `$.getRouter` | SPA router |
263
263
  | `$.store` `$.getStore` | State management |
264
- | `$.http` `$.get` `$.post` `$.put` `$.patch` `$.delete` | HTTP client |
264
+ | `$.http` `$.get` `$.post` `$.put` `$.patch` `$.delete` `$.head` | HTTP client |
265
265
  | `$.reactive` `$.Signal` `$.signal` `$.computed` `$.effect` | Reactive primitives |
266
266
  | `$.debounce` `$.throttle` `$.pipe` `$.once` `$.sleep` `$.memoize` | Function utils |
267
267
  | `$.escapeHtml` `$.stripHtml` `$.html` `$.trust` `$.TrustedHTML` `$.uuid` `$.camelCase` `$.kebabCase` `$.capitalize` `$.truncate` | String utils |
@@ -271,7 +271,7 @@ location / {
271
271
  | `$.retry` `$.timeout` | Async utils |
272
272
  | `$.param` `$.parseQuery` | URL utils |
273
273
  | `$.storage` `$.session` | Storage wrappers |
274
- | `$.EventBus` `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
274
+ | `$.EventBus` `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~100 KB\"`) |
275
275
  | `$.meta` | Build metadata (populated by CLI bundler) |
276
276
  | `$.noConflict` | Release `$` global |
277
277
 
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.9.5
2
+ * zQuery (zeroQuery) v0.9.7
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -2887,7 +2887,7 @@ class Component {
2887
2887
  const defaultSlotNodes = [];
2888
2888
  [...el.childNodes].forEach(node => {
2889
2889
  if (node.nodeType === 1 && node.hasAttribute('slot')) {
2890
- const slotName = node.getAttribute('slot');
2890
+ const slotName = node.getAttribute('slot') || 'default';
2891
2891
  if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
2892
2892
  this._slotContent[slotName] += node.outerHTML;
2893
2893
  } else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
@@ -3296,6 +3296,24 @@ class Component {
3296
3296
  for (const [event, bindings] of eventMap) {
3297
3297
  this._attachDelegatedEvent(event, bindings);
3298
3298
  }
3299
+
3300
+ // .outside — attach a document-level listener for bindings that need
3301
+ // to detect clicks/events outside their element.
3302
+ this._outsideListeners = this._outsideListeners || [];
3303
+ for (const [event, bindings] of eventMap) {
3304
+ for (const binding of bindings) {
3305
+ if (!binding.modifiers.includes('outside')) continue;
3306
+ const outsideHandler = (e) => {
3307
+ if (binding.el.contains(e.target)) return;
3308
+ const match = binding.methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
3309
+ if (!match) return;
3310
+ const fn = this[match[1]];
3311
+ if (typeof fn === 'function') fn.call(this, e);
3312
+ };
3313
+ document.addEventListener(event, outsideHandler, true);
3314
+ this._outsideListeners.push({ event, handler: outsideHandler });
3315
+ }
3316
+ }
3299
3317
  }
3300
3318
 
3301
3319
  // Attach a single delegated listener for an event type
@@ -3340,6 +3358,28 @@ class Component {
3340
3358
  // .self — only fire if target is the element itself
3341
3359
  if (modifiers.includes('self') && e.target !== el) continue;
3342
3360
 
3361
+ // .outside — only fire if event target is OUTSIDE the element
3362
+ if (modifiers.includes('outside')) {
3363
+ if (el.contains(e.target)) continue;
3364
+ }
3365
+
3366
+ // Key modifiers — filter keyboard events by key
3367
+ const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
3368
+ let keyFiltered = false;
3369
+ for (const mod of modifiers) {
3370
+ if (_keyMap[mod]) {
3371
+ const keys = _keyMap[mod].split('|');
3372
+ if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
3373
+ }
3374
+ }
3375
+ if (keyFiltered) continue;
3376
+
3377
+ // System key modifiers — require modifier keys to be held
3378
+ if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
3379
+ if (modifiers.includes('shift') && !e.shiftKey) continue;
3380
+ if (modifiers.includes('alt') && !e.altKey) continue;
3381
+ if (modifiers.includes('meta') && !e.metaKey) continue;
3382
+
3343
3383
  // Handle modifiers
3344
3384
  if (modifiers.includes('prevent')) e.preventDefault();
3345
3385
  if (modifiers.includes('stop')) {
@@ -3425,9 +3465,12 @@ class Component {
3425
3465
  // textarea, select (single & multiple), contenteditable
3426
3466
  // Nested state keys: z-model="user.name" → this.state.user.name
3427
3467
  // Modifiers (boolean attributes on the same element):
3428
- // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
3429
- // z-trim — trim whitespace before writing to state
3430
- // z-number — force Number() conversion regardless of input type
3468
+ // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
3469
+ // z-trim — trim whitespace before writing to state
3470
+ // z-number — force Number() conversion regardless of input type
3471
+ // z-debounce — debounce state writes (default 250ms, or z-debounce="300")
3472
+ // z-uppercase — convert string to uppercase before writing to state
3473
+ // z-lowercase — convert string to lowercase before writing to state
3431
3474
  //
3432
3475
  // Writes to reactive state so the rest of the UI stays in sync.
3433
3476
  // Focus and cursor position are preserved in _render() via focusInfo.
@@ -3443,6 +3486,10 @@ class Component {
3443
3486
  const isLazy = el.hasAttribute('z-lazy');
3444
3487
  const isTrim = el.hasAttribute('z-trim');
3445
3488
  const isNum = el.hasAttribute('z-number');
3489
+ const isUpper = el.hasAttribute('z-uppercase');
3490
+ const isLower = el.hasAttribute('z-lowercase');
3491
+ const hasDebounce = el.hasAttribute('z-debounce');
3492
+ const debounceMs = hasDebounce ? (parseInt(el.getAttribute('z-debounce'), 10) || 250) : 0;
3446
3493
 
3447
3494
  // Read current state value (supports dot-path keys)
3448
3495
  const currentVal = _getPath(this.state, key);
@@ -3483,6 +3530,8 @@ class Component {
3483
3530
 
3484
3531
  // Apply modifiers
3485
3532
  if (isTrim && typeof val === 'string') val = val.trim();
3533
+ if (isUpper && typeof val === 'string') val = val.toUpperCase();
3534
+ if (isLower && typeof val === 'string') val = val.toLowerCase();
3486
3535
  if (isNum || type === 'number' || type === 'range') val = Number(val);
3487
3536
 
3488
3537
  // Write through the reactive proxy (triggers re-render).
@@ -3490,7 +3539,15 @@ class Component {
3490
3539
  _setPath(this.state, key, val);
3491
3540
  };
3492
3541
 
3493
- el.addEventListener(event, handler);
3542
+ if (hasDebounce) {
3543
+ let timer = null;
3544
+ el.addEventListener(event, () => {
3545
+ clearTimeout(timer);
3546
+ timer = setTimeout(handler, debounceMs);
3547
+ });
3548
+ } else {
3549
+ el.addEventListener(event, handler);
3550
+ }
3494
3551
  });
3495
3552
  }
3496
3553
 
@@ -3776,6 +3833,10 @@ class Component {
3776
3833
  }
3777
3834
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
3778
3835
  this._listeners = [];
3836
+ if (this._outsideListeners) {
3837
+ this._outsideListeners.forEach(({ event, handler }) => document.removeEventListener(event, handler, true));
3838
+ this._outsideListeners = [];
3839
+ }
3779
3840
  this._delegatedEvents = null;
3780
3841
  this._eventBindings = null;
3781
3842
  // Clear any pending debounce/throttle timers to prevent stale closures.
@@ -5066,6 +5127,7 @@ const http = {
5066
5127
  put: (url, data, opts) => request('PUT', url, data, opts),
5067
5128
  patch: (url, data, opts) => request('PATCH', url, data, opts),
5068
5129
  delete: (url, data, opts) => request('DELETE', url, data, opts),
5130
+ head: (url, opts) => request('HEAD', url, undefined, opts),
5069
5131
 
5070
5132
  /**
5071
5133
  * Configure defaults
@@ -5076,20 +5138,56 @@ const http = {
5076
5138
  if (opts.timeout !== undefined) _config.timeout = opts.timeout;
5077
5139
  },
5078
5140
 
5141
+ /**
5142
+ * Read-only snapshot of current configuration
5143
+ */
5144
+ getConfig() {
5145
+ return {
5146
+ baseURL: _config.baseURL,
5147
+ headers: { ..._config.headers },
5148
+ timeout: _config.timeout,
5149
+ };
5150
+ },
5151
+
5079
5152
  /**
5080
5153
  * Add request interceptor
5081
5154
  * @param {Function} fn — (fetchOpts, url) → void | false | { url, options }
5155
+ * @returns {Function} unsubscribe function
5082
5156
  */
5083
5157
  onRequest(fn) {
5084
5158
  _interceptors.request.push(fn);
5159
+ return () => {
5160
+ const idx = _interceptors.request.indexOf(fn);
5161
+ if (idx !== -1) _interceptors.request.splice(idx, 1);
5162
+ };
5085
5163
  },
5086
5164
 
5087
5165
  /**
5088
5166
  * Add response interceptor
5089
5167
  * @param {Function} fn — (result) → void
5168
+ * @returns {Function} unsubscribe function
5090
5169
  */
5091
5170
  onResponse(fn) {
5092
5171
  _interceptors.response.push(fn);
5172
+ return () => {
5173
+ const idx = _interceptors.response.indexOf(fn);
5174
+ if (idx !== -1) _interceptors.response.splice(idx, 1);
5175
+ };
5176
+ },
5177
+
5178
+ /**
5179
+ * Clear interceptors — all, or just 'request' / 'response'
5180
+ */
5181
+ clearInterceptors(type) {
5182
+ if (!type || type === 'request') _interceptors.request.length = 0;
5183
+ if (!type || type === 'response') _interceptors.response.length = 0;
5184
+ },
5185
+
5186
+ /**
5187
+ * Run multiple requests in parallel
5188
+ */
5189
+ all(requests) {
5190
+ return Promise.all(requests);
5093
5191
  },
5094
5192
 
5095
5193
  /**
@@ -5689,6 +5787,7 @@ $.post = http.post;
5689
5787
  $.put = http.put;
5690
5788
  $.patch = http.patch;
5691
5789
  $.delete = http.delete;
5790
+ $.head = http.head;
5692
5791
 
5693
5792
  // --- Utilities -------------------------------------------------------------
5694
5793
  $.debounce = debounce;
@@ -5737,8 +5836,8 @@ $.guardCallback = guardCallback;
5737
5836
  $.validate = validate;
5738
5837
 
5739
5838
  // --- Meta ------------------------------------------------------------------
5740
- $.version = '0.9.5';
5741
- $.libSize = '~98 KB';
5839
+ $.version = '0.9.7';
5840
+ $.libSize = '~100 KB';
5742
5841
  $.meta = {}; // populated at build time by CLI bundler
5743
5842
 
5744
5843
  $.noConflict = () => {