zero-query 1.0.9 → 1.1.1
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/cli/commands/build-api.js +442 -0
- package/cli/commands/build.js +33 -2
- package/cli/commands/bundle.js +41 -0
- package/cli/commands/dev/server.js +56 -3
- package/cli/scaffold/default/app/components/contacts/contacts.css +9 -9
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +5 -5
- package/cli/scaffold/default/app/components/playground/playground.js +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +3 -3
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +4 -4
- package/cli/utils.js +6 -6
- package/dist/API.md +6603 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +387 -25
- package/dist/zquery.min.js +47 -17
- package/index.d.ts +9 -3
- package/index.js +10 -2
- package/package.json +2 -1
- package/src/component.js +243 -6
- package/src/reactive.js +4 -3
- package/src/router.js +79 -9
- package/src/store.js +49 -3
- package/tests/cli.test.js +80 -0
- package/tests/compare.test.js +486 -0
- package/tests/dev-server.test.js +489 -0
- package/tests/docs.test.js +1650 -0
- package/tests/electron-features.test.js +864 -0
- package/types/misc.d.ts +7 -7
- package/types/reactive.d.ts +1 -1
- package/types/store.d.ts +2 -1
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v1.
|
|
2
|
+
* zQuery (zeroQuery) v1.1.1
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -426,13 +426,13 @@ function effect(fn) {
|
|
|
426
426
|
function batch(fn) {
|
|
427
427
|
if (Signal._batching) {
|
|
428
428
|
// Already inside a batch, just run
|
|
429
|
-
fn();
|
|
430
|
-
return;
|
|
429
|
+
return fn();
|
|
431
430
|
}
|
|
432
431
|
Signal._batching = true;
|
|
433
432
|
Signal._batchQueue.clear();
|
|
433
|
+
let result;
|
|
434
434
|
try {
|
|
435
|
-
fn();
|
|
435
|
+
result = fn();
|
|
436
436
|
} finally {
|
|
437
437
|
Signal._batching = false;
|
|
438
438
|
// Collect all unique subscribers across all queued signals
|
|
@@ -451,6 +451,7 @@ function batch(fn) {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
|
+
return result;
|
|
454
455
|
}
|
|
455
456
|
|
|
456
457
|
|
|
@@ -3030,8 +3031,58 @@ class Component {
|
|
|
3030
3031
|
this._slotContent['default'] = defaultSlotNodes.join('');
|
|
3031
3032
|
}
|
|
3032
3033
|
|
|
3033
|
-
// Props
|
|
3034
|
-
|
|
3034
|
+
// Props - reactive when definition.props is defined, frozen otherwise
|
|
3035
|
+
if (definition.props && typeof definition.props === 'object' && !Array.isArray(definition.props)) {
|
|
3036
|
+
// Reactive props with type coercion and defaults
|
|
3037
|
+
this.props = this._resolveReactiveProps(definition.props, props);
|
|
3038
|
+
// MutationObserver to re-read props when parent re-renders and changes attributes
|
|
3039
|
+
this._propObserver = new MutationObserver((mutations) => {
|
|
3040
|
+
if (this._destroyed) return;
|
|
3041
|
+
let changed = false;
|
|
3042
|
+
for (const mut of mutations) {
|
|
3043
|
+
if (mut.type === 'attributes') {
|
|
3044
|
+
const attrName = mut.attributeName;
|
|
3045
|
+
// Skip internal attributes
|
|
3046
|
+
if (attrName.startsWith('z-') || attrName.startsWith('@') || attrName.startsWith(':') || attrName.startsWith('data-zq')) continue;
|
|
3047
|
+
// Check if this is a defined prop (attribute names are lowercase)
|
|
3048
|
+
const propName = attrName.startsWith(':') ? attrName.slice(1) : attrName;
|
|
3049
|
+
if (propName in definition.props) {
|
|
3050
|
+
changed = true;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (changed) {
|
|
3055
|
+
this.props = this._resolveReactiveProps(definition.props, {});
|
|
3056
|
+
this._scheduleUpdate();
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
3059
|
+
this._propObserver.observe(el, { attributes: true });
|
|
3060
|
+
} else {
|
|
3061
|
+
// Legacy: frozen props from parent
|
|
3062
|
+
this.props = Object.freeze({ ...props });
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// Store connectors - auto-subscribe to store keys
|
|
3066
|
+
this._storeCleanups = [];
|
|
3067
|
+
this.stores = {};
|
|
3068
|
+
if (definition.stores && typeof definition.stores === 'object') {
|
|
3069
|
+
for (const [alias, connector] of Object.entries(definition.stores)) {
|
|
3070
|
+
if (!connector || !connector._zqConnector) continue;
|
|
3071
|
+
const { store, keys } = connector;
|
|
3072
|
+
// Initialize snapshot
|
|
3073
|
+
const snap = {};
|
|
3074
|
+
for (const key of keys) {
|
|
3075
|
+
snap[key] = store.state[key];
|
|
3076
|
+
}
|
|
3077
|
+
this.stores[alias] = snap;
|
|
3078
|
+
// Subscribe to changes
|
|
3079
|
+
const unsub = store.subscribe(keys, (key, value) => {
|
|
3080
|
+
this.stores[alias][key] = value;
|
|
3081
|
+
if (!this._destroyed) this._scheduleUpdate();
|
|
3082
|
+
});
|
|
3083
|
+
this._storeCleanups.push(unsub);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3035
3086
|
|
|
3036
3087
|
// Reactive state
|
|
3037
3088
|
const initialState = typeof definition.state === 'function'
|
|
@@ -3110,6 +3161,61 @@ class Component {
|
|
|
3110
3161
|
});
|
|
3111
3162
|
}
|
|
3112
3163
|
|
|
3164
|
+
/**
|
|
3165
|
+
* Resolve reactive props from the definition's prop schema.
|
|
3166
|
+
* Reads from element attributes, applies type coercion and defaults.
|
|
3167
|
+
* Passed props (from mount) override attributes.
|
|
3168
|
+
* @param {object} propDefs - { propName: { type, default } }
|
|
3169
|
+
* @param {object} passedProps - props passed programmatically from mount()
|
|
3170
|
+
* @returns {object} resolved props (frozen)
|
|
3171
|
+
*/
|
|
3172
|
+
_resolveReactiveProps(propDefs, passedProps) {
|
|
3173
|
+
const resolved = {};
|
|
3174
|
+
for (const [name, schema] of Object.entries(propDefs)) {
|
|
3175
|
+
const def = typeof schema === 'object' && schema !== null ? schema : { type: schema };
|
|
3176
|
+
const type = def.type;
|
|
3177
|
+
const defaultVal = def.default;
|
|
3178
|
+
|
|
3179
|
+
// Priority: passed props > dynamic :prop attribute > static attribute > default
|
|
3180
|
+
if (name in passedProps) {
|
|
3181
|
+
resolved[name] = passedProps[name];
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
// Check for dynamic :prop attribute (already evaluated by parent mount)
|
|
3186
|
+
let rawAttr = this._el.getAttribute(':' + name);
|
|
3187
|
+
let hasAttr = rawAttr !== null;
|
|
3188
|
+
if (!hasAttr) {
|
|
3189
|
+
rawAttr = this._el.getAttribute(name);
|
|
3190
|
+
hasAttr = rawAttr !== null;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (hasAttr && rawAttr !== null) {
|
|
3194
|
+
resolved[name] = this._coercePropValue(rawAttr, type);
|
|
3195
|
+
} else if (defaultVal !== undefined) {
|
|
3196
|
+
resolved[name] = typeof defaultVal === 'function' ? defaultVal() : defaultVal;
|
|
3197
|
+
} else {
|
|
3198
|
+
resolved[name] = undefined;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return Object.freeze(resolved);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
/**
|
|
3205
|
+
* Coerce a raw attribute string to the specified type.
|
|
3206
|
+
* @param {string} raw - attribute value string
|
|
3207
|
+
* @param {Function} type - String, Number, Boolean, Object, or Array
|
|
3208
|
+
* @returns {*}
|
|
3209
|
+
*/
|
|
3210
|
+
_coercePropValue(raw, type) {
|
|
3211
|
+
if (type === Number) return Number(raw);
|
|
3212
|
+
if (type === Boolean) return raw !== 'false' && raw !== '0' && raw !== '';
|
|
3213
|
+
if (type === Object || type === Array) {
|
|
3214
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
3215
|
+
}
|
|
3216
|
+
return raw; // String or unspecified
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3113
3219
|
// Load external templateUrl / styleUrl if specified (once per definition)
|
|
3114
3220
|
//
|
|
3115
3221
|
// Relative paths are resolved automatically against the component file's
|
|
@@ -3872,8 +3978,20 @@ class Component {
|
|
|
3872
3978
|
item.el.removeAttribute('z-if');
|
|
3873
3979
|
item.el.removeAttribute('z-else-if');
|
|
3874
3980
|
item.el.removeAttribute('z-else');
|
|
3981
|
+
// Transition enter for z-if elements becoming visible
|
|
3982
|
+
const transName = item.el.getAttribute('z-transition');
|
|
3983
|
+
if (transName) {
|
|
3984
|
+
item.el.removeAttribute('z-transition');
|
|
3985
|
+
this._transitionEnter(item.el, transName);
|
|
3986
|
+
}
|
|
3875
3987
|
} else {
|
|
3876
|
-
|
|
3988
|
+
// Transition leave for z-if elements being removed
|
|
3989
|
+
const transName = item.el.getAttribute('z-transition');
|
|
3990
|
+
if (transName) {
|
|
3991
|
+
this._transitionLeave(item.el, transName, () => item.el.remove());
|
|
3992
|
+
} else {
|
|
3993
|
+
item.el.remove();
|
|
3994
|
+
}
|
|
3877
3995
|
}
|
|
3878
3996
|
}
|
|
3879
3997
|
}
|
|
@@ -3882,8 +4000,31 @@ class Component {
|
|
|
3882
4000
|
this._el.querySelectorAll('[z-show]').forEach(el => {
|
|
3883
4001
|
if (el.closest('[z-pre]')) return;
|
|
3884
4002
|
const show = !!this._evalExpr(el.getAttribute('z-show'));
|
|
3885
|
-
|
|
3886
|
-
el.
|
|
4003
|
+
const transName = el.getAttribute('z-transition');
|
|
4004
|
+
const wasHidden = el.style.display === 'none' || el.hasAttribute('data-zq-hidden');
|
|
4005
|
+
|
|
4006
|
+
if (transName) {
|
|
4007
|
+
el.removeAttribute('z-show');
|
|
4008
|
+
if (show && wasHidden) {
|
|
4009
|
+
// Entering: was hidden, now showing
|
|
4010
|
+
el.style.display = '';
|
|
4011
|
+
el.removeAttribute('data-zq-hidden');
|
|
4012
|
+
this._transitionEnter(el, transName);
|
|
4013
|
+
} else if (!show && !wasHidden) {
|
|
4014
|
+
// Leaving: was visible, now hiding
|
|
4015
|
+
el.setAttribute('data-zq-hidden', '');
|
|
4016
|
+
this._transitionLeave(el, transName, () => {
|
|
4017
|
+
el.style.display = 'none';
|
|
4018
|
+
});
|
|
4019
|
+
} else {
|
|
4020
|
+
el.style.display = show ? '' : 'none';
|
|
4021
|
+
if (!show) el.setAttribute('data-zq-hidden', '');
|
|
4022
|
+
else el.removeAttribute('data-zq-hidden');
|
|
4023
|
+
}
|
|
4024
|
+
} else {
|
|
4025
|
+
el.style.display = show ? '' : 'none';
|
|
4026
|
+
el.removeAttribute('z-show');
|
|
4027
|
+
}
|
|
3887
4028
|
});
|
|
3888
4029
|
|
|
3889
4030
|
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
@@ -3958,6 +4099,93 @@ class Component {
|
|
|
3958
4099
|
});
|
|
3959
4100
|
}
|
|
3960
4101
|
|
|
4102
|
+
// ---------------------------------------------------------------------------
|
|
4103
|
+
// Transition helpers - CSS class-based enter/leave animations
|
|
4104
|
+
//
|
|
4105
|
+
// z-transition="fade" generates:
|
|
4106
|
+
// Enter: .fade-enter-from → .fade-enter-active + .fade-enter-to
|
|
4107
|
+
// Leave: .fade-leave-from → .fade-leave-active + .fade-leave-to
|
|
4108
|
+
//
|
|
4109
|
+
// Or component-level transition config:
|
|
4110
|
+
// transition: { enter: 'animate-in', leave: 'animate-out', duration: 200 }
|
|
4111
|
+
// ---------------------------------------------------------------------------
|
|
4112
|
+
|
|
4113
|
+
/**
|
|
4114
|
+
* Run an enter transition on an element.
|
|
4115
|
+
* @param {Element} el - target element
|
|
4116
|
+
* @param {string} name - transition name (e.g. 'fade')
|
|
4117
|
+
*/
|
|
4118
|
+
_transitionEnter(el, name) {
|
|
4119
|
+
// Check for component-level transition config
|
|
4120
|
+
const cfg = this._def.transition;
|
|
4121
|
+
if (cfg && cfg.enter) {
|
|
4122
|
+
el.classList.add(cfg.enter);
|
|
4123
|
+
const duration = cfg.duration || 0;
|
|
4124
|
+
const cleanup = () => el.classList.remove(cfg.enter);
|
|
4125
|
+
if (duration > 0) {
|
|
4126
|
+
setTimeout(cleanup, duration);
|
|
4127
|
+
} else {
|
|
4128
|
+
el.addEventListener('transitionend', cleanup, { once: true });
|
|
4129
|
+
el.addEventListener('animationend', cleanup, { once: true });
|
|
4130
|
+
}
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
// CSS class-based transition pattern
|
|
4135
|
+
el.classList.add(`${name}-enter-from`, `${name}-enter-active`);
|
|
4136
|
+
// Force reflow so the browser registers the initial state
|
|
4137
|
+
void el.offsetHeight;
|
|
4138
|
+
requestAnimationFrame(() => {
|
|
4139
|
+
el.classList.remove(`${name}-enter-from`);
|
|
4140
|
+
el.classList.add(`${name}-enter-to`);
|
|
4141
|
+
const onEnd = () => {
|
|
4142
|
+
el.classList.remove(`${name}-enter-active`, `${name}-enter-to`);
|
|
4143
|
+
};
|
|
4144
|
+
el.addEventListener('transitionend', onEnd, { once: true });
|
|
4145
|
+
el.addEventListener('animationend', onEnd, { once: true });
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
/**
|
|
4150
|
+
* Run a leave transition on an element, then call done().
|
|
4151
|
+
* @param {Element} el - target element
|
|
4152
|
+
* @param {string} name - transition name (e.g. 'fade')
|
|
4153
|
+
* @param {Function} done - callback when transition completes
|
|
4154
|
+
*/
|
|
4155
|
+
_transitionLeave(el, name, done) {
|
|
4156
|
+
// Check for component-level transition config
|
|
4157
|
+
const cfg = this._def.transition;
|
|
4158
|
+
if (cfg && cfg.leave) {
|
|
4159
|
+
el.classList.add(cfg.leave);
|
|
4160
|
+
const duration = cfg.duration || 0;
|
|
4161
|
+
const cleanup = () => {
|
|
4162
|
+
el.classList.remove(cfg.leave);
|
|
4163
|
+
done();
|
|
4164
|
+
};
|
|
4165
|
+
if (duration > 0) {
|
|
4166
|
+
setTimeout(cleanup, duration);
|
|
4167
|
+
} else {
|
|
4168
|
+
el.addEventListener('transitionend', cleanup, { once: true });
|
|
4169
|
+
el.addEventListener('animationend', cleanup, { once: true });
|
|
4170
|
+
}
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
// CSS class-based transition pattern
|
|
4175
|
+
el.classList.add(`${name}-leave-from`, `${name}-leave-active`);
|
|
4176
|
+
void el.offsetHeight;
|
|
4177
|
+
requestAnimationFrame(() => {
|
|
4178
|
+
el.classList.remove(`${name}-leave-from`);
|
|
4179
|
+
el.classList.add(`${name}-leave-to`);
|
|
4180
|
+
const onEnd = () => {
|
|
4181
|
+
el.classList.remove(`${name}-leave-active`, `${name}-leave-to`);
|
|
4182
|
+
done();
|
|
4183
|
+
};
|
|
4184
|
+
el.addEventListener('transitionend', onEnd, { once: true });
|
|
4185
|
+
el.addEventListener('animationend', onEnd, { once: true });
|
|
4186
|
+
});
|
|
4187
|
+
}
|
|
4188
|
+
|
|
3961
4189
|
// Programmatic state update (batch-friendly)
|
|
3962
4190
|
// Passing an empty object forces a re-render (useful for external state changes).
|
|
3963
4191
|
setState(partial) {
|
|
@@ -3981,6 +4209,16 @@ class Component {
|
|
|
3981
4209
|
try { this._def.destroyed.call(this); }
|
|
3982
4210
|
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
|
|
3983
4211
|
}
|
|
4212
|
+
// Clean up prop observer
|
|
4213
|
+
if (this._propObserver) {
|
|
4214
|
+
this._propObserver.disconnect();
|
|
4215
|
+
this._propObserver = null;
|
|
4216
|
+
}
|
|
4217
|
+
// Clean up store connectors
|
|
4218
|
+
if (this._storeCleanups) {
|
|
4219
|
+
this._storeCleanups.forEach(fn => fn());
|
|
4220
|
+
this._storeCleanups = [];
|
|
4221
|
+
}
|
|
3984
4222
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
3985
4223
|
this._listeners = [];
|
|
3986
4224
|
if (this._outsideListeners) {
|
|
@@ -4015,7 +4253,7 @@ class Component {
|
|
|
4015
4253
|
const _reservedKeys = new Set([
|
|
4016
4254
|
'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
|
|
4017
4255
|
'templateUrl', 'styleUrl', 'templates', 'base',
|
|
4018
|
-
'computed', 'watch'
|
|
4256
|
+
'computed', 'watch', 'stores', 'transition', 'activated', 'deactivated'
|
|
4019
4257
|
]);
|
|
4020
4258
|
|
|
4021
4259
|
|
|
@@ -4322,6 +4560,9 @@ class Router {
|
|
|
4322
4560
|
const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
|
|
4323
4561
|
this._mode = isFile ? 'hash' : (config.mode || 'history');
|
|
4324
4562
|
|
|
4563
|
+
// Keep-alive cache: component name → { container, instance }
|
|
4564
|
+
this._keepAliveCache = new Map();
|
|
4565
|
+
|
|
4325
4566
|
// Base path for sub-path deployments
|
|
4326
4567
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|
|
4327
4568
|
let rawBase = config.base;
|
|
@@ -4879,34 +5120,96 @@ class Router {
|
|
|
4879
5120
|
await prefetch(matched.component);
|
|
4880
5121
|
}
|
|
4881
5122
|
|
|
4882
|
-
|
|
4883
|
-
|
|
5123
|
+
const isKeepAlive = !!matched.keepAlive;
|
|
5124
|
+
const componentName = typeof matched.component === 'string' ? matched.component : null;
|
|
5125
|
+
|
|
5126
|
+
// Deactivate previous keep-alive instance (hide instead of destroy)
|
|
5127
|
+
if (this._instance && this._currentKeepAlive && this._currentComponentName) {
|
|
5128
|
+
const cached = this._keepAliveCache.get(this._currentComponentName);
|
|
5129
|
+
if (cached) {
|
|
5130
|
+
cached.container.style.display = 'none';
|
|
5131
|
+
// Call deactivated() lifecycle hook
|
|
5132
|
+
if (cached.instance._def.deactivated) {
|
|
5133
|
+
try { cached.instance._def.deactivated.call(cached.instance); }
|
|
5134
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._currentComponentName}" deactivated() threw`, { component: this._currentComponentName }, err); }
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
this._instance = null;
|
|
5138
|
+
} else if (this._instance) {
|
|
5139
|
+
// Destroy previous non-keepAlive instance
|
|
4884
5140
|
this._instance.destroy();
|
|
4885
5141
|
this._instance = null;
|
|
4886
5142
|
}
|
|
4887
5143
|
|
|
4888
|
-
// Create container
|
|
4889
5144
|
const _routeStart = typeof window !== 'undefined' && window.__zqRenderHook ? performance.now() : 0;
|
|
4890
|
-
this._el.innerHTML = '';
|
|
4891
5145
|
|
|
4892
5146
|
// Pass route params and query as props
|
|
4893
5147
|
const props = { ...params, $route: to, $query: query, $params: params };
|
|
4894
5148
|
|
|
5149
|
+
// Keep-alive: reuse cached instance
|
|
5150
|
+
if (isKeepAlive && componentName && this._keepAliveCache.has(componentName)) {
|
|
5151
|
+
const cached = this._keepAliveCache.get(componentName);
|
|
5152
|
+
// Hide all children, show the cached one
|
|
5153
|
+
[...this._el.children].forEach(c => { c.style.display = 'none'; });
|
|
5154
|
+
cached.container.style.display = '';
|
|
5155
|
+
this._instance = cached.instance;
|
|
5156
|
+
this._currentKeepAlive = true;
|
|
5157
|
+
this._currentComponentName = componentName;
|
|
5158
|
+
// Call activated() lifecycle hook
|
|
5159
|
+
if (cached.instance._def.activated) {
|
|
5160
|
+
try { cached.instance._def.activated.call(cached.instance); }
|
|
5161
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
|
|
5162
|
+
}
|
|
5163
|
+
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
|
|
5164
|
+
}
|
|
4895
5165
|
// If component is a string (registered name), mount it
|
|
4896
|
-
if (
|
|
4897
|
-
|
|
5166
|
+
else if (componentName) {
|
|
5167
|
+
// Hide all keep-alive cached children (don't destroy)
|
|
5168
|
+
[...this._el.children].forEach(c => {
|
|
5169
|
+
if (c.dataset.zqKeepAlive) {
|
|
5170
|
+
c.style.display = 'none';
|
|
5171
|
+
}
|
|
5172
|
+
});
|
|
5173
|
+
// Remove non-keep-alive children
|
|
5174
|
+
[...this._el.children].forEach(c => {
|
|
5175
|
+
if (!c.dataset.zqKeepAlive) c.remove();
|
|
5176
|
+
});
|
|
5177
|
+
|
|
5178
|
+
const container = document.createElement(componentName);
|
|
5179
|
+
if (isKeepAlive) container.dataset.zqKeepAlive = componentName;
|
|
4898
5180
|
this._el.appendChild(container);
|
|
4899
5181
|
try {
|
|
4900
|
-
this._instance = mount(container,
|
|
5182
|
+
this._instance = mount(container, componentName, props);
|
|
4901
5183
|
} catch (err) {
|
|
4902
5184
|
reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
|
|
4903
5185
|
return;
|
|
4904
5186
|
}
|
|
4905
|
-
|
|
5187
|
+
|
|
5188
|
+
if (isKeepAlive) {
|
|
5189
|
+
this._keepAliveCache.set(componentName, { container, instance: this._instance });
|
|
5190
|
+
// Call activated() on first mount
|
|
5191
|
+
if (this._instance._def.activated) {
|
|
5192
|
+
try { this._instance._def.activated.call(this._instance); }
|
|
5193
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${componentName}" activated() threw`, { component: componentName }, err); }
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
this._currentKeepAlive = isKeepAlive;
|
|
5198
|
+
this._currentComponentName = componentName;
|
|
5199
|
+
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', componentName);
|
|
4906
5200
|
}
|
|
4907
5201
|
// If component is a render function
|
|
4908
5202
|
else if (typeof matched.component === 'function') {
|
|
4909
|
-
|
|
5203
|
+
// Clear non-keepAlive content
|
|
5204
|
+
[...this._el.children].forEach(c => {
|
|
5205
|
+
if (c.dataset.zqKeepAlive) c.style.display = 'none';
|
|
5206
|
+
else c.remove();
|
|
5207
|
+
});
|
|
5208
|
+
const wrapper = document.createElement('div');
|
|
5209
|
+
wrapper.innerHTML = matched.component(to);
|
|
5210
|
+
while (wrapper.firstChild) this._el.appendChild(wrapper.firstChild);
|
|
5211
|
+
this._currentKeepAlive = false;
|
|
5212
|
+
this._currentComponentName = null;
|
|
4910
5213
|
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', to);
|
|
4911
5214
|
}
|
|
4912
5215
|
}
|
|
@@ -4965,6 +5268,11 @@ class Router {
|
|
|
4965
5268
|
document.removeEventListener('click', this._onLinkClick);
|
|
4966
5269
|
this._onLinkClick = null;
|
|
4967
5270
|
}
|
|
5271
|
+
// Destroy all keep-alive cached instances
|
|
5272
|
+
for (const [, cached] of this._keepAliveCache) {
|
|
5273
|
+
cached.instance.destroy();
|
|
5274
|
+
}
|
|
5275
|
+
this._keepAliveCache.clear();
|
|
4968
5276
|
if (this._instance) this._instance.destroy();
|
|
4969
5277
|
this._listeners.clear();
|
|
4970
5278
|
this._substateListeners = [];
|
|
@@ -5135,8 +5443,9 @@ class Store {
|
|
|
5135
5443
|
batch(fn) {
|
|
5136
5444
|
this._batching = true;
|
|
5137
5445
|
this._batchQueue = [];
|
|
5446
|
+
let result;
|
|
5138
5447
|
try {
|
|
5139
|
-
fn(this.state);
|
|
5448
|
+
result = fn(this.state);
|
|
5140
5449
|
} finally {
|
|
5141
5450
|
this._batching = false;
|
|
5142
5451
|
// Deduplicate: keep only the last change per key
|
|
@@ -5149,6 +5458,7 @@ class Store {
|
|
|
5149
5458
|
this._notifySubscribers(key, value, old);
|
|
5150
5459
|
}
|
|
5151
5460
|
}
|
|
5461
|
+
return result;
|
|
5152
5462
|
}
|
|
5153
5463
|
|
|
5154
5464
|
/**
|
|
@@ -5236,8 +5546,14 @@ class Store {
|
|
|
5236
5546
|
}
|
|
5237
5547
|
|
|
5238
5548
|
/**
|
|
5239
|
-
* Subscribe to changes on a specific state key
|
|
5240
|
-
*
|
|
5549
|
+
* Subscribe to changes on a specific state key, multiple keys, or all changes.
|
|
5550
|
+
*
|
|
5551
|
+
* Signatures:
|
|
5552
|
+
* subscribe(callback) → wildcard, fires on every change
|
|
5553
|
+
* subscribe('key', callback) → fires when 'key' changes
|
|
5554
|
+
* subscribe(['a','b'], callback) → fires when any listed key changes
|
|
5555
|
+
*
|
|
5556
|
+
* @param {string|string[]|Function} keyOrFn - state key, array of keys, or function for all changes
|
|
5241
5557
|
* @param {Function} [fn] - callback (key, value, oldValue)
|
|
5242
5558
|
* @returns {Function} - unsubscribe
|
|
5243
5559
|
*/
|
|
@@ -5248,6 +5564,16 @@ class Store {
|
|
|
5248
5564
|
return () => this._wildcards.delete(keyOrFn);
|
|
5249
5565
|
}
|
|
5250
5566
|
|
|
5567
|
+
// Multi-key subscription: subscribe(['files', 'isProcessing'], callback)
|
|
5568
|
+
if (Array.isArray(keyOrFn)) {
|
|
5569
|
+
const keys = keyOrFn;
|
|
5570
|
+
const handler = (key, value, old) => {
|
|
5571
|
+
if (keys.includes(key)) fn(key, value, old);
|
|
5572
|
+
};
|
|
5573
|
+
this._wildcards.add(handler);
|
|
5574
|
+
return () => this._wildcards.delete(handler);
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5251
5577
|
if (!this._subscribers.has(keyOrFn)) {
|
|
5252
5578
|
this._subscribers.set(keyOrFn, new Set());
|
|
5253
5579
|
}
|
|
@@ -5318,6 +5644,34 @@ function createStore(name, config) {
|
|
|
5318
5644
|
|
|
5319
5645
|
function getStore(name = 'default') {
|
|
5320
5646
|
return _stores.get(name) || null;
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5649
|
+
|
|
5650
|
+
// ---------------------------------------------------------------------------
|
|
5651
|
+
// Store-Component Connector
|
|
5652
|
+
// ---------------------------------------------------------------------------
|
|
5653
|
+
|
|
5654
|
+
/**
|
|
5655
|
+
* Create a store connector descriptor for use in component definitions.
|
|
5656
|
+
* When used in a component's `stores` config, auto-subscribes to the
|
|
5657
|
+
* listed keys on mount and cleans up on destroy.
|
|
5658
|
+
*
|
|
5659
|
+
* Usage:
|
|
5660
|
+
* $.component('my-comp', {
|
|
5661
|
+
* stores: {
|
|
5662
|
+
* app: connectStore(appStore, ['files', 'isProcessing']),
|
|
5663
|
+
* },
|
|
5664
|
+
* render() {
|
|
5665
|
+
* return `<div>${this.stores.app.files.length} files</div>`;
|
|
5666
|
+
* }
|
|
5667
|
+
* });
|
|
5668
|
+
*
|
|
5669
|
+
* @param {Store} store - the store instance to connect
|
|
5670
|
+
* @param {string[]} keys - state keys to sync
|
|
5671
|
+
* @returns {{ _zqConnector: true, store: Store, keys: string[] }}
|
|
5672
|
+
*/
|
|
5673
|
+
function connectStore(store, keys) {
|
|
5674
|
+
return { _zqConnector: true, store, keys };
|
|
5321
5675
|
}
|
|
5322
5676
|
|
|
5323
5677
|
// --- src/http.js -------------------------------------------------
|
|
@@ -6195,6 +6549,7 @@ $.matchRoute = matchRoute;
|
|
|
6195
6549
|
// --- Store -----------------------------------------------------------------
|
|
6196
6550
|
$.store = createStore;
|
|
6197
6551
|
$.getStore = getStore;
|
|
6552
|
+
$.connectStore = connectStore;
|
|
6198
6553
|
|
|
6199
6554
|
// --- HTTP ------------------------------------------------------------------
|
|
6200
6555
|
$.http = http;
|
|
@@ -6254,11 +6609,18 @@ $.validate = validate;
|
|
|
6254
6609
|
$.formatError = formatError;
|
|
6255
6610
|
|
|
6256
6611
|
// --- Meta ------------------------------------------------------------------
|
|
6257
|
-
$.version = '1.
|
|
6258
|
-
$.libSize = '~
|
|
6259
|
-
$.unitTests = {"passed":
|
|
6612
|
+
$.version = '1.1.1';
|
|
6613
|
+
$.libSize = '~115 KB';
|
|
6614
|
+
$.unitTests = {"passed":2281,"failed":0,"total":2281,"suites":565,"duration":6929,"ok":true};
|
|
6260
6615
|
$.meta = {}; // populated at build time by CLI bundler
|
|
6261
6616
|
|
|
6617
|
+
// --- Environment detection -------------------------------------------------
|
|
6618
|
+
$.isElectron = typeof navigator !== 'undefined' && /Electron/i.test(navigator.userAgent)
|
|
6619
|
+
|| typeof process !== 'undefined' && process.versions != null && !!process.versions.electron;
|
|
6620
|
+
$.platform = $.isElectron ? 'electron'
|
|
6621
|
+
: typeof window !== 'undefined' ? 'browser'
|
|
6622
|
+
: 'node';
|
|
6623
|
+
|
|
6262
6624
|
$.noConflict = () => {
|
|
6263
6625
|
if (typeof window !== 'undefined' && window.$ === $) {
|
|
6264
6626
|
delete window.$;
|