zero-query 0.3.1 → 0.5.2

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/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.3.1
2
+ * zQuery (zeroQuery) v0.5.2
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman — MIT License
@@ -605,6 +605,16 @@ query.id = (id) => document.getElementById(id);
605
605
  query.class = (name) => document.querySelector(`.${name}`);
606
606
  query.classes = (name) => Array.from(document.getElementsByClassName(name));
607
607
  query.tag = (name) => Array.from(document.getElementsByTagName(name));
608
+ Object.defineProperty(query, 'name', {
609
+ value: (name) => Array.from(document.getElementsByName(name)),
610
+ writable: true, configurable: true
611
+ });
612
+ query.attr = (attr, value) => Array.from(
613
+ document.querySelectorAll(value !== undefined ? `[${attr}="${value}"]` : `[${attr}]`)
614
+ );
615
+ query.data = (key, value) => Array.from(
616
+ document.querySelectorAll(value !== undefined ? `[data-${key}="${value}"]` : `[data-${key}]`)
617
+ );
608
618
  query.children = (parentId) => {
609
619
  const p = document.getElementById(parentId);
610
620
  return p ? Array.from(p.children) : [];
@@ -633,14 +643,27 @@ query.ready = (fn) => {
633
643
  else document.addEventListener('DOMContentLoaded', fn);
634
644
  };
635
645
 
636
- // Global event delegation
637
- query.on = (event, selector, handler) => {
646
+ // Global event listeners — supports direct and delegated forms
647
+ // $.on('keydown', handler) direct listener on document
648
+ // $.on('click', '.btn', handler) → delegated via closest()
649
+ query.on = (event, selectorOrHandler, handler) => {
650
+ if (typeof selectorOrHandler === 'function') {
651
+ // 2-arg: direct document listener (keydown, resize, etc.)
652
+ document.addEventListener(event, selectorOrHandler);
653
+ return;
654
+ }
655
+ // 3-arg: delegated
638
656
  document.addEventListener(event, (e) => {
639
- const target = e.target.closest(selector);
657
+ const target = e.target.closest(selectorOrHandler);
640
658
  if (target) handler.call(target, e);
641
659
  });
642
660
  };
643
661
 
662
+ // Remove a direct global listener
663
+ query.off = (event, handler) => {
664
+ document.removeEventListener(event, handler);
665
+ };
666
+
644
667
  // Extend collection prototype (like $.fn in jQuery)
645
668
  query.fn = ZQueryCollection.prototype;
646
669
 
@@ -648,7 +671,7 @@ query.fn = ZQueryCollection.prototype;
648
671
  /**
649
672
  * zQuery Component — Lightweight reactive component system
650
673
  *
651
- * Declarative components using template literals (no JSX, no build step).
674
+ * Declarative components using template literals with directive support.
652
675
  * Proxy-based state triggers targeted re-renders via event delegation.
653
676
  *
654
677
  * Features:
@@ -677,6 +700,18 @@ const _resourceCache = new Map(); // url → Promise<string>
677
700
  // Unique ID counter
678
701
  let _uid = 0;
679
702
 
703
+ // Inject z-cloak base style and mobile tap-highlight reset (once, globally)
704
+ if (typeof document !== 'undefined' && !document.querySelector('[data-zq-cloak]')) {
705
+ const _s = document.createElement('style');
706
+ _s.textContent = '[z-cloak]{display:none!important}*,*::before,*::after{-webkit-tap-highlight-color:transparent}';
707
+ _s.setAttribute('data-zq-cloak', '');
708
+ document.head.appendChild(_s);
709
+ }
710
+
711
+ // Debounce / throttle helpers for event modifiers
712
+ const _debounceTimers = new WeakMap();
713
+ const _throttleTimers = new WeakMap();
714
+
680
715
  /**
681
716
  * Fetch and cache a text resource (HTML template or CSS file).
682
717
  * @param {string} url — URL to fetch
@@ -861,8 +896,11 @@ class Component {
861
896
  if (this._updateQueued) return;
862
897
  this._updateQueued = true;
863
898
  queueMicrotask(() => {
864
- this._updateQueued = false;
865
- if (!this._destroyed) this._render();
899
+ try {
900
+ if (!this._destroyed) this._render();
901
+ } finally {
902
+ this._updateQueued = false;
903
+ }
866
904
  });
867
905
  }
868
906
 
@@ -890,12 +928,14 @@ class Component {
890
928
  // items: ['page-a', { id: 'page-b', label: 'Page B' }, ...]
891
929
  // }
892
930
  // Exposes this.pages (array of {id,label}), this.activePage (current id)
931
+ // Pages are lazy-loaded: only the active page is fetched on first render,
932
+ // remaining pages are prefetched in the background for instant navigation.
893
933
  //
894
934
  async _loadExternals() {
895
935
  const def = this._def;
896
936
  const base = def._base; // auto-detected or explicit
897
937
 
898
- // ── Pages config ─────────────────────────────────────────────
938
+ // -- Pages config ---------------------------------------------
899
939
  if (def.pages && !def._pagesNormalized) {
900
940
  const p = def.pages;
901
941
  const ext = p.ext || '.html';
@@ -907,18 +947,20 @@ class Component {
907
947
  return { id: item.id, label: item.label || _titleCase(item.id) };
908
948
  });
909
949
 
910
- // Auto-generate templateUrl object map
911
- if (!def.templateUrl) {
912
- def.templateUrl = {};
913
- for (const { id } of def._pages) {
914
- def.templateUrl[id] = `${dir}/${id}${ext}`;
915
- }
950
+ // Build URL map for lazy per-page loading.
951
+ // Pages are fetched on demand (active page first, rest prefetched in
952
+ // the background) so the component renders as soon as the visible
953
+ // page is ready instead of waiting for every page to download.
954
+ def._pageUrls = {};
955
+ for (const { id } of def._pages) {
956
+ def._pageUrls[id] = `${dir}/${id}${ext}`;
916
957
  }
958
+ if (!def._externalTemplates) def._externalTemplates = {};
917
959
 
918
960
  def._pagesNormalized = true;
919
961
  }
920
962
 
921
- // ── External templates ──────────────────────────────────────
963
+ // -- External templates --------------------------------------
922
964
  if (def.templateUrl && !def._templateLoaded) {
923
965
  const tu = def.templateUrl;
924
966
  if (typeof tu === 'string') {
@@ -940,7 +982,7 @@ class Component {
940
982
  def._templateLoaded = true;
941
983
  }
942
984
 
943
- // ── External styles ─────────────────────────────────────────
985
+ // -- External styles -----------------------------------------
944
986
  if (def.styleUrl && !def._styleLoaded) {
945
987
  const su = def.styleUrl;
946
988
  if (typeof su === 'string') {
@@ -975,7 +1017,37 @@ class Component {
975
1017
  if (this._def._pages) {
976
1018
  this.pages = this._def._pages;
977
1019
  const pc = this._def.pages;
978
- this.activePage = (pc.param && this.props.$params?.[pc.param]) || pc.default || this._def._pages[0]?.id || '';
1020
+ let active = (pc.param && this.props.$params?.[pc.param]) || pc.default || this._def._pages[0]?.id || '';
1021
+
1022
+ // Fall back to default if the param doesn't match any known page
1023
+ if (this._def._pageUrls && !(active in this._def._pageUrls)) {
1024
+ active = pc.default || this._def._pages[0]?.id || '';
1025
+ }
1026
+ this.activePage = active;
1027
+
1028
+ // Lazy-load: fetch only the active page's template on demand
1029
+ if (this._def._pageUrls && !(active in this._def._externalTemplates)) {
1030
+ const url = this._def._pageUrls[active];
1031
+ if (url) {
1032
+ _fetchResource(url).then(html => {
1033
+ this._def._externalTemplates[active] = html;
1034
+ if (!this._destroyed) this._render();
1035
+ });
1036
+ return; // Wait for active page before rendering
1037
+ }
1038
+ }
1039
+
1040
+ // Prefetch remaining pages in background (once, after active page is ready)
1041
+ if (this._def._pageUrls && !this._def._pagesPrefetched) {
1042
+ this._def._pagesPrefetched = true;
1043
+ for (const [id, url] of Object.entries(this._def._pageUrls)) {
1044
+ if (!(id in this._def._externalTemplates)) {
1045
+ _fetchResource(url).then(html => {
1046
+ this._def._externalTemplates[id] = html;
1047
+ });
1048
+ }
1049
+ }
1050
+ }
979
1051
  }
980
1052
 
981
1053
  // Determine HTML content
@@ -983,9 +1055,13 @@ class Component {
983
1055
  if (this._def.render) {
984
1056
  // Inline render function takes priority
985
1057
  html = this._def.render.call(this);
1058
+ // Expand z-for in render templates ({{}} expressions for iteration items)
1059
+ html = this._expandZFor(html);
986
1060
  } else if (this._def._externalTemplate) {
987
- // External template with {{expression}} interpolation
988
- html = this._def._externalTemplate.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
1061
+ // Expand z-for FIRST (before global {{}} interpolation)
1062
+ html = this._expandZFor(this._def._externalTemplate);
1063
+ // Then do global {{expression}} interpolation on the remaining content
1064
+ html = html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
989
1065
  try {
990
1066
  return new Function('state', 'props', '$', `with(state){return ${expr.trim()}}`)(
991
1067
  this.state.__raw || this.state,
@@ -1018,16 +1094,35 @@ class Component {
1018
1094
  this._styleEl = styleEl;
1019
1095
  }
1020
1096
 
1021
- // ── Focus preservation for z-model ────────────────────────────
1097
+ // -- Focus preservation ----------------------------------------
1022
1098
  // Before replacing innerHTML, save focus state so we can restore
1023
- // cursor position after the DOM is rebuilt.
1099
+ // cursor position after the DOM is rebuilt. Works for any focused
1100
+ // input/textarea/select inside the component, not only z-model.
1024
1101
  let _focusInfo = null;
1025
1102
  const _active = document.activeElement;
1026
1103
  if (_active && this._el.contains(_active)) {
1027
1104
  const modelKey = _active.getAttribute?.('z-model');
1105
+ const refKey = _active.getAttribute?.('z-ref');
1106
+ // Build a selector that can locate the same element after re-render
1107
+ let selector = null;
1028
1108
  if (modelKey) {
1109
+ selector = `[z-model="${modelKey}"]`;
1110
+ } else if (refKey) {
1111
+ selector = `[z-ref="${refKey}"]`;
1112
+ } else {
1113
+ // Fallback: match by tag + type + name + placeholder combination
1114
+ const tag = _active.tagName.toLowerCase();
1115
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
1116
+ let s = tag;
1117
+ if (_active.type) s += `[type="${_active.type}"]`;
1118
+ if (_active.name) s += `[name="${_active.name}"]`;
1119
+ if (_active.placeholder) s += `[placeholder="${CSS.escape(_active.placeholder)}"]`;
1120
+ selector = s;
1121
+ }
1122
+ }
1123
+ if (selector) {
1029
1124
  _focusInfo = {
1030
- key: modelKey,
1125
+ selector,
1031
1126
  start: _active.selectionStart,
1032
1127
  end: _active.selectionEnd,
1033
1128
  dir: _active.selectionDirection,
@@ -1038,14 +1133,17 @@ class Component {
1038
1133
  // Update DOM
1039
1134
  this._el.innerHTML = html;
1040
1135
 
1041
- // Process directives
1136
+ // Process structural & attribute directives
1137
+ this._processDirectives();
1138
+
1139
+ // Process event, ref, and model bindings
1042
1140
  this._bindEvents();
1043
1141
  this._bindRefs();
1044
1142
  this._bindModels();
1045
1143
 
1046
- // Restore focus to z-model element after re-render
1144
+ // Restore focus after re-render
1047
1145
  if (_focusInfo) {
1048
- const el = this._el.querySelector(`[z-model="${_focusInfo.key}"]`);
1146
+ const el = this._el.querySelector(_focusInfo.selector);
1049
1147
  if (el) {
1050
1148
  el.focus();
1051
1149
  try {
@@ -1067,7 +1165,7 @@ class Component {
1067
1165
  }
1068
1166
  }
1069
1167
 
1070
- // Bind @event="method" handlers via delegation
1168
+ // Bind @event="method" and z-on:event="method" handlers via delegation
1071
1169
  _bindEvents() {
1072
1170
  // Clean up old delegated listeners
1073
1171
  this._listeners.forEach(({ event, handler }) => {
@@ -1075,15 +1173,25 @@ class Component {
1075
1173
  });
1076
1174
  this._listeners = [];
1077
1175
 
1078
- // Find all elements with @event attributes
1176
+ // Find all elements with @event or z-on:event attributes
1079
1177
  const allEls = this._el.querySelectorAll('*');
1080
1178
  const eventMap = new Map(); // event → [{ selector, method, modifiers }]
1081
1179
 
1082
1180
  allEls.forEach(child => {
1181
+ // Skip elements inside z-pre subtrees
1182
+ if (child.closest('[z-pre]')) return;
1183
+
1083
1184
  [...child.attributes].forEach(attr => {
1084
- if (!attr.name.startsWith('@')) return;
1185
+ // Support both @event and z-on:event syntax
1186
+ let raw;
1187
+ if (attr.name.startsWith('@')) {
1188
+ raw = attr.name.slice(1); // @click.prevent → click.prevent
1189
+ } else if (attr.name.startsWith('z-on:')) {
1190
+ raw = attr.name.slice(5); // z-on:click.prevent → click.prevent
1191
+ } else {
1192
+ return;
1193
+ }
1085
1194
 
1086
- const raw = attr.name.slice(1); // e.g. "click.prevent"
1087
1195
  const parts = raw.split('.');
1088
1196
  const event = parts[0];
1089
1197
  const modifiers = parts.slice(1);
@@ -1102,43 +1210,83 @@ class Component {
1102
1210
 
1103
1211
  // Register delegated listeners on the component root
1104
1212
  for (const [event, bindings] of eventMap) {
1213
+ // Determine listener options from modifiers
1214
+ const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
1215
+ const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
1216
+ const listenerOpts = (needsCapture || needsPassive)
1217
+ ? { capture: needsCapture, passive: needsPassive }
1218
+ : false;
1219
+
1105
1220
  const handler = (e) => {
1106
1221
  for (const { selector, methodExpr, modifiers, el } of bindings) {
1107
1222
  if (!e.target.closest(selector)) continue;
1108
1223
 
1224
+ // .self — only fire if target is the element itself
1225
+ if (modifiers.includes('self') && e.target !== el) continue;
1226
+
1109
1227
  // Handle modifiers
1110
1228
  if (modifiers.includes('prevent')) e.preventDefault();
1111
1229
  if (modifiers.includes('stop')) e.stopPropagation();
1112
1230
 
1113
- // Parse method expression: "method" or "method(arg1, arg2)"
1114
- const match = methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
1115
- if (match) {
1231
+ // Build the invocation function
1232
+ const invoke = (evt) => {
1233
+ const match = methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
1234
+ if (!match) return;
1116
1235
  const methodName = match[1];
1117
1236
  const fn = this[methodName];
1118
- if (typeof fn === 'function') {
1119
- if (match[2] !== undefined) {
1120
- // Parse arguments (supports strings, numbers, state refs)
1121
- const args = match[2].split(',').map(a => {
1122
- a = a.trim();
1123
- if (a === '') return undefined;
1124
- if (a === 'true') return true;
1125
- if (a === 'false') return false;
1126
- if (a === 'null') return null;
1127
- if (/^-?\d+(\.\d+)?$/.test(a)) return Number(a);
1128
- if ((a.startsWith("'") && a.endsWith("'")) || (a.startsWith('"') && a.endsWith('"'))) return a.slice(1, -1);
1129
- // State reference
1130
- if (a.startsWith('state.')) return this.state[a.slice(6)];
1131
- return a;
1132
- }).filter(a => a !== undefined);
1133
- fn(e, ...args);
1134
- } else {
1135
- fn(e);
1136
- }
1237
+ if (typeof fn !== 'function') return;
1238
+ if (match[2] !== undefined) {
1239
+ const args = match[2].split(',').map(a => {
1240
+ a = a.trim();
1241
+ if (a === '') return undefined;
1242
+ if (a === '$event') return evt;
1243
+ if (a === 'true') return true;
1244
+ if (a === 'false') return false;
1245
+ if (a === 'null') return null;
1246
+ if (/^-?\d+(\.\d+)?$/.test(a)) return Number(a);
1247
+ if ((a.startsWith("'") && a.endsWith("'")) || (a.startsWith('"') && a.endsWith('"'))) return a.slice(1, -1);
1248
+ if (a.startsWith('state.')) return _getPath(this.state, a.slice(6));
1249
+ return a;
1250
+ }).filter(a => a !== undefined);
1251
+ fn(...args);
1252
+ } else {
1253
+ fn(evt);
1137
1254
  }
1255
+ };
1256
+
1257
+ // .debounce.{ms} — delay invocation until idle
1258
+ const debounceIdx = modifiers.indexOf('debounce');
1259
+ if (debounceIdx !== -1) {
1260
+ const ms = parseInt(modifiers[debounceIdx + 1], 10) || 250;
1261
+ const timers = _debounceTimers.get(el) || {};
1262
+ clearTimeout(timers[event]);
1263
+ timers[event] = setTimeout(() => invoke(e), ms);
1264
+ _debounceTimers.set(el, timers);
1265
+ continue;
1138
1266
  }
1267
+
1268
+ // .throttle.{ms} — fire at most once per interval
1269
+ const throttleIdx = modifiers.indexOf('throttle');
1270
+ if (throttleIdx !== -1) {
1271
+ const ms = parseInt(modifiers[throttleIdx + 1], 10) || 250;
1272
+ const timers = _throttleTimers.get(el) || {};
1273
+ if (timers[event]) continue;
1274
+ invoke(e);
1275
+ timers[event] = setTimeout(() => { timers[event] = null; }, ms);
1276
+ _throttleTimers.set(el, timers);
1277
+ continue;
1278
+ }
1279
+
1280
+ // .once — fire once then ignore
1281
+ if (modifiers.includes('once')) {
1282
+ if (el.dataset.zqOnce === event) continue;
1283
+ el.dataset.zqOnce = event;
1284
+ }
1285
+
1286
+ invoke(e);
1139
1287
  }
1140
1288
  };
1141
- this._el.addEventListener(event, handler);
1289
+ this._el.addEventListener(event, handler, listenerOpts);
1142
1290
  this._listeners.push({ event, handler });
1143
1291
  }
1144
1292
  }
@@ -1179,7 +1327,7 @@ class Component {
1179
1327
  // Read current state value (supports dot-path keys)
1180
1328
  const currentVal = _getPath(this.state, key);
1181
1329
 
1182
- // ── Set initial DOM value from state ────────────────────────
1330
+ // -- Set initial DOM value from state ------------------------
1183
1331
  if (tag === 'input' && type === 'checkbox') {
1184
1332
  el.checked = !!currentVal;
1185
1333
  } else if (tag === 'input' && type === 'radio') {
@@ -1195,12 +1343,12 @@ class Component {
1195
1343
  el.value = currentVal ?? '';
1196
1344
  }
1197
1345
 
1198
- // ── Determine event type ────────────────────────────────────
1346
+ // -- Determine event type ------------------------------------
1199
1347
  const event = isLazy || tag === 'select' || type === 'checkbox' || type === 'radio'
1200
1348
  ? 'change'
1201
1349
  : isEditable ? 'input' : 'input';
1202
1350
 
1203
- // ── Handler: read DOM → write to reactive state ─────────────
1351
+ // -- Handler: read DOM → write to reactive state -------------
1204
1352
  const handler = () => {
1205
1353
  let val;
1206
1354
  if (type === 'checkbox') val = el.checked;
@@ -1221,9 +1369,240 @@ class Component {
1221
1369
  });
1222
1370
  }
1223
1371
 
1372
+ // ---------------------------------------------------------------------------
1373
+ // Expression evaluator — runs expr in component context (state, props, refs)
1374
+ // ---------------------------------------------------------------------------
1375
+ _evalExpr(expr) {
1376
+ try {
1377
+ return new Function('state', 'props', 'refs', '$',
1378
+ `with(state){return (${expr})}`)(
1379
+ this.state.__raw || this.state,
1380
+ this.props,
1381
+ this.refs,
1382
+ typeof window !== 'undefined' ? window.$ : undefined
1383
+ );
1384
+ } catch { return undefined; }
1385
+ }
1386
+
1387
+ // ---------------------------------------------------------------------------
1388
+ // z-for — Expand list-rendering directives (pre-innerHTML, string level)
1389
+ //
1390
+ // <li z-for="item in items">{{item.name}}</li>
1391
+ // <li z-for="(item, i) in items">{{i}}: {{item.name}}</li>
1392
+ // <div z-for="n in 5">{{n}}</div> (range)
1393
+ // <div z-for="(val, key) in obj">{{key}}: {{val}}</div> (object)
1394
+ //
1395
+ // Uses a temporary DOM to parse, clone elements per item, and evaluate
1396
+ // {{}} expressions with the iteration variable in scope.
1397
+ // ---------------------------------------------------------------------------
1398
+ _expandZFor(html) {
1399
+ if (!html.includes('z-for')) return html;
1400
+
1401
+ const temp = document.createElement('div');
1402
+ temp.innerHTML = html;
1403
+
1404
+ const _recurse = (root) => {
1405
+ // Process innermost z-for elements first (no nested z-for inside)
1406
+ let forEls = [...root.querySelectorAll('[z-for]')]
1407
+ .filter(el => !el.querySelector('[z-for]'));
1408
+ if (!forEls.length) return;
1409
+
1410
+ for (const el of forEls) {
1411
+ if (!el.parentNode) continue; // already removed
1412
+ const expr = el.getAttribute('z-for');
1413
+ const m = expr.match(
1414
+ /^\s*(?:\(\s*(\w+)(?:\s*,\s*(\w+))?\s*\)|(\w+))\s+in\s+(.+)\s*$/
1415
+ );
1416
+ if (!m) { el.removeAttribute('z-for'); continue; }
1417
+
1418
+ const itemVar = m[1] || m[3];
1419
+ const indexVar = m[2] || '$index';
1420
+ const listExpr = m[4].trim();
1421
+
1422
+ let list = this._evalExpr(listExpr);
1423
+ if (list == null) { el.remove(); continue; }
1424
+ // Number range: z-for="n in 5" → [1, 2, 3, 4, 5]
1425
+ if (typeof list === 'number') {
1426
+ list = Array.from({ length: list }, (_, i) => i + 1);
1427
+ }
1428
+ // Object iteration: z-for="(val, key) in obj" → entries
1429
+ if (!Array.isArray(list) && typeof list === 'object' && typeof list[Symbol.iterator] !== 'function') {
1430
+ list = Object.entries(list).map(([k, v]) => ({ key: k, value: v }));
1431
+ }
1432
+ if (!Array.isArray(list) && typeof list[Symbol.iterator] === 'function') {
1433
+ list = [...list];
1434
+ }
1435
+ if (!Array.isArray(list)) { el.remove(); continue; }
1436
+
1437
+ const parent = el.parentNode;
1438
+ const tplEl = el.cloneNode(true);
1439
+ tplEl.removeAttribute('z-for');
1440
+ const tplOuter = tplEl.outerHTML;
1441
+
1442
+ const fragment = document.createDocumentFragment();
1443
+ const evalReplace = (str, item, index) =>
1444
+ str.replace(/\{\{(.+?)\}\}/g, (_, inner) => {
1445
+ try {
1446
+ return new Function(itemVar, indexVar, 'state', 'props', '$',
1447
+ `with(state){return (${inner.trim()})}`)(
1448
+ item, index,
1449
+ this.state.__raw || this.state,
1450
+ this.props,
1451
+ typeof window !== 'undefined' ? window.$ : undefined
1452
+ );
1453
+ } catch { return ''; }
1454
+ });
1455
+
1456
+ for (let i = 0; i < list.length; i++) {
1457
+ const processed = evalReplace(tplOuter, list[i], i);
1458
+ const wrapper = document.createElement('div');
1459
+ wrapper.innerHTML = processed;
1460
+ while (wrapper.firstChild) fragment.appendChild(wrapper.firstChild);
1461
+ }
1462
+
1463
+ parent.replaceChild(fragment, el);
1464
+ }
1465
+
1466
+ // Handle remaining nested z-for (now exposed)
1467
+ if (root.querySelector('[z-for]')) _recurse(root);
1468
+ };
1469
+
1470
+ _recurse(temp);
1471
+ return temp.innerHTML;
1472
+ }
1473
+
1474
+ // ---------------------------------------------------------------------------
1475
+ // _processDirectives — Post-innerHTML DOM-level directive processing
1476
+ // ---------------------------------------------------------------------------
1477
+ _processDirectives() {
1478
+ // z-pre: skip all directive processing on subtrees
1479
+ // (we leave z-pre elements in the DOM, but skip their descendants)
1480
+
1481
+ // -- z-if / z-else-if / z-else (conditional rendering) --------
1482
+ const ifEls = [...this._el.querySelectorAll('[z-if]')];
1483
+ for (const el of ifEls) {
1484
+ if (!el.parentNode || el.closest('[z-pre]')) continue;
1485
+
1486
+ const show = !!this._evalExpr(el.getAttribute('z-if'));
1487
+
1488
+ // Collect chain: adjacent z-else-if / z-else siblings
1489
+ const chain = [{ el, show }];
1490
+ let sib = el.nextElementSibling;
1491
+ while (sib) {
1492
+ if (sib.hasAttribute('z-else-if')) {
1493
+ chain.push({ el: sib, show: !!this._evalExpr(sib.getAttribute('z-else-if')) });
1494
+ sib = sib.nextElementSibling;
1495
+ } else if (sib.hasAttribute('z-else')) {
1496
+ chain.push({ el: sib, show: true });
1497
+ break;
1498
+ } else {
1499
+ break;
1500
+ }
1501
+ }
1502
+
1503
+ // Keep the first truthy branch, remove the rest
1504
+ let found = false;
1505
+ for (const item of chain) {
1506
+ if (!found && item.show) {
1507
+ found = true;
1508
+ item.el.removeAttribute('z-if');
1509
+ item.el.removeAttribute('z-else-if');
1510
+ item.el.removeAttribute('z-else');
1511
+ } else {
1512
+ item.el.remove();
1513
+ }
1514
+ }
1515
+ }
1516
+
1517
+ // -- z-show (toggle display) -----------------------------------
1518
+ this._el.querySelectorAll('[z-show]').forEach(el => {
1519
+ if (el.closest('[z-pre]')) return;
1520
+ const show = !!this._evalExpr(el.getAttribute('z-show'));
1521
+ el.style.display = show ? '' : 'none';
1522
+ el.removeAttribute('z-show');
1523
+ });
1524
+
1525
+ // -- z-bind:attr / :attr (dynamic attribute binding) -----------
1526
+ this._el.querySelectorAll('*').forEach(el => {
1527
+ if (el.closest('[z-pre]')) return;
1528
+ [...el.attributes].forEach(attr => {
1529
+ let attrName;
1530
+ if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
1531
+ else if (attr.name.startsWith(':') && !attr.name.startsWith('::')) attrName = attr.name.slice(1);
1532
+ else return;
1533
+
1534
+ const val = this._evalExpr(attr.value);
1535
+ el.removeAttribute(attr.name);
1536
+ if (val === false || val === null || val === undefined) {
1537
+ el.removeAttribute(attrName);
1538
+ } else if (val === true) {
1539
+ el.setAttribute(attrName, '');
1540
+ } else {
1541
+ el.setAttribute(attrName, String(val));
1542
+ }
1543
+ });
1544
+ });
1545
+
1546
+ // -- z-class (dynamic class binding) ---------------------------
1547
+ this._el.querySelectorAll('[z-class]').forEach(el => {
1548
+ if (el.closest('[z-pre]')) return;
1549
+ const val = this._evalExpr(el.getAttribute('z-class'));
1550
+ if (typeof val === 'string') {
1551
+ val.split(/\s+/).filter(Boolean).forEach(c => el.classList.add(c));
1552
+ } else if (Array.isArray(val)) {
1553
+ val.filter(Boolean).forEach(c => el.classList.add(String(c)));
1554
+ } else if (val && typeof val === 'object') {
1555
+ for (const [cls, active] of Object.entries(val)) {
1556
+ el.classList.toggle(cls, !!active);
1557
+ }
1558
+ }
1559
+ el.removeAttribute('z-class');
1560
+ });
1561
+
1562
+ // -- z-style (dynamic inline styles) ---------------------------
1563
+ this._el.querySelectorAll('[z-style]').forEach(el => {
1564
+ if (el.closest('[z-pre]')) return;
1565
+ const val = this._evalExpr(el.getAttribute('z-style'));
1566
+ if (typeof val === 'string') {
1567
+ el.style.cssText += ';' + val;
1568
+ } else if (val && typeof val === 'object') {
1569
+ for (const [prop, v] of Object.entries(val)) {
1570
+ el.style[prop] = v;
1571
+ }
1572
+ }
1573
+ el.removeAttribute('z-style');
1574
+ });
1575
+
1576
+ // -- z-html (innerHTML injection) ------------------------------
1577
+ this._el.querySelectorAll('[z-html]').forEach(el => {
1578
+ if (el.closest('[z-pre]')) return;
1579
+ const val = this._evalExpr(el.getAttribute('z-html'));
1580
+ el.innerHTML = val != null ? String(val) : '';
1581
+ el.removeAttribute('z-html');
1582
+ });
1583
+
1584
+ // -- z-text (safe textContent binding) -------------------------
1585
+ this._el.querySelectorAll('[z-text]').forEach(el => {
1586
+ if (el.closest('[z-pre]')) return;
1587
+ const val = this._evalExpr(el.getAttribute('z-text'));
1588
+ el.textContent = val != null ? String(val) : '';
1589
+ el.removeAttribute('z-text');
1590
+ });
1591
+
1592
+ // -- z-cloak (remove after render) -----------------------------
1593
+ this._el.querySelectorAll('[z-cloak]').forEach(el => {
1594
+ el.removeAttribute('z-cloak');
1595
+ });
1596
+ }
1597
+
1224
1598
  // Programmatic state update (batch-friendly)
1599
+ // Passing an empty object forces a re-render (useful for external state changes).
1225
1600
  setState(partial) {
1226
- Object.assign(this.state, partial);
1601
+ if (partial && Object.keys(partial).length > 0) {
1602
+ Object.assign(this.state, partial);
1603
+ } else {
1604
+ this._scheduleUpdate();
1605
+ }
1227
1606
  }
1228
1607
 
1229
1608
  // Emit custom event up the DOM
@@ -1481,9 +1860,9 @@ function style(urls, opts = {}) {
1481
1860
  class Router {
1482
1861
  constructor(config = {}) {
1483
1862
  this._el = null;
1484
- // Auto-detect: file:// protocol can't use pushState, fall back to hash
1863
+ // file:// protocol can't use pushState always force hash mode
1485
1864
  const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
1486
- this._mode = config.mode || (isFile ? 'hash' : 'history');
1865
+ this._mode = isFile ? 'hash' : (config.mode || 'history');
1487
1866
 
1488
1867
  // Base path for sub-path deployments
1489
1868
  // Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
@@ -2450,14 +2829,14 @@ const bus = new EventBus();
2450
2829
 
2451
2830
  // --- index.js (assembly) ——————————————————————————————————————————
2452
2831
  /**
2453
- * ┌─────────────────────────────────────────────────────────┐
2832
+ * ┌---------------------------------------------------------┐
2454
2833
  * │ zQuery (zeroQuery) — Lightweight Frontend Library │
2455
2834
  * │ │
2456
2835
  * │ jQuery-like selectors · Reactive components │
2457
2836
  * │ SPA router · State management · Zero dependencies │
2458
2837
  * │ │
2459
2838
  * │ https://github.com/tonywied17/zero-query │
2460
- * └─────────────────────────────────────────────────────────┘
2839
+ * └---------------------------------------------------------┘
2461
2840
  */
2462
2841
 
2463
2842
 
@@ -2493,11 +2872,16 @@ function $(selector, context) {
2493
2872
  }
2494
2873
 
2495
2874
 
2496
- // --- Quick refs ------------------------------------------------------------
2875
+ // --- Quick refs (DOM selectors) --------------------------------------------
2497
2876
  $.id = query.id;
2498
2877
  $.class = query.class;
2499
2878
  $.classes = query.classes;
2500
2879
  $.tag = query.tag;
2880
+ Object.defineProperty($, 'name', {
2881
+ value: query.name, writable: true, configurable: true
2882
+ });
2883
+ $.attr = query.attr;
2884
+ $.data = query.data;
2501
2885
  $.children = query.children;
2502
2886
 
2503
2887
  // --- Collection selector ---------------------------------------------------
@@ -2521,10 +2905,12 @@ $.all = function(selector, context) {
2521
2905
  $.create = query.create;
2522
2906
  $.ready = query.ready;
2523
2907
  $.on = query.on;
2908
+ $.off = query.off;
2524
2909
  $.fn = query.fn;
2525
2910
 
2526
2911
  // --- Reactive primitives ---------------------------------------------------
2527
2912
  $.reactive = reactive;
2913
+ $.Signal = Signal;
2528
2914
  $.signal = signal;
2529
2915
  $.computed = computed;
2530
2916
  $.effect = effect;
@@ -2576,7 +2962,7 @@ $.session = session;
2576
2962
  $.bus = bus;
2577
2963
 
2578
2964
  // --- Meta ------------------------------------------------------------------
2579
- $.version = '0.3.1';
2965
+ $.version = '0.5.2';
2580
2966
  $.meta = {}; // populated at build time by CLI bundler
2581
2967
 
2582
2968
  $.noConflict = () => {