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/src/component.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zQuery Component — Lightweight reactive component system
|
|
3
3
|
*
|
|
4
|
-
* Declarative components using template literals
|
|
4
|
+
* Declarative components using template literals with directive support.
|
|
5
5
|
* Proxy-based state triggers targeted re-renders via event delegation.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -31,6 +31,18 @@ const _resourceCache = new Map(); // url → Promise<string>
|
|
|
31
31
|
// Unique ID counter
|
|
32
32
|
let _uid = 0;
|
|
33
33
|
|
|
34
|
+
// Inject z-cloak base style and mobile tap-highlight reset (once, globally)
|
|
35
|
+
if (typeof document !== 'undefined' && !document.querySelector('[data-zq-cloak]')) {
|
|
36
|
+
const _s = document.createElement('style');
|
|
37
|
+
_s.textContent = '[z-cloak]{display:none!important}*,*::before,*::after{-webkit-tap-highlight-color:transparent}';
|
|
38
|
+
_s.setAttribute('data-zq-cloak', '');
|
|
39
|
+
document.head.appendChild(_s);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Debounce / throttle helpers for event modifiers
|
|
43
|
+
const _debounceTimers = new WeakMap();
|
|
44
|
+
const _throttleTimers = new WeakMap();
|
|
45
|
+
|
|
34
46
|
/**
|
|
35
47
|
* Fetch and cache a text resource (HTML template or CSS file).
|
|
36
48
|
* @param {string} url — URL to fetch
|
|
@@ -215,8 +227,11 @@ class Component {
|
|
|
215
227
|
if (this._updateQueued) return;
|
|
216
228
|
this._updateQueued = true;
|
|
217
229
|
queueMicrotask(() => {
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
try {
|
|
231
|
+
if (!this._destroyed) this._render();
|
|
232
|
+
} finally {
|
|
233
|
+
this._updateQueued = false;
|
|
234
|
+
}
|
|
220
235
|
});
|
|
221
236
|
}
|
|
222
237
|
|
|
@@ -244,12 +259,14 @@ class Component {
|
|
|
244
259
|
// items: ['page-a', { id: 'page-b', label: 'Page B' }, ...]
|
|
245
260
|
// }
|
|
246
261
|
// Exposes this.pages (array of {id,label}), this.activePage (current id)
|
|
262
|
+
// Pages are lazy-loaded: only the active page is fetched on first render,
|
|
263
|
+
// remaining pages are prefetched in the background for instant navigation.
|
|
247
264
|
//
|
|
248
265
|
async _loadExternals() {
|
|
249
266
|
const def = this._def;
|
|
250
267
|
const base = def._base; // auto-detected or explicit
|
|
251
268
|
|
|
252
|
-
//
|
|
269
|
+
// -- Pages config ---------------------------------------------
|
|
253
270
|
if (def.pages && !def._pagesNormalized) {
|
|
254
271
|
const p = def.pages;
|
|
255
272
|
const ext = p.ext || '.html';
|
|
@@ -261,18 +278,20 @@ class Component {
|
|
|
261
278
|
return { id: item.id, label: item.label || _titleCase(item.id) };
|
|
262
279
|
});
|
|
263
280
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
// Build URL map for lazy per-page loading.
|
|
282
|
+
// Pages are fetched on demand (active page first, rest prefetched in
|
|
283
|
+
// the background) so the component renders as soon as the visible
|
|
284
|
+
// page is ready instead of waiting for every page to download.
|
|
285
|
+
def._pageUrls = {};
|
|
286
|
+
for (const { id } of def._pages) {
|
|
287
|
+
def._pageUrls[id] = `${dir}/${id}${ext}`;
|
|
270
288
|
}
|
|
289
|
+
if (!def._externalTemplates) def._externalTemplates = {};
|
|
271
290
|
|
|
272
291
|
def._pagesNormalized = true;
|
|
273
292
|
}
|
|
274
293
|
|
|
275
|
-
//
|
|
294
|
+
// -- External templates --------------------------------------
|
|
276
295
|
if (def.templateUrl && !def._templateLoaded) {
|
|
277
296
|
const tu = def.templateUrl;
|
|
278
297
|
if (typeof tu === 'string') {
|
|
@@ -294,7 +313,7 @@ class Component {
|
|
|
294
313
|
def._templateLoaded = true;
|
|
295
314
|
}
|
|
296
315
|
|
|
297
|
-
//
|
|
316
|
+
// -- External styles -----------------------------------------
|
|
298
317
|
if (def.styleUrl && !def._styleLoaded) {
|
|
299
318
|
const su = def.styleUrl;
|
|
300
319
|
if (typeof su === 'string') {
|
|
@@ -329,7 +348,37 @@ class Component {
|
|
|
329
348
|
if (this._def._pages) {
|
|
330
349
|
this.pages = this._def._pages;
|
|
331
350
|
const pc = this._def.pages;
|
|
332
|
-
|
|
351
|
+
let active = (pc.param && this.props.$params?.[pc.param]) || pc.default || this._def._pages[0]?.id || '';
|
|
352
|
+
|
|
353
|
+
// Fall back to default if the param doesn't match any known page
|
|
354
|
+
if (this._def._pageUrls && !(active in this._def._pageUrls)) {
|
|
355
|
+
active = pc.default || this._def._pages[0]?.id || '';
|
|
356
|
+
}
|
|
357
|
+
this.activePage = active;
|
|
358
|
+
|
|
359
|
+
// Lazy-load: fetch only the active page's template on demand
|
|
360
|
+
if (this._def._pageUrls && !(active in this._def._externalTemplates)) {
|
|
361
|
+
const url = this._def._pageUrls[active];
|
|
362
|
+
if (url) {
|
|
363
|
+
_fetchResource(url).then(html => {
|
|
364
|
+
this._def._externalTemplates[active] = html;
|
|
365
|
+
if (!this._destroyed) this._render();
|
|
366
|
+
});
|
|
367
|
+
return; // Wait for active page before rendering
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Prefetch remaining pages in background (once, after active page is ready)
|
|
372
|
+
if (this._def._pageUrls && !this._def._pagesPrefetched) {
|
|
373
|
+
this._def._pagesPrefetched = true;
|
|
374
|
+
for (const [id, url] of Object.entries(this._def._pageUrls)) {
|
|
375
|
+
if (!(id in this._def._externalTemplates)) {
|
|
376
|
+
_fetchResource(url).then(html => {
|
|
377
|
+
this._def._externalTemplates[id] = html;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
333
382
|
}
|
|
334
383
|
|
|
335
384
|
// Determine HTML content
|
|
@@ -337,9 +386,13 @@ class Component {
|
|
|
337
386
|
if (this._def.render) {
|
|
338
387
|
// Inline render function takes priority
|
|
339
388
|
html = this._def.render.call(this);
|
|
389
|
+
// Expand z-for in render templates ({{}} expressions for iteration items)
|
|
390
|
+
html = this._expandZFor(html);
|
|
340
391
|
} else if (this._def._externalTemplate) {
|
|
341
|
-
//
|
|
342
|
-
html = this._def._externalTemplate
|
|
392
|
+
// Expand z-for FIRST (before global {{}} interpolation)
|
|
393
|
+
html = this._expandZFor(this._def._externalTemplate);
|
|
394
|
+
// Then do global {{expression}} interpolation on the remaining content
|
|
395
|
+
html = html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
343
396
|
try {
|
|
344
397
|
return new Function('state', 'props', '$', `with(state){return ${expr.trim()}}`)(
|
|
345
398
|
this.state.__raw || this.state,
|
|
@@ -372,16 +425,35 @@ class Component {
|
|
|
372
425
|
this._styleEl = styleEl;
|
|
373
426
|
}
|
|
374
427
|
|
|
375
|
-
//
|
|
428
|
+
// -- Focus preservation ----------------------------------------
|
|
376
429
|
// Before replacing innerHTML, save focus state so we can restore
|
|
377
|
-
// cursor position after the DOM is rebuilt.
|
|
430
|
+
// cursor position after the DOM is rebuilt. Works for any focused
|
|
431
|
+
// input/textarea/select inside the component, not only z-model.
|
|
378
432
|
let _focusInfo = null;
|
|
379
433
|
const _active = document.activeElement;
|
|
380
434
|
if (_active && this._el.contains(_active)) {
|
|
381
435
|
const modelKey = _active.getAttribute?.('z-model');
|
|
436
|
+
const refKey = _active.getAttribute?.('z-ref');
|
|
437
|
+
// Build a selector that can locate the same element after re-render
|
|
438
|
+
let selector = null;
|
|
382
439
|
if (modelKey) {
|
|
440
|
+
selector = `[z-model="${modelKey}"]`;
|
|
441
|
+
} else if (refKey) {
|
|
442
|
+
selector = `[z-ref="${refKey}"]`;
|
|
443
|
+
} else {
|
|
444
|
+
// Fallback: match by tag + type + name + placeholder combination
|
|
445
|
+
const tag = _active.tagName.toLowerCase();
|
|
446
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
447
|
+
let s = tag;
|
|
448
|
+
if (_active.type) s += `[type="${_active.type}"]`;
|
|
449
|
+
if (_active.name) s += `[name="${_active.name}"]`;
|
|
450
|
+
if (_active.placeholder) s += `[placeholder="${CSS.escape(_active.placeholder)}"]`;
|
|
451
|
+
selector = s;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (selector) {
|
|
383
455
|
_focusInfo = {
|
|
384
|
-
|
|
456
|
+
selector,
|
|
385
457
|
start: _active.selectionStart,
|
|
386
458
|
end: _active.selectionEnd,
|
|
387
459
|
dir: _active.selectionDirection,
|
|
@@ -392,14 +464,17 @@ class Component {
|
|
|
392
464
|
// Update DOM
|
|
393
465
|
this._el.innerHTML = html;
|
|
394
466
|
|
|
395
|
-
// Process directives
|
|
467
|
+
// Process structural & attribute directives
|
|
468
|
+
this._processDirectives();
|
|
469
|
+
|
|
470
|
+
// Process event, ref, and model bindings
|
|
396
471
|
this._bindEvents();
|
|
397
472
|
this._bindRefs();
|
|
398
473
|
this._bindModels();
|
|
399
474
|
|
|
400
|
-
// Restore focus
|
|
475
|
+
// Restore focus after re-render
|
|
401
476
|
if (_focusInfo) {
|
|
402
|
-
const el = this._el.querySelector(
|
|
477
|
+
const el = this._el.querySelector(_focusInfo.selector);
|
|
403
478
|
if (el) {
|
|
404
479
|
el.focus();
|
|
405
480
|
try {
|
|
@@ -421,7 +496,7 @@ class Component {
|
|
|
421
496
|
}
|
|
422
497
|
}
|
|
423
498
|
|
|
424
|
-
// Bind @event="method" handlers via delegation
|
|
499
|
+
// Bind @event="method" and z-on:event="method" handlers via delegation
|
|
425
500
|
_bindEvents() {
|
|
426
501
|
// Clean up old delegated listeners
|
|
427
502
|
this._listeners.forEach(({ event, handler }) => {
|
|
@@ -429,15 +504,25 @@ class Component {
|
|
|
429
504
|
});
|
|
430
505
|
this._listeners = [];
|
|
431
506
|
|
|
432
|
-
// Find all elements with @event attributes
|
|
507
|
+
// Find all elements with @event or z-on:event attributes
|
|
433
508
|
const allEls = this._el.querySelectorAll('*');
|
|
434
509
|
const eventMap = new Map(); // event → [{ selector, method, modifiers }]
|
|
435
510
|
|
|
436
511
|
allEls.forEach(child => {
|
|
512
|
+
// Skip elements inside z-pre subtrees
|
|
513
|
+
if (child.closest('[z-pre]')) return;
|
|
514
|
+
|
|
437
515
|
[...child.attributes].forEach(attr => {
|
|
438
|
-
|
|
516
|
+
// Support both @event and z-on:event syntax
|
|
517
|
+
let raw;
|
|
518
|
+
if (attr.name.startsWith('@')) {
|
|
519
|
+
raw = attr.name.slice(1); // @click.prevent → click.prevent
|
|
520
|
+
} else if (attr.name.startsWith('z-on:')) {
|
|
521
|
+
raw = attr.name.slice(5); // z-on:click.prevent → click.prevent
|
|
522
|
+
} else {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
439
525
|
|
|
440
|
-
const raw = attr.name.slice(1); // e.g. "click.prevent"
|
|
441
526
|
const parts = raw.split('.');
|
|
442
527
|
const event = parts[0];
|
|
443
528
|
const modifiers = parts.slice(1);
|
|
@@ -456,43 +541,83 @@ class Component {
|
|
|
456
541
|
|
|
457
542
|
// Register delegated listeners on the component root
|
|
458
543
|
for (const [event, bindings] of eventMap) {
|
|
544
|
+
// Determine listener options from modifiers
|
|
545
|
+
const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
|
|
546
|
+
const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
|
|
547
|
+
const listenerOpts = (needsCapture || needsPassive)
|
|
548
|
+
? { capture: needsCapture, passive: needsPassive }
|
|
549
|
+
: false;
|
|
550
|
+
|
|
459
551
|
const handler = (e) => {
|
|
460
552
|
for (const { selector, methodExpr, modifiers, el } of bindings) {
|
|
461
553
|
if (!e.target.closest(selector)) continue;
|
|
462
554
|
|
|
555
|
+
// .self — only fire if target is the element itself
|
|
556
|
+
if (modifiers.includes('self') && e.target !== el) continue;
|
|
557
|
+
|
|
463
558
|
// Handle modifiers
|
|
464
559
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
465
560
|
if (modifiers.includes('stop')) e.stopPropagation();
|
|
466
561
|
|
|
467
|
-
//
|
|
468
|
-
const
|
|
469
|
-
|
|
562
|
+
// Build the invocation function
|
|
563
|
+
const invoke = (evt) => {
|
|
564
|
+
const match = methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
|
|
565
|
+
if (!match) return;
|
|
470
566
|
const methodName = match[1];
|
|
471
567
|
const fn = this[methodName];
|
|
472
|
-
if (typeof fn
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
fn(e);
|
|
490
|
-
}
|
|
568
|
+
if (typeof fn !== 'function') return;
|
|
569
|
+
if (match[2] !== undefined) {
|
|
570
|
+
const args = match[2].split(',').map(a => {
|
|
571
|
+
a = a.trim();
|
|
572
|
+
if (a === '') return undefined;
|
|
573
|
+
if (a === '$event') return evt;
|
|
574
|
+
if (a === 'true') return true;
|
|
575
|
+
if (a === 'false') return false;
|
|
576
|
+
if (a === 'null') return null;
|
|
577
|
+
if (/^-?\d+(\.\d+)?$/.test(a)) return Number(a);
|
|
578
|
+
if ((a.startsWith("'") && a.endsWith("'")) || (a.startsWith('"') && a.endsWith('"'))) return a.slice(1, -1);
|
|
579
|
+
if (a.startsWith('state.')) return _getPath(this.state, a.slice(6));
|
|
580
|
+
return a;
|
|
581
|
+
}).filter(a => a !== undefined);
|
|
582
|
+
fn(...args);
|
|
583
|
+
} else {
|
|
584
|
+
fn(evt);
|
|
491
585
|
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// .debounce.{ms} — delay invocation until idle
|
|
589
|
+
const debounceIdx = modifiers.indexOf('debounce');
|
|
590
|
+
if (debounceIdx !== -1) {
|
|
591
|
+
const ms = parseInt(modifiers[debounceIdx + 1], 10) || 250;
|
|
592
|
+
const timers = _debounceTimers.get(el) || {};
|
|
593
|
+
clearTimeout(timers[event]);
|
|
594
|
+
timers[event] = setTimeout(() => invoke(e), ms);
|
|
595
|
+
_debounceTimers.set(el, timers);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// .throttle.{ms} — fire at most once per interval
|
|
600
|
+
const throttleIdx = modifiers.indexOf('throttle');
|
|
601
|
+
if (throttleIdx !== -1) {
|
|
602
|
+
const ms = parseInt(modifiers[throttleIdx + 1], 10) || 250;
|
|
603
|
+
const timers = _throttleTimers.get(el) || {};
|
|
604
|
+
if (timers[event]) continue;
|
|
605
|
+
invoke(e);
|
|
606
|
+
timers[event] = setTimeout(() => { timers[event] = null; }, ms);
|
|
607
|
+
_throttleTimers.set(el, timers);
|
|
608
|
+
continue;
|
|
492
609
|
}
|
|
610
|
+
|
|
611
|
+
// .once — fire once then ignore
|
|
612
|
+
if (modifiers.includes('once')) {
|
|
613
|
+
if (el.dataset.zqOnce === event) continue;
|
|
614
|
+
el.dataset.zqOnce = event;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
invoke(e);
|
|
493
618
|
}
|
|
494
619
|
};
|
|
495
|
-
this._el.addEventListener(event, handler);
|
|
620
|
+
this._el.addEventListener(event, handler, listenerOpts);
|
|
496
621
|
this._listeners.push({ event, handler });
|
|
497
622
|
}
|
|
498
623
|
}
|
|
@@ -533,7 +658,7 @@ class Component {
|
|
|
533
658
|
// Read current state value (supports dot-path keys)
|
|
534
659
|
const currentVal = _getPath(this.state, key);
|
|
535
660
|
|
|
536
|
-
//
|
|
661
|
+
// -- Set initial DOM value from state ------------------------
|
|
537
662
|
if (tag === 'input' && type === 'checkbox') {
|
|
538
663
|
el.checked = !!currentVal;
|
|
539
664
|
} else if (tag === 'input' && type === 'radio') {
|
|
@@ -549,12 +674,12 @@ class Component {
|
|
|
549
674
|
el.value = currentVal ?? '';
|
|
550
675
|
}
|
|
551
676
|
|
|
552
|
-
//
|
|
677
|
+
// -- Determine event type ------------------------------------
|
|
553
678
|
const event = isLazy || tag === 'select' || type === 'checkbox' || type === 'radio'
|
|
554
679
|
? 'change'
|
|
555
680
|
: isEditable ? 'input' : 'input';
|
|
556
681
|
|
|
557
|
-
//
|
|
682
|
+
// -- Handler: read DOM → write to reactive state -------------
|
|
558
683
|
const handler = () => {
|
|
559
684
|
let val;
|
|
560
685
|
if (type === 'checkbox') val = el.checked;
|
|
@@ -575,9 +700,240 @@ class Component {
|
|
|
575
700
|
});
|
|
576
701
|
}
|
|
577
702
|
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// Expression evaluator — runs expr in component context (state, props, refs)
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
_evalExpr(expr) {
|
|
707
|
+
try {
|
|
708
|
+
return new Function('state', 'props', 'refs', '$',
|
|
709
|
+
`with(state){return (${expr})}`)(
|
|
710
|
+
this.state.__raw || this.state,
|
|
711
|
+
this.props,
|
|
712
|
+
this.refs,
|
|
713
|
+
typeof window !== 'undefined' ? window.$ : undefined
|
|
714
|
+
);
|
|
715
|
+
} catch { return undefined; }
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// z-for — Expand list-rendering directives (pre-innerHTML, string level)
|
|
720
|
+
//
|
|
721
|
+
// <li z-for="item in items">{{item.name}}</li>
|
|
722
|
+
// <li z-for="(item, i) in items">{{i}}: {{item.name}}</li>
|
|
723
|
+
// <div z-for="n in 5">{{n}}</div> (range)
|
|
724
|
+
// <div z-for="(val, key) in obj">{{key}}: {{val}}</div> (object)
|
|
725
|
+
//
|
|
726
|
+
// Uses a temporary DOM to parse, clone elements per item, and evaluate
|
|
727
|
+
// {{}} expressions with the iteration variable in scope.
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
_expandZFor(html) {
|
|
730
|
+
if (!html.includes('z-for')) return html;
|
|
731
|
+
|
|
732
|
+
const temp = document.createElement('div');
|
|
733
|
+
temp.innerHTML = html;
|
|
734
|
+
|
|
735
|
+
const _recurse = (root) => {
|
|
736
|
+
// Process innermost z-for elements first (no nested z-for inside)
|
|
737
|
+
let forEls = [...root.querySelectorAll('[z-for]')]
|
|
738
|
+
.filter(el => !el.querySelector('[z-for]'));
|
|
739
|
+
if (!forEls.length) return;
|
|
740
|
+
|
|
741
|
+
for (const el of forEls) {
|
|
742
|
+
if (!el.parentNode) continue; // already removed
|
|
743
|
+
const expr = el.getAttribute('z-for');
|
|
744
|
+
const m = expr.match(
|
|
745
|
+
/^\s*(?:\(\s*(\w+)(?:\s*,\s*(\w+))?\s*\)|(\w+))\s+in\s+(.+)\s*$/
|
|
746
|
+
);
|
|
747
|
+
if (!m) { el.removeAttribute('z-for'); continue; }
|
|
748
|
+
|
|
749
|
+
const itemVar = m[1] || m[3];
|
|
750
|
+
const indexVar = m[2] || '$index';
|
|
751
|
+
const listExpr = m[4].trim();
|
|
752
|
+
|
|
753
|
+
let list = this._evalExpr(listExpr);
|
|
754
|
+
if (list == null) { el.remove(); continue; }
|
|
755
|
+
// Number range: z-for="n in 5" → [1, 2, 3, 4, 5]
|
|
756
|
+
if (typeof list === 'number') {
|
|
757
|
+
list = Array.from({ length: list }, (_, i) => i + 1);
|
|
758
|
+
}
|
|
759
|
+
// Object iteration: z-for="(val, key) in obj" → entries
|
|
760
|
+
if (!Array.isArray(list) && typeof list === 'object' && typeof list[Symbol.iterator] !== 'function') {
|
|
761
|
+
list = Object.entries(list).map(([k, v]) => ({ key: k, value: v }));
|
|
762
|
+
}
|
|
763
|
+
if (!Array.isArray(list) && typeof list[Symbol.iterator] === 'function') {
|
|
764
|
+
list = [...list];
|
|
765
|
+
}
|
|
766
|
+
if (!Array.isArray(list)) { el.remove(); continue; }
|
|
767
|
+
|
|
768
|
+
const parent = el.parentNode;
|
|
769
|
+
const tplEl = el.cloneNode(true);
|
|
770
|
+
tplEl.removeAttribute('z-for');
|
|
771
|
+
const tplOuter = tplEl.outerHTML;
|
|
772
|
+
|
|
773
|
+
const fragment = document.createDocumentFragment();
|
|
774
|
+
const evalReplace = (str, item, index) =>
|
|
775
|
+
str.replace(/\{\{(.+?)\}\}/g, (_, inner) => {
|
|
776
|
+
try {
|
|
777
|
+
return new Function(itemVar, indexVar, 'state', 'props', '$',
|
|
778
|
+
`with(state){return (${inner.trim()})}`)(
|
|
779
|
+
item, index,
|
|
780
|
+
this.state.__raw || this.state,
|
|
781
|
+
this.props,
|
|
782
|
+
typeof window !== 'undefined' ? window.$ : undefined
|
|
783
|
+
);
|
|
784
|
+
} catch { return ''; }
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
for (let i = 0; i < list.length; i++) {
|
|
788
|
+
const processed = evalReplace(tplOuter, list[i], i);
|
|
789
|
+
const wrapper = document.createElement('div');
|
|
790
|
+
wrapper.innerHTML = processed;
|
|
791
|
+
while (wrapper.firstChild) fragment.appendChild(wrapper.firstChild);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
parent.replaceChild(fragment, el);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Handle remaining nested z-for (now exposed)
|
|
798
|
+
if (root.querySelector('[z-for]')) _recurse(root);
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
_recurse(temp);
|
|
802
|
+
return temp.innerHTML;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
// _processDirectives — Post-innerHTML DOM-level directive processing
|
|
807
|
+
// ---------------------------------------------------------------------------
|
|
808
|
+
_processDirectives() {
|
|
809
|
+
// z-pre: skip all directive processing on subtrees
|
|
810
|
+
// (we leave z-pre elements in the DOM, but skip their descendants)
|
|
811
|
+
|
|
812
|
+
// -- z-if / z-else-if / z-else (conditional rendering) --------
|
|
813
|
+
const ifEls = [...this._el.querySelectorAll('[z-if]')];
|
|
814
|
+
for (const el of ifEls) {
|
|
815
|
+
if (!el.parentNode || el.closest('[z-pre]')) continue;
|
|
816
|
+
|
|
817
|
+
const show = !!this._evalExpr(el.getAttribute('z-if'));
|
|
818
|
+
|
|
819
|
+
// Collect chain: adjacent z-else-if / z-else siblings
|
|
820
|
+
const chain = [{ el, show }];
|
|
821
|
+
let sib = el.nextElementSibling;
|
|
822
|
+
while (sib) {
|
|
823
|
+
if (sib.hasAttribute('z-else-if')) {
|
|
824
|
+
chain.push({ el: sib, show: !!this._evalExpr(sib.getAttribute('z-else-if')) });
|
|
825
|
+
sib = sib.nextElementSibling;
|
|
826
|
+
} else if (sib.hasAttribute('z-else')) {
|
|
827
|
+
chain.push({ el: sib, show: true });
|
|
828
|
+
break;
|
|
829
|
+
} else {
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Keep the first truthy branch, remove the rest
|
|
835
|
+
let found = false;
|
|
836
|
+
for (const item of chain) {
|
|
837
|
+
if (!found && item.show) {
|
|
838
|
+
found = true;
|
|
839
|
+
item.el.removeAttribute('z-if');
|
|
840
|
+
item.el.removeAttribute('z-else-if');
|
|
841
|
+
item.el.removeAttribute('z-else');
|
|
842
|
+
} else {
|
|
843
|
+
item.el.remove();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// -- z-show (toggle display) -----------------------------------
|
|
849
|
+
this._el.querySelectorAll('[z-show]').forEach(el => {
|
|
850
|
+
if (el.closest('[z-pre]')) return;
|
|
851
|
+
const show = !!this._evalExpr(el.getAttribute('z-show'));
|
|
852
|
+
el.style.display = show ? '' : 'none';
|
|
853
|
+
el.removeAttribute('z-show');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
857
|
+
this._el.querySelectorAll('*').forEach(el => {
|
|
858
|
+
if (el.closest('[z-pre]')) return;
|
|
859
|
+
[...el.attributes].forEach(attr => {
|
|
860
|
+
let attrName;
|
|
861
|
+
if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
|
|
862
|
+
else if (attr.name.startsWith(':') && !attr.name.startsWith('::')) attrName = attr.name.slice(1);
|
|
863
|
+
else return;
|
|
864
|
+
|
|
865
|
+
const val = this._evalExpr(attr.value);
|
|
866
|
+
el.removeAttribute(attr.name);
|
|
867
|
+
if (val === false || val === null || val === undefined) {
|
|
868
|
+
el.removeAttribute(attrName);
|
|
869
|
+
} else if (val === true) {
|
|
870
|
+
el.setAttribute(attrName, '');
|
|
871
|
+
} else {
|
|
872
|
+
el.setAttribute(attrName, String(val));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// -- z-class (dynamic class binding) ---------------------------
|
|
878
|
+
this._el.querySelectorAll('[z-class]').forEach(el => {
|
|
879
|
+
if (el.closest('[z-pre]')) return;
|
|
880
|
+
const val = this._evalExpr(el.getAttribute('z-class'));
|
|
881
|
+
if (typeof val === 'string') {
|
|
882
|
+
val.split(/\s+/).filter(Boolean).forEach(c => el.classList.add(c));
|
|
883
|
+
} else if (Array.isArray(val)) {
|
|
884
|
+
val.filter(Boolean).forEach(c => el.classList.add(String(c)));
|
|
885
|
+
} else if (val && typeof val === 'object') {
|
|
886
|
+
for (const [cls, active] of Object.entries(val)) {
|
|
887
|
+
el.classList.toggle(cls, !!active);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
el.removeAttribute('z-class');
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// -- z-style (dynamic inline styles) ---------------------------
|
|
894
|
+
this._el.querySelectorAll('[z-style]').forEach(el => {
|
|
895
|
+
if (el.closest('[z-pre]')) return;
|
|
896
|
+
const val = this._evalExpr(el.getAttribute('z-style'));
|
|
897
|
+
if (typeof val === 'string') {
|
|
898
|
+
el.style.cssText += ';' + val;
|
|
899
|
+
} else if (val && typeof val === 'object') {
|
|
900
|
+
for (const [prop, v] of Object.entries(val)) {
|
|
901
|
+
el.style[prop] = v;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
el.removeAttribute('z-style');
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// -- z-html (innerHTML injection) ------------------------------
|
|
908
|
+
this._el.querySelectorAll('[z-html]').forEach(el => {
|
|
909
|
+
if (el.closest('[z-pre]')) return;
|
|
910
|
+
const val = this._evalExpr(el.getAttribute('z-html'));
|
|
911
|
+
el.innerHTML = val != null ? String(val) : '';
|
|
912
|
+
el.removeAttribute('z-html');
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// -- z-text (safe textContent binding) -------------------------
|
|
916
|
+
this._el.querySelectorAll('[z-text]').forEach(el => {
|
|
917
|
+
if (el.closest('[z-pre]')) return;
|
|
918
|
+
const val = this._evalExpr(el.getAttribute('z-text'));
|
|
919
|
+
el.textContent = val != null ? String(val) : '';
|
|
920
|
+
el.removeAttribute('z-text');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// -- z-cloak (remove after render) -----------------------------
|
|
924
|
+
this._el.querySelectorAll('[z-cloak]').forEach(el => {
|
|
925
|
+
el.removeAttribute('z-cloak');
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
578
929
|
// Programmatic state update (batch-friendly)
|
|
930
|
+
// Passing an empty object forces a re-render (useful for external state changes).
|
|
579
931
|
setState(partial) {
|
|
580
|
-
Object.
|
|
932
|
+
if (partial && Object.keys(partial).length > 0) {
|
|
933
|
+
Object.assign(this.state, partial);
|
|
934
|
+
} else {
|
|
935
|
+
this._scheduleUpdate();
|
|
936
|
+
}
|
|
581
937
|
}
|
|
582
938
|
|
|
583
939
|
// Emit custom event up the DOM
|
package/src/core.js
CHANGED
|
@@ -496,13 +496,26 @@ query.ready = (fn) => {
|
|
|
496
496
|
else document.addEventListener('DOMContentLoaded', fn);
|
|
497
497
|
};
|
|
498
498
|
|
|
499
|
-
// Global event
|
|
500
|
-
|
|
499
|
+
// Global event listeners — supports direct and delegated forms
|
|
500
|
+
// $.on('keydown', handler) → direct listener on document
|
|
501
|
+
// $.on('click', '.btn', handler) → delegated via closest()
|
|
502
|
+
query.on = (event, selectorOrHandler, handler) => {
|
|
503
|
+
if (typeof selectorOrHandler === 'function') {
|
|
504
|
+
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
505
|
+
document.addEventListener(event, selectorOrHandler);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// 3-arg: delegated
|
|
501
509
|
document.addEventListener(event, (e) => {
|
|
502
|
-
const target = e.target.closest(
|
|
510
|
+
const target = e.target.closest(selectorOrHandler);
|
|
503
511
|
if (target) handler.call(target, e);
|
|
504
512
|
});
|
|
505
513
|
};
|
|
506
514
|
|
|
515
|
+
// Remove a direct global listener
|
|
516
|
+
query.off = (event, handler) => {
|
|
517
|
+
document.removeEventListener(event, handler);
|
|
518
|
+
};
|
|
519
|
+
|
|
507
520
|
// Extend collection prototype (like $.fn in jQuery)
|
|
508
521
|
query.fn = ZQueryCollection.prototype;
|
package/src/router.js
CHANGED
|
@@ -22,9 +22,9 @@ import { mount, destroy } from './component.js';
|
|
|
22
22
|
class Router {
|
|
23
23
|
constructor(config = {}) {
|
|
24
24
|
this._el = null;
|
|
25
|
-
//
|
|
25
|
+
// file:// protocol can't use pushState — always force hash mode
|
|
26
26
|
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
27
|
-
this._mode =
|
|
27
|
+
this._mode = isFile ? 'hash' : (config.mode || 'history');
|
|
28
28
|
|
|
29
29
|
// Base path for sub-path deployments
|
|
30
30
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|