zero-query 0.1.2 → 0.2.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 +132 -16
- package/cli.js +833 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2605 -0
- package/dist/zquery.min.js +17 -0
- package/index.js +1 -0
- package/package.json +10 -3
- package/src/component.js +12 -0
- package/src/router.js +40 -6
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zQuery (zeroQuery) v0.2.2
|
|
3
|
+
* Lightweight Frontend Library
|
|
4
|
+
* https://github.com/tonywied17/zero-query
|
|
5
|
+
* (c) 2026 Anthony Wiedman — MIT License
|
|
6
|
+
*/
|
|
7
|
+
(function(global) {
|
|
8
|
+
'use strict';
|
|
9
|
+
function reactive(target, onChange, _path = '') { if (typeof target !== 'object' || target === null) return target; const proxyCache = new WeakMap(); const handler = { get(obj, key) { if (key === '__isReactive') return true; if (key === '__raw') return obj; const value = obj[key]; if (typeof value === 'object' && value !== null) { if (proxyCache.has(value)) return proxyCache.get(value); const childProxy = new Proxy(value, handler); proxyCache.set(value, childProxy); return childProxy; } return value; }, set(obj, key, value) { const old = obj[key]; if (old === value) return true; obj[key] = value; onChange(key, value, old); return true; }, deleteProperty(obj, key) { const old = obj[key]; delete obj[key]; onChange(key, undefined, old); return true; } }; return new Proxy(target, handler);
|
|
10
|
+
class ZQueryCollection { constructor(elements) { this.elements = Array.isArray(elements) ? elements : [elements]; this.length = this.elements.length; this.elements.forEach((el, i) => { this[i] = el; }); } each(fn) { this.elements.forEach((el, i) => fn.call(el, i, el)); return this; } map(fn) { return this.elements.map((el, i) => fn.call(el, i, el)); } first() { return this.elements[0] || null; } last() { return this.elements[this.length - 1] || null; } eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); } toArray(){ return [...this.elements]; } [Symbol.iterator]() { return this.elements[Symbol.iterator](); } find(selector) { const found = []; this.elements.forEach(el => found.push(...el.querySelectorAll(selector))); return new ZQueryCollection(found); } parent() { const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))]; return new ZQueryCollection(parents); } closest(selector) { return new ZQueryCollection( this.elements.map(el => el.closest(selector)).filter(Boolean) ); } children(selector) { const kids = []; this.elements.forEach(el => { kids.push(...(selector ? el.querySelectorAll(`:scope > ${selector}`) : el.children)); }); return new ZQueryCollection([...kids]); } siblings() { const sibs = []; this.elements.forEach(el => { sibs.push(...[...el.parentElement.children].filter(c => c !== el)); }); return new ZQueryCollection(sibs); } next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); } prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); } filter(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter(selector)); } return new ZQueryCollection(this.elements.filter(el => el.matches(selector))); } not(selector) { if (typeof selector === 'function') { return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el))); } return new ZQueryCollection(this.elements.filter(el => !el.matches(selector))); } has(selector) { return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector))); } addClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.add(...classes)); } removeClass(...names) { const classes = names.flatMap(n => n.split(/\s+/)); return this.each((_, el) => el.classList.remove(...classes)); } toggleClass(name, force) { return this.each((_, el) => el.classList.toggle(name, force)); } hasClass(name) { return this.first()?.classList.contains(name) || false; } attr(name, value) { if (value === undefined) return this.first()?.getAttribute(name); return this.each((_, el) => el.setAttribute(name, value)); } removeAttr(name) { return this.each((_, el) => el.removeAttribute(name)); } prop(name, value) { if (value === undefined) return this.first()?.[name]; return this.each((_, el) => { el[name] = value; }); } data(key, value) { if (value === undefined) { if (key === undefined) return this.first()?.dataset; const raw = this.first()?.dataset[key]; try { return JSON.parse(raw); } catch { return raw; } } return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; }); } css(props) { if (typeof props === 'string') { return getComputedStyle(this.first())[props]; } return this.each((_, el) => Object.assign(el.style, props)); } width() { return this.first()?.getBoundingClientRect().width; } height() { return this.first()?.getBoundingClientRect().height; } offset() { const r = this.first()?.getBoundingClientRect(); return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null; } position() { const el = this.first(); return el ? { top: el.offsetTop, left: el.offsetLeft } : null; } html(content) { if (content === undefined) return this.first()?.innerHTML; return this.each((_, el) => { el.innerHTML = content; }); } text(content) { if (content === undefined) return this.first()?.textContent; return this.each((_, el) => { el.textContent = content; }); } val(value) { if (value === undefined) return this.first()?.value; return this.each((_, el) => { el.value = value; }); } append(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content); else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c)); else if (content instanceof Node) el.appendChild(content); }); } prepend(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content); else if (content instanceof Node) el.insertBefore(content, el.firstChild); }); } after(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('afterend', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling); }); } before(content) { return this.each((_, el) => { if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content); else if (content instanceof Node) el.parentNode.insertBefore(content, el); }); } wrap(wrapper) { return this.each((_, el) => { const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true); el.parentNode.insertBefore(w, el); w.appendChild(el); }); } remove() { return this.each((_, el) => el.remove()); } empty() { return this.each((_, el) => { el.innerHTML = ''; }); } clone(deep = true) { return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep))); } replaceWith(content) { return this.each((_, el) => { if (typeof content === 'string') { el.insertAdjacentHTML('afterend', content); el.remove(); } else if (content instanceof Node) { el.parentNode.replaceChild(content, el); } }); } show(display = '') { return this.each((_, el) => { el.style.display = display; }); } hide() { return this.each((_, el) => { el.style.display = 'none'; }); } toggle(display = '') { return this.each((_, el) => { el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none'; }); } on(event, selectorOrHandler, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => { if (typeof selectorOrHandler === 'function') { el.addEventListener(evt, selectorOrHandler); } else { el.addEventListener(evt, (e) => { const target = e.target.closest(selectorOrHandler); if (target && el.contains(target)) handler.call(target, e); }); } }); }); } off(event, handler) { const events = event.split(/\s+/); return this.each((_, el) => { events.forEach(evt => el.removeEventListener(evt, handler)); }); } one(event, handler) { return this.each((_, el) => { el.addEventListener(event, handler, { once: true }); }); } trigger(event, detail) { return this.each((_, el) => { el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true })); }); } click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); } submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); } focus() { this.first()?.focus(); return this; } blur() { this.first()?.blur(); return this; } animate(props, duration = 300, easing = 'ease') { return new Promise(resolve => { const count = { done: 0 }; this.each((_, el) => { el.style.transition = `all ${duration}ms ${easing}`; requestAnimationFrame(() => { Object.assign(el.style, props); const onEnd = () => { el.removeEventListener('transitionend', onEnd); el.style.transition = ''; if (++count.done >= this.length) resolve(this); }; el.addEventListener('transitionend', onEnd); }); }); setTimeout(() => resolve(this), duration + 50); }); } fadeIn(duration = 300) { return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration); } fadeOut(duration = 300) { return this.animate({ opacity: '0' }, duration).then(col => col.hide()); } slideToggle(duration = 300) { return this.each((_, el) => { if (el.style.display === 'none' || getComputedStyle(el).display === 'none') { el.style.display = ''; el.style.overflow = 'hidden'; const h = el.scrollHeight + 'px'; el.style.maxHeight = '0'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = h; }); setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } else { el.style.overflow = 'hidden'; el.style.maxHeight = el.scrollHeight + 'px'; el.style.transition = `max-height ${duration}ms ease`; requestAnimationFrame(() => { el.style.maxHeight = '0'; }); setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration); } }); } serialize() { const form = this.first(); if (!form || form.tagName !== 'FORM') return ''; return new URLSearchParams(new FormData(form)).toString(); } serializeObject() { const form = this.first(); if (!form || form.tagName !== 'FORM') return {}; const obj = {}; new FormData(form).forEach((v, k) => { if (obj[k] !== undefined) { if (!Array.isArray(obj[k])) obj[k] = [obj[k]]; obj[k].push(v); } else { obj[k] = v; } }); return obj; }
|
|
11
|
+
const _registry = new Map(); const _instances = new Map(); const _resourceCache = new Map(); let _uid = 0;
|
|
12
|
+
class Router { constructor(config = {}) { this._el = null; const isFile = typeof location !== 'undefined' && location.protocol === 'file:'; this._mode = config.mode || (isFile ? 'hash' : 'history'); let rawBase = config.base; if (rawBase == null) { rawBase = (typeof window !== 'undefined' && window.__ZQ_BASE) || ''; if (!rawBase && typeof document !== 'undefined') { const baseEl = document.querySelector('base'); if (baseEl) { try { rawBase = new URL(baseEl.href).pathname; } catch { rawBase = baseEl.getAttribute('href') || ''; } if (rawBase === '/') rawBase = ''; } } } this._base = String(rawBase).replace(/\/+$/, ''); if (this._base && !this._base.startsWith('/')) this._base = '/' + this._base; this._routes = []; this._fallback = config.fallback || null; this._current = null; this._guards = { before: [], after: [] }; this._listeners = new Set(); this._instance = null; this._resolving = false; if (config.el) { this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el; } if (config.routes) { config.routes.forEach(r => this.add(r)); } if (this._mode === 'hash') { window.addEventListener('hashchange', () => this._resolve()); } else { window.addEventListener('popstate', () => this._resolve()); } document.addEventListener('click', (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const link = e.target.closest('[z-link]'); if (!link) return; if (link.getAttribute('target') === '_blank') return; e.preventDefault(); this.navigate(link.getAttribute('z-link')); }); if (this._el) { queueMicrotask(() => this._resolve()); } } add(route) { const keys = []; const pattern = route.path .replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const regex = new RegExp(`^${pattern}$`); this._routes.push({ ...route, _regex: regex, _keys: keys }); if (route.fallback) { const fbKeys = []; const fbPattern = route.fallback .replace(/:(\w+)/g, (_, key) => { fbKeys.push(key); return '([^/]+)'; }) .replace(/\*/g, '(.*)'); const fbRegex = new RegExp(`^${fbPattern}$`); this._routes.push({ ...route, path: route.fallback, _regex: fbRegex, _keys: fbKeys }); } return this; } remove(path) { this._routes = this._routes.filter(r => r.path !== path); return this; } navigate(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.hash = '#' + normalized; } else { window.history.pushState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } replace(path, options = {}) { const [cleanPath, fragment] = (path || '').split('#'); let normalized = this._normalizePath(cleanPath); const hash = fragment ? '#' + fragment : ''; if (this._mode === 'hash') { if (fragment) window.__zqScrollTarget = fragment; window.location.replace('#' + normalized); } else { window.history.replaceState(options.state || {}, '', this._base + normalized + hash); this._resolve(); } return this; } _normalizePath(path) { let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); if (this._base) { if (p === this._base) return '/'; if (p.startsWith(this._base + '/')) p = p.slice(this._base.length) || '/'; } return p; } resolve(path) { const normalized = path && path.startsWith('/') ? path : (path ? `/${path}` : '/'); return this._base + normalized; } back() { window.history.back(); return this; } forward() { window.history.forward(); return this; } go(n) { window.history.go(n); return this; } beforeEach(fn) { this._guards.before.push(fn); return this; } afterEach(fn) { this._guards.after.push(fn); return this; } onChange(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); } get current() { return this._current; } get base() { return this._base; } get path() { if (this._mode === 'hash') { const raw = window.location.hash.slice(1) || '/'; if (raw && !raw.startsWith('/')) { window.__zqScrollTarget = raw; const fallbackPath = (this._current && this._current.path) || '/'; window.location.replace('#' + fallbackPath); return fallbackPath; } return raw; } let pathname = window.location.pathname || '/'; if (pathname.length > 1 && pathname.endsWith('/')) { pathname = pathname.slice(0, -1); } if (this._base) { if (pathname === this._base) return '/'; if (pathname.startsWith(this._base + '/')) { return pathname.slice(this._base.length) || '/'; } } return pathname; } get query() { const search = this._mode === 'hash' ? (window.location.hash.split('?')[1] || '') : window.location.search.slice(1); return Object.fromEntries(new URLSearchParams(search)); } async _resolve() { if (this._resolving) return; this._resolving = true; try { await this.__resolve(); } finally { this._resolving = false; } } async __resolve() { const fullPath = this.path; const [pathPart, queryString] = fullPath.split('?'); const path = pathPart || '/'; const query = Object.fromEntries(new URLSearchParams(queryString || '')); let matched = null; let params = {}; for (const route of this._routes) { const m = path.match(route._regex); if (m) { matched = route; route._keys.forEach((key, i) => { params[key] = m[i + 1]; }); break; } } if (!matched && this._fallback) { matched = { component: this._fallback, path: '*', _keys: [], _regex: /.*/ }; } if (!matched) return; const to = { route: matched, params, query, path }; const from = this._current; for (const guard of this._guards.before) { const result = await guard(to, from); if (result === false) return; if (typeof result === 'string') { return this.navigate(result); } } if (matched.load) { try { await matched.load(); } catch (err) { console.error(`zQuery Router: Failed to load module for "${matched.path}"`, err); return; } } this._current = to; if (this._el && matched.component) { if (this._instance) { this._instance.destroy(); this._instance = null; } this._el.innerHTML = ''; const props = { ...params, $route: to, $query: query, $params: params }; if (typeof matched.component === 'string') { const container = document.createElement(matched.component); this._el.appendChild(container); this._instance = mount(container, matched.component, props); } else if (typeof matched.component === 'function') { this._el.innerHTML = matched.component(to); } } for (const guard of this._guards.after) { await guard(to, from); } this._listeners.forEach(fn => fn(to, from)); } destroy() { if (this._instance) this._instance.destroy(); this._listeners.clear(); this._routes = []; this._guards = { before: [], after: [] }; }
|
|
13
|
+
class Store { constructor(config = {}) { this._subscribers = new Map(); this._wildcards = new Set(); this._actions = config.actions || {}; this._getters = config.getters || {}; this._middleware = []; this._history = []; this._debug = config.debug || false; const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) }; this.state = reactive(initial, (key, value, old) => { const subs = this._subscribers.get(key); if (subs) subs.forEach(fn => fn(value, old, key)); this._wildcards.forEach(fn => fn(key, value, old)); }); this.getters = {}; for (const [name, fn] of Object.entries(this._getters)) { Object.defineProperty(this.getters, name, { get: () => fn(this.state.__raw || this.state), enumerable: true }); } } dispatch(name, ...args) { const action = this._actions[name]; if (!action) { console.warn(`zQuery Store: Unknown action "${name}"`); return; } for (const mw of this._middleware) { const result = mw(name, args, this.state); if (result === false) return; } if (this._debug) { console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args); } const result = action(this.state, ...args); this._history.push({ action: name, args, timestamp: Date.now() }); return result; } subscribe(keyOrFn, fn) { if (typeof keyOrFn === 'function') { this._wildcards.add(keyOrFn); return () => this._wildcards.delete(keyOrFn); } if (!this._subscribers.has(keyOrFn)) { this._subscribers.set(keyOrFn, new Set()); } this._subscribers.get(keyOrFn).add(fn); return () => this._subscribers.get(keyOrFn)?.delete(fn); } snapshot() { return JSON.parse(JSON.stringify(this.state.__raw || this.state)); } replaceState(newState) { const raw = this.state.__raw || this.state; for (const key of Object.keys(raw)) { delete this.state[key]; } Object.assign(this.state, newState); } use(fn) { this._middleware.push(fn); return this; } get history() { return [...this._history]; } reset(initialState) { this.replaceState(initialState); this._history = []; }
|
|
14
|
+
const _config = { baseURL: '', headers: { 'Content-Type': 'application/json' }, timeout: 30000,
|
|
15
|
+
function debounce(fn, ms = 250) { let timer; const debounced = (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; debounced.cancel = () => clearTimeout(timer); return debounced;
|
|
16
|
+
function $(selector, context) { if (typeof selector === 'function') { query.ready(selector); return; } return query(selector, context);
|
|
17
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
package/index.js
CHANGED
|
@@ -133,6 +133,7 @@ $.bus = bus;
|
|
|
133
133
|
|
|
134
134
|
// --- Meta ------------------------------------------------------------------
|
|
135
135
|
$.version = '__VERSION__';
|
|
136
|
+
$.meta = {}; // populated at build time by CLI bundler
|
|
136
137
|
|
|
137
138
|
$.noConflict = () => {
|
|
138
139
|
if (typeof window !== 'undefined' && window.$ === $) {
|
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zquery": "./cli.js"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"src",
|
|
11
|
+
"dist",
|
|
8
12
|
"index.js",
|
|
13
|
+
"cli.js",
|
|
9
14
|
"LICENSE",
|
|
10
15
|
"README.md"
|
|
11
16
|
],
|
|
12
17
|
"scripts": {
|
|
13
18
|
"build": "node build.js",
|
|
14
|
-
"dev": "
|
|
19
|
+
"dev": "npx zquery bundle examples/starter-app/scripts/app.js --watch",
|
|
15
20
|
"serve": "node examples/starter-app/local-server.js",
|
|
16
|
-
"
|
|
21
|
+
"dev-lib": "node build.js --watch",
|
|
22
|
+
"bundle": "node cli.js bundle",
|
|
23
|
+
"bundle:app": "node cli.js bundle examples/starter-app/scripts/app.js"
|
|
17
24
|
},
|
|
18
25
|
"keywords": [
|
|
19
26
|
"dom",
|
package/src/component.js
CHANGED
|
@@ -39,6 +39,18 @@ let _uid = 0;
|
|
|
39
39
|
function _fetchResource(url) {
|
|
40
40
|
if (_resourceCache.has(url)) return _resourceCache.get(url);
|
|
41
41
|
|
|
42
|
+
// Check inline resource map (populated by CLI bundler for file:// support).
|
|
43
|
+
// Keys are relative paths; match against the URL suffix.
|
|
44
|
+
if (typeof window !== 'undefined' && window.__zqInline) {
|
|
45
|
+
for (const [path, content] of Object.entries(window.__zqInline)) {
|
|
46
|
+
if (url === path || url.endsWith('/' + path) || url.endsWith('\\' + path)) {
|
|
47
|
+
const resolved = Promise.resolve(content);
|
|
48
|
+
_resourceCache.set(url, resolved);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
// Resolve relative URLs against <base href> or origin root.
|
|
43
55
|
// This prevents SPA route paths (e.g. /docs/advanced) from
|
|
44
56
|
// breaking relative resource URLs like 'scripts/components/foo.css'.
|
package/src/router.js
CHANGED
|
@@ -22,7 +22,9 @@ import { mount, destroy } from './component.js';
|
|
|
22
22
|
class Router {
|
|
23
23
|
constructor(config = {}) {
|
|
24
24
|
this._el = null;
|
|
25
|
-
|
|
25
|
+
// Auto-detect: file:// protocol can't use pushState, fall back to hash
|
|
26
|
+
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
27
|
+
this._mode = config.mode || (isFile ? 'hash' : 'history');
|
|
26
28
|
|
|
27
29
|
// Base path for sub-path deployments
|
|
28
30
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|
|
@@ -48,6 +50,7 @@ class Router {
|
|
|
48
50
|
this._guards = { before: [], after: [] };
|
|
49
51
|
this._listeners = new Set();
|
|
50
52
|
this._instance = null; // current mounted component
|
|
53
|
+
this._resolving = false; // re-entrancy guard
|
|
51
54
|
|
|
52
55
|
// Set outlet element
|
|
53
56
|
if (config.el) {
|
|
@@ -119,22 +122,31 @@ class Router {
|
|
|
119
122
|
// --- Navigation ----------------------------------------------------------
|
|
120
123
|
|
|
121
124
|
navigate(path, options = {}) {
|
|
122
|
-
|
|
125
|
+
// Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
|
|
126
|
+
const [cleanPath, fragment] = (path || '').split('#');
|
|
127
|
+
let normalized = this._normalizePath(cleanPath);
|
|
128
|
+
const hash = fragment ? '#' + fragment : '';
|
|
123
129
|
if (this._mode === 'hash') {
|
|
130
|
+
// Hash mode uses the URL hash for routing, so a #fragment can't live
|
|
131
|
+
// in the URL. Store it as a scroll target for the destination component.
|
|
132
|
+
if (fragment) window.__zqScrollTarget = fragment;
|
|
124
133
|
window.location.hash = '#' + normalized;
|
|
125
134
|
} else {
|
|
126
|
-
window.history.pushState(options.state || {}, '', this._base + normalized);
|
|
135
|
+
window.history.pushState(options.state || {}, '', this._base + normalized + hash);
|
|
127
136
|
this._resolve();
|
|
128
137
|
}
|
|
129
138
|
return this;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
replace(path, options = {}) {
|
|
133
|
-
|
|
142
|
+
const [cleanPath, fragment] = (path || '').split('#');
|
|
143
|
+
let normalized = this._normalizePath(cleanPath);
|
|
144
|
+
const hash = fragment ? '#' + fragment : '';
|
|
134
145
|
if (this._mode === 'hash') {
|
|
146
|
+
if (fragment) window.__zqScrollTarget = fragment;
|
|
135
147
|
window.location.replace('#' + normalized);
|
|
136
148
|
} else {
|
|
137
|
-
window.history.replaceState(options.state || {}, '', this._base + normalized);
|
|
149
|
+
window.history.replaceState(options.state || {}, '', this._base + normalized + hash);
|
|
138
150
|
this._resolve();
|
|
139
151
|
}
|
|
140
152
|
return this;
|
|
@@ -198,7 +210,18 @@ class Router {
|
|
|
198
210
|
|
|
199
211
|
get path() {
|
|
200
212
|
if (this._mode === 'hash') {
|
|
201
|
-
|
|
213
|
+
const raw = window.location.hash.slice(1) || '/';
|
|
214
|
+
// If the hash doesn't start with '/', it's an in-page anchor
|
|
215
|
+
// (e.g. #some-heading), not a route. Treat it as a scroll target
|
|
216
|
+
// and resolve to the last known route (or '/').
|
|
217
|
+
if (raw && !raw.startsWith('/')) {
|
|
218
|
+
window.__zqScrollTarget = raw;
|
|
219
|
+
// Restore the route hash silently so the URL stays valid
|
|
220
|
+
const fallbackPath = (this._current && this._current.path) || '/';
|
|
221
|
+
window.location.replace('#' + fallbackPath);
|
|
222
|
+
return fallbackPath;
|
|
223
|
+
}
|
|
224
|
+
return raw;
|
|
202
225
|
}
|
|
203
226
|
let pathname = window.location.pathname || '/';
|
|
204
227
|
// Strip trailing slash for consistency (except root '/')
|
|
@@ -226,6 +249,17 @@ class Router {
|
|
|
226
249
|
// --- Internal resolve ----------------------------------------------------
|
|
227
250
|
|
|
228
251
|
async _resolve() {
|
|
252
|
+
// Prevent re-entrant calls (e.g. listener triggering navigation)
|
|
253
|
+
if (this._resolving) return;
|
|
254
|
+
this._resolving = true;
|
|
255
|
+
try {
|
|
256
|
+
await this.__resolve();
|
|
257
|
+
} finally {
|
|
258
|
+
this._resolving = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async __resolve() {
|
|
229
263
|
const fullPath = this.path;
|
|
230
264
|
const [pathPart, queryString] = fullPath.split('?');
|
|
231
265
|
const path = pathPart || '/';
|