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/README.md +48 -19
- package/cli/args.js +33 -0
- package/cli/commands/build.js +58 -0
- package/cli/commands/bundle.js +687 -0
- package/cli/commands/create.js +67 -0
- package/cli/commands/dev.js +520 -0
- package/cli/help.js +104 -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 +448 -62
- package/dist/zquery.min.js +5 -5
- package/index.d.ts +213 -66
- package/index.js +14 -7
- package/package.json +8 -8
- package/src/component.js +408 -52
- package/src/core.js +26 -3
- package/src/router.js +2 -2
- package/cli.js +0 -1208
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.
|
|
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
|
|
637
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
865
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
988
|
-
html = this._def._externalTemplate
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1144
|
+
// Restore focus after re-render
|
|
1047
1145
|
if (_focusInfo) {
|
|
1048
|
-
const el = this._el.querySelector(
|
|
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
|
-
|
|
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
|
-
//
|
|
1114
|
-
const
|
|
1115
|
-
|
|
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
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
1863
|
+
// file:// protocol can't use pushState — always force hash mode
|
|
1485
1864
|
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
1486
|
-
this._mode =
|
|
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.
|
|
2965
|
+
$.version = '0.5.2';
|
|
2580
2966
|
$.meta = {}; // populated at build time by CLI bundler
|
|
2581
2967
|
|
|
2582
2968
|
$.noConflict = () => {
|