zero-query 0.2.9 → 0.4.9
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 +42 -17
- package/cli/args.js +33 -0
- package/cli/commands/build.js +58 -0
- package/cli/commands/bundle.js +584 -0
- package/cli/commands/create.js +67 -0
- package/cli/commands/dev.js +516 -0
- package/cli/help.js +92 -0
- package/cli/index.js +53 -0
- package/cli/scaffold/LICENSE +21 -0
- package/cli/scaffold/index.html +62 -0
- package/cli/scaffold/scripts/app.js +101 -0
- package/cli/scaffold/scripts/components/about.js +119 -0
- package/cli/scaffold/scripts/components/api-demo.js +103 -0
- package/cli/scaffold/scripts/components/contacts/contacts.css +253 -0
- package/cli/scaffold/scripts/components/contacts/contacts.html +139 -0
- package/cli/scaffold/scripts/components/contacts/contacts.js +137 -0
- package/cli/scaffold/scripts/components/counter.js +65 -0
- package/cli/scaffold/scripts/components/home.js +137 -0
- package/cli/scaffold/scripts/components/not-found.js +16 -0
- package/cli/scaffold/scripts/components/todos.js +130 -0
- package/cli/scaffold/scripts/routes.js +13 -0
- package/cli/scaffold/scripts/store.js +96 -0
- package/cli/scaffold/styles/styles.css +556 -0
- package/cli/utils.js +122 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +431 -61
- package/dist/zquery.min.js +5 -5
- package/index.d.ts +206 -66
- package/index.js +5 -4
- package/package.json +8 -8
- package/src/component.js +408 -52
- package/src/core.js +16 -3
- package/src/router.js +2 -2
- package/cli.js +0 -1200
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.
|
|
2
|
+
* zQuery (zeroQuery) v0.4.9
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman — MIT License
|
|
@@ -633,14 +633,27 @@ query.ready = (fn) => {
|
|
|
633
633
|
else document.addEventListener('DOMContentLoaded', fn);
|
|
634
634
|
};
|
|
635
635
|
|
|
636
|
-
// Global event
|
|
637
|
-
|
|
636
|
+
// Global event listeners — supports direct and delegated forms
|
|
637
|
+
// $.on('keydown', handler) → direct listener on document
|
|
638
|
+
// $.on('click', '.btn', handler) → delegated via closest()
|
|
639
|
+
query.on = (event, selectorOrHandler, handler) => {
|
|
640
|
+
if (typeof selectorOrHandler === 'function') {
|
|
641
|
+
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
642
|
+
document.addEventListener(event, selectorOrHandler);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// 3-arg: delegated
|
|
638
646
|
document.addEventListener(event, (e) => {
|
|
639
|
-
const target = e.target.closest(
|
|
647
|
+
const target = e.target.closest(selectorOrHandler);
|
|
640
648
|
if (target) handler.call(target, e);
|
|
641
649
|
});
|
|
642
650
|
};
|
|
643
651
|
|
|
652
|
+
// Remove a direct global listener
|
|
653
|
+
query.off = (event, handler) => {
|
|
654
|
+
document.removeEventListener(event, handler);
|
|
655
|
+
};
|
|
656
|
+
|
|
644
657
|
// Extend collection prototype (like $.fn in jQuery)
|
|
645
658
|
query.fn = ZQueryCollection.prototype;
|
|
646
659
|
|
|
@@ -648,7 +661,7 @@ query.fn = ZQueryCollection.prototype;
|
|
|
648
661
|
/**
|
|
649
662
|
* zQuery Component — Lightweight reactive component system
|
|
650
663
|
*
|
|
651
|
-
* Declarative components using template literals
|
|
664
|
+
* Declarative components using template literals with directive support.
|
|
652
665
|
* Proxy-based state triggers targeted re-renders via event delegation.
|
|
653
666
|
*
|
|
654
667
|
* Features:
|
|
@@ -677,6 +690,18 @@ const _resourceCache = new Map(); // url → Promise<string>
|
|
|
677
690
|
// Unique ID counter
|
|
678
691
|
let _uid = 0;
|
|
679
692
|
|
|
693
|
+
// Inject z-cloak base style and mobile tap-highlight reset (once, globally)
|
|
694
|
+
if (typeof document !== 'undefined' && !document.querySelector('[data-zq-cloak]')) {
|
|
695
|
+
const _s = document.createElement('style');
|
|
696
|
+
_s.textContent = '[z-cloak]{display:none!important}*,*::before,*::after{-webkit-tap-highlight-color:transparent}';
|
|
697
|
+
_s.setAttribute('data-zq-cloak', '');
|
|
698
|
+
document.head.appendChild(_s);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Debounce / throttle helpers for event modifiers
|
|
702
|
+
const _debounceTimers = new WeakMap();
|
|
703
|
+
const _throttleTimers = new WeakMap();
|
|
704
|
+
|
|
680
705
|
/**
|
|
681
706
|
* Fetch and cache a text resource (HTML template or CSS file).
|
|
682
707
|
* @param {string} url — URL to fetch
|
|
@@ -861,8 +886,11 @@ class Component {
|
|
|
861
886
|
if (this._updateQueued) return;
|
|
862
887
|
this._updateQueued = true;
|
|
863
888
|
queueMicrotask(() => {
|
|
864
|
-
|
|
865
|
-
|
|
889
|
+
try {
|
|
890
|
+
if (!this._destroyed) this._render();
|
|
891
|
+
} finally {
|
|
892
|
+
this._updateQueued = false;
|
|
893
|
+
}
|
|
866
894
|
});
|
|
867
895
|
}
|
|
868
896
|
|
|
@@ -890,12 +918,14 @@ class Component {
|
|
|
890
918
|
// items: ['page-a', { id: 'page-b', label: 'Page B' }, ...]
|
|
891
919
|
// }
|
|
892
920
|
// Exposes this.pages (array of {id,label}), this.activePage (current id)
|
|
921
|
+
// Pages are lazy-loaded: only the active page is fetched on first render,
|
|
922
|
+
// remaining pages are prefetched in the background for instant navigation.
|
|
893
923
|
//
|
|
894
924
|
async _loadExternals() {
|
|
895
925
|
const def = this._def;
|
|
896
926
|
const base = def._base; // auto-detected or explicit
|
|
897
927
|
|
|
898
|
-
//
|
|
928
|
+
// -- Pages config ---------------------------------------------
|
|
899
929
|
if (def.pages && !def._pagesNormalized) {
|
|
900
930
|
const p = def.pages;
|
|
901
931
|
const ext = p.ext || '.html';
|
|
@@ -907,18 +937,20 @@ class Component {
|
|
|
907
937
|
return { id: item.id, label: item.label || _titleCase(item.id) };
|
|
908
938
|
});
|
|
909
939
|
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
940
|
+
// Build URL map for lazy per-page loading.
|
|
941
|
+
// Pages are fetched on demand (active page first, rest prefetched in
|
|
942
|
+
// the background) so the component renders as soon as the visible
|
|
943
|
+
// page is ready instead of waiting for every page to download.
|
|
944
|
+
def._pageUrls = {};
|
|
945
|
+
for (const { id } of def._pages) {
|
|
946
|
+
def._pageUrls[id] = `${dir}/${id}${ext}`;
|
|
916
947
|
}
|
|
948
|
+
if (!def._externalTemplates) def._externalTemplates = {};
|
|
917
949
|
|
|
918
950
|
def._pagesNormalized = true;
|
|
919
951
|
}
|
|
920
952
|
|
|
921
|
-
//
|
|
953
|
+
// -- External templates --------------------------------------
|
|
922
954
|
if (def.templateUrl && !def._templateLoaded) {
|
|
923
955
|
const tu = def.templateUrl;
|
|
924
956
|
if (typeof tu === 'string') {
|
|
@@ -940,7 +972,7 @@ class Component {
|
|
|
940
972
|
def._templateLoaded = true;
|
|
941
973
|
}
|
|
942
974
|
|
|
943
|
-
//
|
|
975
|
+
// -- External styles -----------------------------------------
|
|
944
976
|
if (def.styleUrl && !def._styleLoaded) {
|
|
945
977
|
const su = def.styleUrl;
|
|
946
978
|
if (typeof su === 'string') {
|
|
@@ -975,7 +1007,37 @@ class Component {
|
|
|
975
1007
|
if (this._def._pages) {
|
|
976
1008
|
this.pages = this._def._pages;
|
|
977
1009
|
const pc = this._def.pages;
|
|
978
|
-
|
|
1010
|
+
let active = (pc.param && this.props.$params?.[pc.param]) || pc.default || this._def._pages[0]?.id || '';
|
|
1011
|
+
|
|
1012
|
+
// Fall back to default if the param doesn't match any known page
|
|
1013
|
+
if (this._def._pageUrls && !(active in this._def._pageUrls)) {
|
|
1014
|
+
active = pc.default || this._def._pages[0]?.id || '';
|
|
1015
|
+
}
|
|
1016
|
+
this.activePage = active;
|
|
1017
|
+
|
|
1018
|
+
// Lazy-load: fetch only the active page's template on demand
|
|
1019
|
+
if (this._def._pageUrls && !(active in this._def._externalTemplates)) {
|
|
1020
|
+
const url = this._def._pageUrls[active];
|
|
1021
|
+
if (url) {
|
|
1022
|
+
_fetchResource(url).then(html => {
|
|
1023
|
+
this._def._externalTemplates[active] = html;
|
|
1024
|
+
if (!this._destroyed) this._render();
|
|
1025
|
+
});
|
|
1026
|
+
return; // Wait for active page before rendering
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Prefetch remaining pages in background (once, after active page is ready)
|
|
1031
|
+
if (this._def._pageUrls && !this._def._pagesPrefetched) {
|
|
1032
|
+
this._def._pagesPrefetched = true;
|
|
1033
|
+
for (const [id, url] of Object.entries(this._def._pageUrls)) {
|
|
1034
|
+
if (!(id in this._def._externalTemplates)) {
|
|
1035
|
+
_fetchResource(url).then(html => {
|
|
1036
|
+
this._def._externalTemplates[id] = html;
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
979
1041
|
}
|
|
980
1042
|
|
|
981
1043
|
// Determine HTML content
|
|
@@ -983,9 +1045,13 @@ class Component {
|
|
|
983
1045
|
if (this._def.render) {
|
|
984
1046
|
// Inline render function takes priority
|
|
985
1047
|
html = this._def.render.call(this);
|
|
1048
|
+
// Expand z-for in render templates ({{}} expressions for iteration items)
|
|
1049
|
+
html = this._expandZFor(html);
|
|
986
1050
|
} else if (this._def._externalTemplate) {
|
|
987
|
-
//
|
|
988
|
-
html = this._def._externalTemplate
|
|
1051
|
+
// Expand z-for FIRST (before global {{}} interpolation)
|
|
1052
|
+
html = this._expandZFor(this._def._externalTemplate);
|
|
1053
|
+
// Then do global {{expression}} interpolation on the remaining content
|
|
1054
|
+
html = html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
989
1055
|
try {
|
|
990
1056
|
return new Function('state', 'props', '$', `with(state){return ${expr.trim()}}`)(
|
|
991
1057
|
this.state.__raw || this.state,
|
|
@@ -1018,16 +1084,35 @@ class Component {
|
|
|
1018
1084
|
this._styleEl = styleEl;
|
|
1019
1085
|
}
|
|
1020
1086
|
|
|
1021
|
-
//
|
|
1087
|
+
// -- Focus preservation ----------------------------------------
|
|
1022
1088
|
// Before replacing innerHTML, save focus state so we can restore
|
|
1023
|
-
// cursor position after the DOM is rebuilt.
|
|
1089
|
+
// cursor position after the DOM is rebuilt. Works for any focused
|
|
1090
|
+
// input/textarea/select inside the component, not only z-model.
|
|
1024
1091
|
let _focusInfo = null;
|
|
1025
1092
|
const _active = document.activeElement;
|
|
1026
1093
|
if (_active && this._el.contains(_active)) {
|
|
1027
1094
|
const modelKey = _active.getAttribute?.('z-model');
|
|
1095
|
+
const refKey = _active.getAttribute?.('z-ref');
|
|
1096
|
+
// Build a selector that can locate the same element after re-render
|
|
1097
|
+
let selector = null;
|
|
1028
1098
|
if (modelKey) {
|
|
1099
|
+
selector = `[z-model="${modelKey}"]`;
|
|
1100
|
+
} else if (refKey) {
|
|
1101
|
+
selector = `[z-ref="${refKey}"]`;
|
|
1102
|
+
} else {
|
|
1103
|
+
// Fallback: match by tag + type + name + placeholder combination
|
|
1104
|
+
const tag = _active.tagName.toLowerCase();
|
|
1105
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
1106
|
+
let s = tag;
|
|
1107
|
+
if (_active.type) s += `[type="${_active.type}"]`;
|
|
1108
|
+
if (_active.name) s += `[name="${_active.name}"]`;
|
|
1109
|
+
if (_active.placeholder) s += `[placeholder="${CSS.escape(_active.placeholder)}"]`;
|
|
1110
|
+
selector = s;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (selector) {
|
|
1029
1114
|
_focusInfo = {
|
|
1030
|
-
|
|
1115
|
+
selector,
|
|
1031
1116
|
start: _active.selectionStart,
|
|
1032
1117
|
end: _active.selectionEnd,
|
|
1033
1118
|
dir: _active.selectionDirection,
|
|
@@ -1038,14 +1123,17 @@ class Component {
|
|
|
1038
1123
|
// Update DOM
|
|
1039
1124
|
this._el.innerHTML = html;
|
|
1040
1125
|
|
|
1041
|
-
// Process directives
|
|
1126
|
+
// Process structural & attribute directives
|
|
1127
|
+
this._processDirectives();
|
|
1128
|
+
|
|
1129
|
+
// Process event, ref, and model bindings
|
|
1042
1130
|
this._bindEvents();
|
|
1043
1131
|
this._bindRefs();
|
|
1044
1132
|
this._bindModels();
|
|
1045
1133
|
|
|
1046
|
-
// Restore focus
|
|
1134
|
+
// Restore focus after re-render
|
|
1047
1135
|
if (_focusInfo) {
|
|
1048
|
-
const el = this._el.querySelector(
|
|
1136
|
+
const el = this._el.querySelector(_focusInfo.selector);
|
|
1049
1137
|
if (el) {
|
|
1050
1138
|
el.focus();
|
|
1051
1139
|
try {
|
|
@@ -1067,7 +1155,7 @@ class Component {
|
|
|
1067
1155
|
}
|
|
1068
1156
|
}
|
|
1069
1157
|
|
|
1070
|
-
// Bind @event="method" handlers via delegation
|
|
1158
|
+
// Bind @event="method" and z-on:event="method" handlers via delegation
|
|
1071
1159
|
_bindEvents() {
|
|
1072
1160
|
// Clean up old delegated listeners
|
|
1073
1161
|
this._listeners.forEach(({ event, handler }) => {
|
|
@@ -1075,15 +1163,25 @@ class Component {
|
|
|
1075
1163
|
});
|
|
1076
1164
|
this._listeners = [];
|
|
1077
1165
|
|
|
1078
|
-
// Find all elements with @event attributes
|
|
1166
|
+
// Find all elements with @event or z-on:event attributes
|
|
1079
1167
|
const allEls = this._el.querySelectorAll('*');
|
|
1080
1168
|
const eventMap = new Map(); // event → [{ selector, method, modifiers }]
|
|
1081
1169
|
|
|
1082
1170
|
allEls.forEach(child => {
|
|
1171
|
+
// Skip elements inside z-pre subtrees
|
|
1172
|
+
if (child.closest('[z-pre]')) return;
|
|
1173
|
+
|
|
1083
1174
|
[...child.attributes].forEach(attr => {
|
|
1084
|
-
|
|
1175
|
+
// Support both @event and z-on:event syntax
|
|
1176
|
+
let raw;
|
|
1177
|
+
if (attr.name.startsWith('@')) {
|
|
1178
|
+
raw = attr.name.slice(1); // @click.prevent → click.prevent
|
|
1179
|
+
} else if (attr.name.startsWith('z-on:')) {
|
|
1180
|
+
raw = attr.name.slice(5); // z-on:click.prevent → click.prevent
|
|
1181
|
+
} else {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1085
1184
|
|
|
1086
|
-
const raw = attr.name.slice(1); // e.g. "click.prevent"
|
|
1087
1185
|
const parts = raw.split('.');
|
|
1088
1186
|
const event = parts[0];
|
|
1089
1187
|
const modifiers = parts.slice(1);
|
|
@@ -1102,43 +1200,83 @@ class Component {
|
|
|
1102
1200
|
|
|
1103
1201
|
// Register delegated listeners on the component root
|
|
1104
1202
|
for (const [event, bindings] of eventMap) {
|
|
1203
|
+
// Determine listener options from modifiers
|
|
1204
|
+
const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
|
|
1205
|
+
const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
|
|
1206
|
+
const listenerOpts = (needsCapture || needsPassive)
|
|
1207
|
+
? { capture: needsCapture, passive: needsPassive }
|
|
1208
|
+
: false;
|
|
1209
|
+
|
|
1105
1210
|
const handler = (e) => {
|
|
1106
1211
|
for (const { selector, methodExpr, modifiers, el } of bindings) {
|
|
1107
1212
|
if (!e.target.closest(selector)) continue;
|
|
1108
1213
|
|
|
1214
|
+
// .self — only fire if target is the element itself
|
|
1215
|
+
if (modifiers.includes('self') && e.target !== el) continue;
|
|
1216
|
+
|
|
1109
1217
|
// Handle modifiers
|
|
1110
1218
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
1111
1219
|
if (modifiers.includes('stop')) e.stopPropagation();
|
|
1112
1220
|
|
|
1113
|
-
//
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1221
|
+
// Build the invocation function
|
|
1222
|
+
const invoke = (evt) => {
|
|
1223
|
+
const match = methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
|
|
1224
|
+
if (!match) return;
|
|
1116
1225
|
const methodName = match[1];
|
|
1117
1226
|
const fn = this[methodName];
|
|
1118
|
-
if (typeof fn
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
fn(e);
|
|
1136
|
-
}
|
|
1227
|
+
if (typeof fn !== 'function') return;
|
|
1228
|
+
if (match[2] !== undefined) {
|
|
1229
|
+
const args = match[2].split(',').map(a => {
|
|
1230
|
+
a = a.trim();
|
|
1231
|
+
if (a === '') return undefined;
|
|
1232
|
+
if (a === '$event') return evt;
|
|
1233
|
+
if (a === 'true') return true;
|
|
1234
|
+
if (a === 'false') return false;
|
|
1235
|
+
if (a === 'null') return null;
|
|
1236
|
+
if (/^-?\d+(\.\d+)?$/.test(a)) return Number(a);
|
|
1237
|
+
if ((a.startsWith("'") && a.endsWith("'")) || (a.startsWith('"') && a.endsWith('"'))) return a.slice(1, -1);
|
|
1238
|
+
if (a.startsWith('state.')) return _getPath(this.state, a.slice(6));
|
|
1239
|
+
return a;
|
|
1240
|
+
}).filter(a => a !== undefined);
|
|
1241
|
+
fn(...args);
|
|
1242
|
+
} else {
|
|
1243
|
+
fn(evt);
|
|
1137
1244
|
}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// .debounce.{ms} — delay invocation until idle
|
|
1248
|
+
const debounceIdx = modifiers.indexOf('debounce');
|
|
1249
|
+
if (debounceIdx !== -1) {
|
|
1250
|
+
const ms = parseInt(modifiers[debounceIdx + 1], 10) || 250;
|
|
1251
|
+
const timers = _debounceTimers.get(el) || {};
|
|
1252
|
+
clearTimeout(timers[event]);
|
|
1253
|
+
timers[event] = setTimeout(() => invoke(e), ms);
|
|
1254
|
+
_debounceTimers.set(el, timers);
|
|
1255
|
+
continue;
|
|
1138
1256
|
}
|
|
1257
|
+
|
|
1258
|
+
// .throttle.{ms} — fire at most once per interval
|
|
1259
|
+
const throttleIdx = modifiers.indexOf('throttle');
|
|
1260
|
+
if (throttleIdx !== -1) {
|
|
1261
|
+
const ms = parseInt(modifiers[throttleIdx + 1], 10) || 250;
|
|
1262
|
+
const timers = _throttleTimers.get(el) || {};
|
|
1263
|
+
if (timers[event]) continue;
|
|
1264
|
+
invoke(e);
|
|
1265
|
+
timers[event] = setTimeout(() => { timers[event] = null; }, ms);
|
|
1266
|
+
_throttleTimers.set(el, timers);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// .once — fire once then ignore
|
|
1271
|
+
if (modifiers.includes('once')) {
|
|
1272
|
+
if (el.dataset.zqOnce === event) continue;
|
|
1273
|
+
el.dataset.zqOnce = event;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
invoke(e);
|
|
1139
1277
|
}
|
|
1140
1278
|
};
|
|
1141
|
-
this._el.addEventListener(event, handler);
|
|
1279
|
+
this._el.addEventListener(event, handler, listenerOpts);
|
|
1142
1280
|
this._listeners.push({ event, handler });
|
|
1143
1281
|
}
|
|
1144
1282
|
}
|
|
@@ -1179,7 +1317,7 @@ class Component {
|
|
|
1179
1317
|
// Read current state value (supports dot-path keys)
|
|
1180
1318
|
const currentVal = _getPath(this.state, key);
|
|
1181
1319
|
|
|
1182
|
-
//
|
|
1320
|
+
// -- Set initial DOM value from state ------------------------
|
|
1183
1321
|
if (tag === 'input' && type === 'checkbox') {
|
|
1184
1322
|
el.checked = !!currentVal;
|
|
1185
1323
|
} else if (tag === 'input' && type === 'radio') {
|
|
@@ -1195,12 +1333,12 @@ class Component {
|
|
|
1195
1333
|
el.value = currentVal ?? '';
|
|
1196
1334
|
}
|
|
1197
1335
|
|
|
1198
|
-
//
|
|
1336
|
+
// -- Determine event type ------------------------------------
|
|
1199
1337
|
const event = isLazy || tag === 'select' || type === 'checkbox' || type === 'radio'
|
|
1200
1338
|
? 'change'
|
|
1201
1339
|
: isEditable ? 'input' : 'input';
|
|
1202
1340
|
|
|
1203
|
-
//
|
|
1341
|
+
// -- Handler: read DOM → write to reactive state -------------
|
|
1204
1342
|
const handler = () => {
|
|
1205
1343
|
let val;
|
|
1206
1344
|
if (type === 'checkbox') val = el.checked;
|
|
@@ -1221,9 +1359,240 @@ class Component {
|
|
|
1221
1359
|
});
|
|
1222
1360
|
}
|
|
1223
1361
|
|
|
1362
|
+
// ---------------------------------------------------------------------------
|
|
1363
|
+
// Expression evaluator — runs expr in component context (state, props, refs)
|
|
1364
|
+
// ---------------------------------------------------------------------------
|
|
1365
|
+
_evalExpr(expr) {
|
|
1366
|
+
try {
|
|
1367
|
+
return new Function('state', 'props', 'refs', '$',
|
|
1368
|
+
`with(state){return (${expr})}`)(
|
|
1369
|
+
this.state.__raw || this.state,
|
|
1370
|
+
this.props,
|
|
1371
|
+
this.refs,
|
|
1372
|
+
typeof window !== 'undefined' ? window.$ : undefined
|
|
1373
|
+
);
|
|
1374
|
+
} catch { return undefined; }
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ---------------------------------------------------------------------------
|
|
1378
|
+
// z-for — Expand list-rendering directives (pre-innerHTML, string level)
|
|
1379
|
+
//
|
|
1380
|
+
// <li z-for="item in items">{{item.name}}</li>
|
|
1381
|
+
// <li z-for="(item, i) in items">{{i}}: {{item.name}}</li>
|
|
1382
|
+
// <div z-for="n in 5">{{n}}</div> (range)
|
|
1383
|
+
// <div z-for="(val, key) in obj">{{key}}: {{val}}</div> (object)
|
|
1384
|
+
//
|
|
1385
|
+
// Uses a temporary DOM to parse, clone elements per item, and evaluate
|
|
1386
|
+
// {{}} expressions with the iteration variable in scope.
|
|
1387
|
+
// ---------------------------------------------------------------------------
|
|
1388
|
+
_expandZFor(html) {
|
|
1389
|
+
if (!html.includes('z-for')) return html;
|
|
1390
|
+
|
|
1391
|
+
const temp = document.createElement('div');
|
|
1392
|
+
temp.innerHTML = html;
|
|
1393
|
+
|
|
1394
|
+
const _recurse = (root) => {
|
|
1395
|
+
// Process innermost z-for elements first (no nested z-for inside)
|
|
1396
|
+
let forEls = [...root.querySelectorAll('[z-for]')]
|
|
1397
|
+
.filter(el => !el.querySelector('[z-for]'));
|
|
1398
|
+
if (!forEls.length) return;
|
|
1399
|
+
|
|
1400
|
+
for (const el of forEls) {
|
|
1401
|
+
if (!el.parentNode) continue; // already removed
|
|
1402
|
+
const expr = el.getAttribute('z-for');
|
|
1403
|
+
const m = expr.match(
|
|
1404
|
+
/^\s*(?:\(\s*(\w+)(?:\s*,\s*(\w+))?\s*\)|(\w+))\s+in\s+(.+)\s*$/
|
|
1405
|
+
);
|
|
1406
|
+
if (!m) { el.removeAttribute('z-for'); continue; }
|
|
1407
|
+
|
|
1408
|
+
const itemVar = m[1] || m[3];
|
|
1409
|
+
const indexVar = m[2] || '$index';
|
|
1410
|
+
const listExpr = m[4].trim();
|
|
1411
|
+
|
|
1412
|
+
let list = this._evalExpr(listExpr);
|
|
1413
|
+
if (list == null) { el.remove(); continue; }
|
|
1414
|
+
// Number range: z-for="n in 5" → [1, 2, 3, 4, 5]
|
|
1415
|
+
if (typeof list === 'number') {
|
|
1416
|
+
list = Array.from({ length: list }, (_, i) => i + 1);
|
|
1417
|
+
}
|
|
1418
|
+
// Object iteration: z-for="(val, key) in obj" → entries
|
|
1419
|
+
if (!Array.isArray(list) && typeof list === 'object' && typeof list[Symbol.iterator] !== 'function') {
|
|
1420
|
+
list = Object.entries(list).map(([k, v]) => ({ key: k, value: v }));
|
|
1421
|
+
}
|
|
1422
|
+
if (!Array.isArray(list) && typeof list[Symbol.iterator] === 'function') {
|
|
1423
|
+
list = [...list];
|
|
1424
|
+
}
|
|
1425
|
+
if (!Array.isArray(list)) { el.remove(); continue; }
|
|
1426
|
+
|
|
1427
|
+
const parent = el.parentNode;
|
|
1428
|
+
const tplEl = el.cloneNode(true);
|
|
1429
|
+
tplEl.removeAttribute('z-for');
|
|
1430
|
+
const tplOuter = tplEl.outerHTML;
|
|
1431
|
+
|
|
1432
|
+
const fragment = document.createDocumentFragment();
|
|
1433
|
+
const evalReplace = (str, item, index) =>
|
|
1434
|
+
str.replace(/\{\{(.+?)\}\}/g, (_, inner) => {
|
|
1435
|
+
try {
|
|
1436
|
+
return new Function(itemVar, indexVar, 'state', 'props', '$',
|
|
1437
|
+
`with(state){return (${inner.trim()})}`)(
|
|
1438
|
+
item, index,
|
|
1439
|
+
this.state.__raw || this.state,
|
|
1440
|
+
this.props,
|
|
1441
|
+
typeof window !== 'undefined' ? window.$ : undefined
|
|
1442
|
+
);
|
|
1443
|
+
} catch { return ''; }
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
for (let i = 0; i < list.length; i++) {
|
|
1447
|
+
const processed = evalReplace(tplOuter, list[i], i);
|
|
1448
|
+
const wrapper = document.createElement('div');
|
|
1449
|
+
wrapper.innerHTML = processed;
|
|
1450
|
+
while (wrapper.firstChild) fragment.appendChild(wrapper.firstChild);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
parent.replaceChild(fragment, el);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Handle remaining nested z-for (now exposed)
|
|
1457
|
+
if (root.querySelector('[z-for]')) _recurse(root);
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
_recurse(temp);
|
|
1461
|
+
return temp.innerHTML;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// ---------------------------------------------------------------------------
|
|
1465
|
+
// _processDirectives — Post-innerHTML DOM-level directive processing
|
|
1466
|
+
// ---------------------------------------------------------------------------
|
|
1467
|
+
_processDirectives() {
|
|
1468
|
+
// z-pre: skip all directive processing on subtrees
|
|
1469
|
+
// (we leave z-pre elements in the DOM, but skip their descendants)
|
|
1470
|
+
|
|
1471
|
+
// -- z-if / z-else-if / z-else (conditional rendering) --------
|
|
1472
|
+
const ifEls = [...this._el.querySelectorAll('[z-if]')];
|
|
1473
|
+
for (const el of ifEls) {
|
|
1474
|
+
if (!el.parentNode || el.closest('[z-pre]')) continue;
|
|
1475
|
+
|
|
1476
|
+
const show = !!this._evalExpr(el.getAttribute('z-if'));
|
|
1477
|
+
|
|
1478
|
+
// Collect chain: adjacent z-else-if / z-else siblings
|
|
1479
|
+
const chain = [{ el, show }];
|
|
1480
|
+
let sib = el.nextElementSibling;
|
|
1481
|
+
while (sib) {
|
|
1482
|
+
if (sib.hasAttribute('z-else-if')) {
|
|
1483
|
+
chain.push({ el: sib, show: !!this._evalExpr(sib.getAttribute('z-else-if')) });
|
|
1484
|
+
sib = sib.nextElementSibling;
|
|
1485
|
+
} else if (sib.hasAttribute('z-else')) {
|
|
1486
|
+
chain.push({ el: sib, show: true });
|
|
1487
|
+
break;
|
|
1488
|
+
} else {
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Keep the first truthy branch, remove the rest
|
|
1494
|
+
let found = false;
|
|
1495
|
+
for (const item of chain) {
|
|
1496
|
+
if (!found && item.show) {
|
|
1497
|
+
found = true;
|
|
1498
|
+
item.el.removeAttribute('z-if');
|
|
1499
|
+
item.el.removeAttribute('z-else-if');
|
|
1500
|
+
item.el.removeAttribute('z-else');
|
|
1501
|
+
} else {
|
|
1502
|
+
item.el.remove();
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// -- z-show (toggle display) -----------------------------------
|
|
1508
|
+
this._el.querySelectorAll('[z-show]').forEach(el => {
|
|
1509
|
+
if (el.closest('[z-pre]')) return;
|
|
1510
|
+
const show = !!this._evalExpr(el.getAttribute('z-show'));
|
|
1511
|
+
el.style.display = show ? '' : 'none';
|
|
1512
|
+
el.removeAttribute('z-show');
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
1516
|
+
this._el.querySelectorAll('*').forEach(el => {
|
|
1517
|
+
if (el.closest('[z-pre]')) return;
|
|
1518
|
+
[...el.attributes].forEach(attr => {
|
|
1519
|
+
let attrName;
|
|
1520
|
+
if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
|
|
1521
|
+
else if (attr.name.startsWith(':') && !attr.name.startsWith('::')) attrName = attr.name.slice(1);
|
|
1522
|
+
else return;
|
|
1523
|
+
|
|
1524
|
+
const val = this._evalExpr(attr.value);
|
|
1525
|
+
el.removeAttribute(attr.name);
|
|
1526
|
+
if (val === false || val === null || val === undefined) {
|
|
1527
|
+
el.removeAttribute(attrName);
|
|
1528
|
+
} else if (val === true) {
|
|
1529
|
+
el.setAttribute(attrName, '');
|
|
1530
|
+
} else {
|
|
1531
|
+
el.setAttribute(attrName, String(val));
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
// -- z-class (dynamic class binding) ---------------------------
|
|
1537
|
+
this._el.querySelectorAll('[z-class]').forEach(el => {
|
|
1538
|
+
if (el.closest('[z-pre]')) return;
|
|
1539
|
+
const val = this._evalExpr(el.getAttribute('z-class'));
|
|
1540
|
+
if (typeof val === 'string') {
|
|
1541
|
+
val.split(/\s+/).filter(Boolean).forEach(c => el.classList.add(c));
|
|
1542
|
+
} else if (Array.isArray(val)) {
|
|
1543
|
+
val.filter(Boolean).forEach(c => el.classList.add(String(c)));
|
|
1544
|
+
} else if (val && typeof val === 'object') {
|
|
1545
|
+
for (const [cls, active] of Object.entries(val)) {
|
|
1546
|
+
el.classList.toggle(cls, !!active);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
el.removeAttribute('z-class');
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
// -- z-style (dynamic inline styles) ---------------------------
|
|
1553
|
+
this._el.querySelectorAll('[z-style]').forEach(el => {
|
|
1554
|
+
if (el.closest('[z-pre]')) return;
|
|
1555
|
+
const val = this._evalExpr(el.getAttribute('z-style'));
|
|
1556
|
+
if (typeof val === 'string') {
|
|
1557
|
+
el.style.cssText += ';' + val;
|
|
1558
|
+
} else if (val && typeof val === 'object') {
|
|
1559
|
+
for (const [prop, v] of Object.entries(val)) {
|
|
1560
|
+
el.style[prop] = v;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
el.removeAttribute('z-style');
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// -- z-html (innerHTML injection) ------------------------------
|
|
1567
|
+
this._el.querySelectorAll('[z-html]').forEach(el => {
|
|
1568
|
+
if (el.closest('[z-pre]')) return;
|
|
1569
|
+
const val = this._evalExpr(el.getAttribute('z-html'));
|
|
1570
|
+
el.innerHTML = val != null ? String(val) : '';
|
|
1571
|
+
el.removeAttribute('z-html');
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// -- z-text (safe textContent binding) -------------------------
|
|
1575
|
+
this._el.querySelectorAll('[z-text]').forEach(el => {
|
|
1576
|
+
if (el.closest('[z-pre]')) return;
|
|
1577
|
+
const val = this._evalExpr(el.getAttribute('z-text'));
|
|
1578
|
+
el.textContent = val != null ? String(val) : '';
|
|
1579
|
+
el.removeAttribute('z-text');
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// -- z-cloak (remove after render) -----------------------------
|
|
1583
|
+
this._el.querySelectorAll('[z-cloak]').forEach(el => {
|
|
1584
|
+
el.removeAttribute('z-cloak');
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1224
1588
|
// Programmatic state update (batch-friendly)
|
|
1589
|
+
// Passing an empty object forces a re-render (useful for external state changes).
|
|
1225
1590
|
setState(partial) {
|
|
1226
|
-
Object.
|
|
1591
|
+
if (partial && Object.keys(partial).length > 0) {
|
|
1592
|
+
Object.assign(this.state, partial);
|
|
1593
|
+
} else {
|
|
1594
|
+
this._scheduleUpdate();
|
|
1595
|
+
}
|
|
1227
1596
|
}
|
|
1228
1597
|
|
|
1229
1598
|
// Emit custom event up the DOM
|
|
@@ -1481,9 +1850,9 @@ function style(urls, opts = {}) {
|
|
|
1481
1850
|
class Router {
|
|
1482
1851
|
constructor(config = {}) {
|
|
1483
1852
|
this._el = null;
|
|
1484
|
-
//
|
|
1853
|
+
// file:// protocol can't use pushState — always force hash mode
|
|
1485
1854
|
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
1486
|
-
this._mode =
|
|
1855
|
+
this._mode = isFile ? 'hash' : (config.mode || 'history');
|
|
1487
1856
|
|
|
1488
1857
|
// Base path for sub-path deployments
|
|
1489
1858
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|
|
@@ -2450,14 +2819,14 @@ const bus = new EventBus();
|
|
|
2450
2819
|
|
|
2451
2820
|
// --- index.js (assembly) ——————————————————————————————————————————
|
|
2452
2821
|
/**
|
|
2453
|
-
*
|
|
2822
|
+
* ┌---------------------------------------------------------┐
|
|
2454
2823
|
* │ zQuery (zeroQuery) — Lightweight Frontend Library │
|
|
2455
2824
|
* │ │
|
|
2456
2825
|
* │ jQuery-like selectors · Reactive components │
|
|
2457
2826
|
* │ SPA router · State management · Zero dependencies │
|
|
2458
2827
|
* │ │
|
|
2459
2828
|
* │ https://github.com/tonywied17/zero-query │
|
|
2460
|
-
*
|
|
2829
|
+
* └---------------------------------------------------------┘
|
|
2461
2830
|
*/
|
|
2462
2831
|
|
|
2463
2832
|
|
|
@@ -2521,6 +2890,7 @@ $.all = function(selector, context) {
|
|
|
2521
2890
|
$.create = query.create;
|
|
2522
2891
|
$.ready = query.ready;
|
|
2523
2892
|
$.on = query.on;
|
|
2893
|
+
$.off = query.off;
|
|
2524
2894
|
$.fn = query.fn;
|
|
2525
2895
|
|
|
2526
2896
|
// --- Reactive primitives ---------------------------------------------------
|
|
@@ -2576,7 +2946,7 @@ $.session = session;
|
|
|
2576
2946
|
$.bus = bus;
|
|
2577
2947
|
|
|
2578
2948
|
// --- Meta ------------------------------------------------------------------
|
|
2579
|
-
$.version = '0.
|
|
2949
|
+
$.version = '0.4.9';
|
|
2580
2950
|
$.meta = {}; // populated at build time by CLI bundler
|
|
2581
2951
|
|
|
2582
2952
|
$.noConflict = () => {
|