@mintjamsinc/ichigojs 0.1.86 → 0.1.88
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/dist/ichigo.cjs +1760 -1443
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +1760 -1443
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +1760 -1443
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VBindings.d.ts +24 -0
- package/dist/types/ichigo/VBindingsPreparer.d.ts +6 -0
- package/dist/types/ichigo/VDOMUpdater.d.ts +6 -0
- package/dist/types/ichigo/VTextEvaluator.d.ts +6 -0
- package/dist/types/ichigo/directives/VConditionalDirective.d.ts +5 -0
- package/dist/types/ichigo/directives/VConditionalDirectiveContext.d.ts +5 -0
- package/dist/types/ichigo/util/ExpressionEvaluator.d.ts +18 -0
- package/dist/types/ichigo/util/ReactiveProxy.d.ts +34 -0
- package/package.json +1 -1
package/dist/ichigo.cjs
CHANGED
|
@@ -6734,1117 +6734,1235 @@
|
|
|
6734
6734
|
|
|
6735
6735
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
6736
6736
|
/**
|
|
6737
|
-
* Utility class for
|
|
6737
|
+
* Utility class for creating reactive proxies that automatically track changes.
|
|
6738
6738
|
*/
|
|
6739
|
-
class
|
|
6739
|
+
class ReactiveProxy {
|
|
6740
6740
|
/**
|
|
6741
|
-
*
|
|
6742
|
-
*
|
|
6743
|
-
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
6744
|
-
* @param options Optional parsing options.
|
|
6745
|
-
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
6746
|
-
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
6747
|
-
* for backward compatibility with interpolation and binding directives).
|
|
6748
|
-
* @returns An array of identifier names.
|
|
6741
|
+
* A WeakMap to store the proxy for each target object and path combination.
|
|
6742
|
+
* This prevents creating multiple proxies for the same object accessed from different paths.
|
|
6749
6743
|
*/
|
|
6750
|
-
static
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
// For `this.x` or `this.a.b` we don't want to include 'this' itself.
|
|
6802
|
-
// Stop unwrapping here; we'll drop a leading 'this' later.
|
|
6803
|
-
break;
|
|
6804
|
-
}
|
|
6805
|
-
else if (cur.object.type === 'MemberExpression') {
|
|
6806
|
-
// continue unwrapping
|
|
6807
|
-
cur = cur.object;
|
|
6808
|
-
}
|
|
6809
|
-
else {
|
|
6810
|
-
// other object types (CallExpression, etc.) — stop here
|
|
6811
|
-
break;
|
|
6812
|
-
}
|
|
6813
|
-
}
|
|
6814
|
-
else {
|
|
6815
|
-
break;
|
|
6816
|
-
}
|
|
6817
|
-
}
|
|
6818
|
-
// A chain that does not start at a name (e.g. `this.a.b` or `f().b`) begins
|
|
6819
|
-
// with its first property; one that begins with an index names no binding.
|
|
6820
|
-
if (parts.length > 0 && parts[0].startsWith('.')) {
|
|
6821
|
-
parts[0] = parts[0].substring(1);
|
|
6822
|
-
}
|
|
6823
|
-
if (parts.length > 0 && !parts[0].startsWith('[')) {
|
|
6824
|
-
// Add progressive chains: 'a', 'a.b', 'a.b[0]', 'a.b[0].c'
|
|
6825
|
-
for (let i = 0; i < parts.length; i++) {
|
|
6826
|
-
const chain = parts.slice(0, i + 1).join('');
|
|
6827
|
-
identifiers.add(chain);
|
|
6828
|
-
// Also apply functionDependencies lookup for the chain key
|
|
6829
|
-
if (functionDependencies[chain]) {
|
|
6830
|
-
for (const dependency of functionDependencies[chain]) {
|
|
6831
|
-
identifiers.add(dependency);
|
|
6832
|
-
}
|
|
6833
|
-
}
|
|
6834
|
-
}
|
|
6835
|
-
}
|
|
6836
|
-
}
|
|
6837
|
-
});
|
|
6838
|
-
return Array.from(identifiers);
|
|
6744
|
+
static proxyCache = new WeakMap();
|
|
6745
|
+
/**
|
|
6746
|
+
* A WeakMap to track which objects are proxies, mapping proxy -> original target.
|
|
6747
|
+
* This prevents double-wrapping of already proxied objects.
|
|
6748
|
+
*/
|
|
6749
|
+
static proxyToTarget = new WeakMap();
|
|
6750
|
+
/**
|
|
6751
|
+
* A WeakSet to track objects marked as "raw" (non-reactive).
|
|
6752
|
+
* These objects will not be wrapped with Proxy.
|
|
6753
|
+
*/
|
|
6754
|
+
static rawObjects = new WeakSet();
|
|
6755
|
+
/**
|
|
6756
|
+
* A WeakMap to store the path for each proxy object.
|
|
6757
|
+
* This allows retrieving the source path of an object for computed property mapping.
|
|
6758
|
+
*/
|
|
6759
|
+
static proxyPaths = new WeakMap();
|
|
6760
|
+
/**
|
|
6761
|
+
* Dispatchers per (target, path). Every target reached while walking a proxy
|
|
6762
|
+
* subtree registers an entry pointing at the same dispatcher as the outermost
|
|
6763
|
+
* proxy of that subtree, so callers can look up the dispatcher from any
|
|
6764
|
+
* intermediate proxy when subscribing.
|
|
6765
|
+
*/
|
|
6766
|
+
static dispatchers = new WeakMap();
|
|
6767
|
+
/**
|
|
6768
|
+
* A Map to store path aliases.
|
|
6769
|
+
* Key: alias path (e.g., "editingNestedStep.steps")
|
|
6770
|
+
* Value: source path (e.g., "routes[0].steps[0].steps")
|
|
6771
|
+
* This allows mapping variable names to their actual source paths for dependency tracking.
|
|
6772
|
+
*/
|
|
6773
|
+
static pathAliases = new Map();
|
|
6774
|
+
/**
|
|
6775
|
+
* Compiled patterns for identifiers that contain a "[*]" index, keyed by the
|
|
6776
|
+
* identifier and whether descendants of the path also match.
|
|
6777
|
+
*/
|
|
6778
|
+
static pathPatterns = new Map();
|
|
6779
|
+
/**
|
|
6780
|
+
* The set collecting the paths read through any proxy while track() runs, or
|
|
6781
|
+
* undefined when no expression is being tracked.
|
|
6782
|
+
*/
|
|
6783
|
+
static tracker;
|
|
6784
|
+
/**
|
|
6785
|
+
* Whether a property key is spelled as an array index ("0", "12") rather than a
|
|
6786
|
+
* property name. Both the proxy and the expression analyzer decide between
|
|
6787
|
+
* "items[0]" and "items.name" with this test alone, so a numeric key on a plain
|
|
6788
|
+
* object and a numeric key on an array get the same spelling.
|
|
6789
|
+
*
|
|
6790
|
+
* @param key The property key as a string.
|
|
6791
|
+
* @returns True if the key is a canonical non-negative integer.
|
|
6792
|
+
*/
|
|
6793
|
+
static isIndexKey(key) {
|
|
6794
|
+
return /^(?:0|[1-9]\d*)$/.test(key);
|
|
6839
6795
|
}
|
|
6840
6796
|
/**
|
|
6841
|
-
*
|
|
6842
|
-
*
|
|
6843
|
-
*
|
|
6844
|
-
* @
|
|
6797
|
+
* Appends a property key to a source path the way change paths are reported:
|
|
6798
|
+
* an index in brackets ("items[0]"), a name after a dot ("user.name").
|
|
6799
|
+
*
|
|
6800
|
+
* @param path The path of the containing object, or "" for a root binding.
|
|
6801
|
+
* @param key The property key being appended.
|
|
6802
|
+
* @returns The joined path.
|
|
6845
6803
|
*/
|
|
6846
|
-
static
|
|
6847
|
-
const
|
|
6848
|
-
|
|
6804
|
+
static joinPath(path, key) {
|
|
6805
|
+
const keyStr = String(key);
|
|
6806
|
+
if (!path) {
|
|
6807
|
+
return keyStr;
|
|
6808
|
+
}
|
|
6809
|
+
return this.isIndexKey(keyStr) ? `${path}[${keyStr}]` : `${path}.${keyStr}`;
|
|
6849
6810
|
}
|
|
6850
6811
|
/**
|
|
6851
|
-
*
|
|
6852
|
-
*
|
|
6853
|
-
*
|
|
6812
|
+
* Runs fn while recording, into paths, the source path of every property read
|
|
6813
|
+
* through a reactive proxy. Nested calls record into the innermost set only;
|
|
6814
|
+
* the previous set is restored when fn returns or throws.
|
|
6815
|
+
*
|
|
6816
|
+
* @param paths The set that receives the paths read.
|
|
6817
|
+
* @param fn The function to run.
|
|
6818
|
+
* @returns The value fn returns.
|
|
6854
6819
|
*/
|
|
6855
|
-
static
|
|
6856
|
-
const
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
let source = func.toString();
|
|
6861
|
-
// Handle different function formats
|
|
6862
|
-
// Arrow function: () => expr or (args) => { ... }
|
|
6863
|
-
// Method shorthand: methodName() { ... }
|
|
6864
|
-
// Function expression: function() { ... }
|
|
6865
|
-
// Detect function type more accurately
|
|
6866
|
-
const isArrowFunction = /^\s*(\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(source);
|
|
6867
|
-
const isFunctionExpression = source.startsWith('function');
|
|
6868
|
-
const isAsyncFunction = source.startsWith('async');
|
|
6869
|
-
// If it's a method shorthand (e.g., "methodName() { ... }" or "async methodName() { ... }"), convert to function expression
|
|
6870
|
-
if (!isFunctionExpression && !isArrowFunction) {
|
|
6871
|
-
// It's likely a method shorthand, convert to function expression
|
|
6872
|
-
if (isAsyncFunction) {
|
|
6873
|
-
// Remove 'async' prefix and add 'async function' prefix
|
|
6874
|
-
source = `async function ${source.substring(5).trim()}`;
|
|
6875
|
-
}
|
|
6876
|
-
else {
|
|
6877
|
-
source = `function ${source}`;
|
|
6878
|
-
}
|
|
6879
|
-
}
|
|
6880
|
-
// Wrap in parentheses for parsing
|
|
6881
|
-
source = `(${source})`;
|
|
6882
|
-
const ast = parse(source, { ecmaVersion: "latest" });
|
|
6883
|
-
const dependencies = new Set();
|
|
6884
|
-
const declaredVariables = new Set();
|
|
6885
|
-
const thisAliases = new Set();
|
|
6886
|
-
// First, collect all declared variables (const, let, var, function params, etc.)
|
|
6887
|
-
ancestor(ast, {
|
|
6888
|
-
VariableDeclarator(node) {
|
|
6889
|
-
if (node.id.type === 'Identifier') {
|
|
6890
|
-
declaredVariables.add(node.id.name);
|
|
6891
|
-
// Detect aliases assigned from `this`, e.g. `let vm = this;`
|
|
6892
|
-
if (node.init && node.init.type === 'ThisExpression') {
|
|
6893
|
-
thisAliases.add(node.id.name);
|
|
6894
|
-
}
|
|
6895
|
-
}
|
|
6896
|
-
},
|
|
6897
|
-
FunctionDeclaration(node) {
|
|
6898
|
-
if (node.id && node.id.type === 'Identifier') {
|
|
6899
|
-
declaredVariables.add(node.id.name);
|
|
6900
|
-
}
|
|
6901
|
-
// Add function parameters
|
|
6902
|
-
node.params.forEach((param) => {
|
|
6903
|
-
if (param.type === 'Identifier') {
|
|
6904
|
-
declaredVariables.add(param.name);
|
|
6905
|
-
}
|
|
6906
|
-
});
|
|
6907
|
-
},
|
|
6908
|
-
FunctionExpression(node, ancestors) {
|
|
6909
|
-
// Only process the outermost function (skip nested functions)
|
|
6910
|
-
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
6911
|
-
n.type === 'FunctionDeclaration' ||
|
|
6912
|
-
n.type === 'ArrowFunctionExpression');
|
|
6913
|
-
if (functionAncestors.length === 1) {
|
|
6914
|
-
// Add function parameters of the main function
|
|
6915
|
-
node.params.forEach((param) => {
|
|
6916
|
-
if (param.type === 'Identifier') {
|
|
6917
|
-
declaredVariables.add(param.name);
|
|
6918
|
-
}
|
|
6919
|
-
});
|
|
6920
|
-
}
|
|
6921
|
-
},
|
|
6922
|
-
ArrowFunctionExpression(node, ancestors) {
|
|
6923
|
-
// Only process the outermost function (skip nested functions)
|
|
6924
|
-
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
6925
|
-
n.type === 'FunctionDeclaration' ||
|
|
6926
|
-
n.type === 'ArrowFunctionExpression');
|
|
6927
|
-
if (functionAncestors.length === 1) {
|
|
6928
|
-
// Add function parameters of the main function
|
|
6929
|
-
node.params.forEach((param) => {
|
|
6930
|
-
if (param.type === 'Identifier') {
|
|
6931
|
-
declaredVariables.add(param.name);
|
|
6932
|
-
}
|
|
6933
|
-
});
|
|
6934
|
-
}
|
|
6935
|
-
}
|
|
6936
|
-
});
|
|
6937
|
-
// Then, collect identifiers that are not declared within the function
|
|
6938
|
-
simple(ast, {
|
|
6939
|
-
Identifier(node) {
|
|
6940
|
-
// Skip the function name itself, declared variables, and common globals
|
|
6941
|
-
if (node.name !== funcName && !declaredVariables.has(node.name)) {
|
|
6942
|
-
dependencies.add(node.name);
|
|
6943
|
-
}
|
|
6944
|
-
},
|
|
6945
|
-
MemberExpression(node) {
|
|
6946
|
-
// Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
|
|
6947
|
-
if (node.property.type === 'Identifier') {
|
|
6948
|
-
if (node.object.type === 'ThisExpression') {
|
|
6949
|
-
dependencies.add(node.property.name);
|
|
6950
|
-
}
|
|
6951
|
-
else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
|
|
6952
|
-
dependencies.add(node.property.name);
|
|
6953
|
-
}
|
|
6954
|
-
}
|
|
6955
|
-
}
|
|
6956
|
-
});
|
|
6957
|
-
functionDependencies[funcName] = Array.from(dependencies);
|
|
6958
|
-
}
|
|
6959
|
-
catch (error) {
|
|
6960
|
-
console.warn(`Failed to analyze function '${funcName}':`, error);
|
|
6961
|
-
// If parsing fails, assume no dependencies
|
|
6962
|
-
functionDependencies[funcName] = [];
|
|
6963
|
-
}
|
|
6964
|
-
}
|
|
6965
|
-
// Second pass: resolve each function's transitive reactive dependencies.
|
|
6966
|
-
//
|
|
6967
|
-
// A function's direct dependency list mixes two kinds of identifiers: references to other
|
|
6968
|
-
// functions (call edges) and references to reactive data properties (the values we actually
|
|
6969
|
-
// care about). We want, for every function, the full set of reactive properties reachable by
|
|
6970
|
-
// following call edges to any depth.
|
|
6971
|
-
//
|
|
6972
|
-
// Functions are free to reference each other cyclically — a poller that reschedules itself
|
|
6973
|
-
// (`scheduleOperationPoll` -> `pollOperation` -> `scheduleOperationPoll`), a pair of mutually
|
|
6974
|
-
// recursive helpers, or any state machine. This is valid JavaScript, so the resolver must not
|
|
6975
|
-
// treat it as an error. Every function in a cycle (more precisely, a strongly connected
|
|
6976
|
-
// component) is reachable from the others, so they all share the same transitive dependency
|
|
6977
|
-
// set: the union of every member's direct reactive dependencies.
|
|
6978
|
-
//
|
|
6979
|
-
// A depth-first walk that bails out on back-edges cannot compute that correctly — it drops the
|
|
6980
|
-
// dependencies contributed around the cycle and caches incomplete, traversal-order-dependent
|
|
6981
|
-
// results, which would silently break reactivity for any computed that reads reactive data
|
|
6982
|
-
// through a cyclic method. We instead split each function's direct dependencies into reactive
|
|
6983
|
-
// properties and call edges, then propagate reactive properties along call edges until the sets
|
|
6984
|
-
// stop growing (a fixpoint). This converges regardless of cycles and yields the exact transitive
|
|
6985
|
-
// closure for every function, including those participating in a cycle.
|
|
6986
|
-
const directReactive = {};
|
|
6987
|
-
const callEdges = {};
|
|
6988
|
-
for (const funcName of Object.keys(functions)) {
|
|
6989
|
-
const reactive = new Set();
|
|
6990
|
-
const edges = [];
|
|
6991
|
-
for (const dep of functionDependencies[funcName] || []) {
|
|
6992
|
-
if (dep === funcName)
|
|
6993
|
-
continue; // Skip self-references.
|
|
6994
|
-
if (functions[dep]) {
|
|
6995
|
-
edges.push(dep); // Call edge to another function.
|
|
6996
|
-
}
|
|
6997
|
-
else {
|
|
6998
|
-
reactive.add(dep); // Reactive data property (terminal dependency).
|
|
6999
|
-
}
|
|
7000
|
-
}
|
|
7001
|
-
directReactive[funcName] = reactive;
|
|
7002
|
-
callEdges[funcName] = edges;
|
|
7003
|
-
}
|
|
7004
|
-
// Seed each function with its own direct reactive dependencies, then propagate along call edges
|
|
7005
|
-
// until no set changes. Each pass can only add properties, and the universe of properties is
|
|
7006
|
-
// finite, so the loop is guaranteed to terminate.
|
|
7007
|
-
const resolved = {};
|
|
7008
|
-
for (const funcName of Object.keys(functions)) {
|
|
7009
|
-
resolved[funcName] = new Set(directReactive[funcName]);
|
|
7010
|
-
}
|
|
7011
|
-
let changed = true;
|
|
7012
|
-
while (changed) {
|
|
7013
|
-
changed = false;
|
|
7014
|
-
for (const funcName of Object.keys(functions)) {
|
|
7015
|
-
const target = resolved[funcName];
|
|
7016
|
-
for (const edge of callEdges[funcName]) {
|
|
7017
|
-
for (const dep of resolved[edge]) {
|
|
7018
|
-
if (!target.has(dep)) {
|
|
7019
|
-
target.add(dep);
|
|
7020
|
-
changed = true;
|
|
7021
|
-
}
|
|
7022
|
-
}
|
|
7023
|
-
}
|
|
7024
|
-
}
|
|
6820
|
+
static track(paths, fn) {
|
|
6821
|
+
const previous = this.tracker;
|
|
6822
|
+
this.tracker = paths;
|
|
6823
|
+
try {
|
|
6824
|
+
return fn();
|
|
7025
6825
|
}
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
resolvedDependencies[funcName] = Array.from(resolved[funcName]);
|
|
6826
|
+
finally {
|
|
6827
|
+
this.tracker = previous;
|
|
7029
6828
|
}
|
|
7030
|
-
return resolvedDependencies;
|
|
7031
6829
|
}
|
|
7032
6830
|
/**
|
|
7033
|
-
*
|
|
7034
|
-
*
|
|
7035
|
-
*
|
|
7036
|
-
*
|
|
7037
|
-
*
|
|
7038
|
-
*
|
|
6831
|
+
* The `Object.prototype.toString` tags of the *only* value types we deeply
|
|
6832
|
+
* proxy. Everything else is returned untouched (raw).
|
|
6833
|
+
*
|
|
6834
|
+
* Rationale: a Proxy can only safely intercept plain data containers. Built-in
|
|
6835
|
+
* and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
|
|
6836
|
+
* `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
|
|
6837
|
+
* slots, and invoking their native methods with a Proxy as the `this`/receiver
|
|
6838
|
+
* throws "Illegal invocation". They must therefore never be wrapped. This is an
|
|
6839
|
+
* allow-list rather than a deny-list so that *every* such exotic type is
|
|
6840
|
+
* excluded by default, not just the handful we happened to enumerate.
|
|
6841
|
+
*
|
|
6842
|
+
* Plain user-defined class instances report `[object Object]` (unless they set
|
|
6843
|
+
* `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
|
|
6844
|
+
* `Map`/`Set` stay reactive because the proxy specially wraps their mutation
|
|
6845
|
+
* methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
|
|
6846
|
+
* since they cannot be wrapped that way and would break the same as `File`.
|
|
6847
|
+
*
|
|
6848
|
+
* Callers that want a plain object kept non-reactive can still opt out
|
|
6849
|
+
* explicitly via {@link markRaw}.
|
|
7039
6850
|
*/
|
|
7040
|
-
static
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
6851
|
+
static REACTIVE_TYPE_TAGS = new Set([
|
|
6852
|
+
'[object Object]',
|
|
6853
|
+
'[object Array]',
|
|
6854
|
+
'[object Arguments]',
|
|
6855
|
+
'[object Map]',
|
|
6856
|
+
'[object Set]',
|
|
6857
|
+
]);
|
|
6858
|
+
/**
|
|
6859
|
+
* Whether the given non-null object value is one we deeply proxy.
|
|
6860
|
+
* See {@link REACTIVE_TYPE_TAGS} for the reasoning.
|
|
6861
|
+
*/
|
|
6862
|
+
static isReactivable(value) {
|
|
6863
|
+
return this.REACTIVE_TYPE_TAGS.has(Object.prototype.toString.call(value));
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Creates a reactive proxy for the given object.
|
|
6867
|
+
* The proxy will call the onChange callback whenever a property is modified.
|
|
6868
|
+
*
|
|
6869
|
+
* @param target The object to make reactive.
|
|
6870
|
+
* @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
|
|
6871
|
+
* @param path The current path in the object tree (used internally for nested objects).
|
|
6872
|
+
* @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
|
|
6873
|
+
* @returns A reactive proxy of the target object.
|
|
6874
|
+
*/
|
|
6875
|
+
static create(target, onChange, path = '', inheritedDispatcher) {
|
|
6876
|
+
// If the target is not an object or is null, return it as-is
|
|
6877
|
+
if (typeof target !== 'object' || target === null) {
|
|
6878
|
+
return target;
|
|
7056
6879
|
}
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
return
|
|
6880
|
+
// Don't wrap objects marked as raw (non-reactive)
|
|
6881
|
+
if (this.rawObjects.has(target)) {
|
|
6882
|
+
return target;
|
|
7060
6883
|
}
|
|
7061
|
-
//
|
|
7062
|
-
//
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
6884
|
+
// Only deeply proxy plain data containers; return every built-in/host
|
|
6885
|
+
// object (Date, File, Blob, Promise, WeakMap, DOM nodes, …) as-is, since
|
|
6886
|
+
// their native methods break when invoked through a Proxy receiver.
|
|
6887
|
+
if (!this.isReactivable(target)) {
|
|
6888
|
+
return target;
|
|
6889
|
+
}
|
|
6890
|
+
// Check if the target is already a proxy - if so, return it as-is to prevent double-wrapping
|
|
6891
|
+
if (this.proxyToTarget.has(target)) {
|
|
6892
|
+
return target;
|
|
6893
|
+
}
|
|
6894
|
+
// Resolve (and register) the dispatcher for this (target, path).
|
|
6895
|
+
// Nested create() calls inherit the dispatcher from the enclosing call so
|
|
6896
|
+
// that a single dispatcher fans out changes at any depth of the subtree.
|
|
6897
|
+
const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
|
|
6898
|
+
if (onChange) {
|
|
6899
|
+
dispatcher.listeners.add(onChange);
|
|
6900
|
+
}
|
|
6901
|
+
// Check if we already have a proxy for this target with this path
|
|
6902
|
+
let pathMap = this.proxyCache.get(target);
|
|
6903
|
+
if (pathMap) {
|
|
6904
|
+
const existingProxy = pathMap.get(path);
|
|
6905
|
+
if (existingProxy) {
|
|
6906
|
+
return existingProxy;
|
|
7069
6907
|
}
|
|
7070
6908
|
}
|
|
7071
|
-
|
|
7072
|
-
|
|
6909
|
+
else {
|
|
6910
|
+
pathMap = new Map();
|
|
6911
|
+
this.proxyCache.set(target, pathMap);
|
|
7073
6912
|
}
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
full(parsedAst, (node) => {
|
|
7090
|
-
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7091
|
-
locallyDeclared.add(node.id.name);
|
|
7092
|
-
if (node.init && node.init.type === 'ThisExpression') {
|
|
7093
|
-
thisAliases.add(node.id.name);
|
|
7094
|
-
}
|
|
6913
|
+
// Create the proxy with path captured in closure
|
|
6914
|
+
const proxy = new Proxy(target, {
|
|
6915
|
+
get(obj, key, receiver) {
|
|
6916
|
+
const value = Reflect.get(obj, key);
|
|
6917
|
+
// Record the read for an expression being tracked (see track()).
|
|
6918
|
+
// Function-valued members (push, Map#get, …) are accesses to behaviour,
|
|
6919
|
+
// not data, so they are left out.
|
|
6920
|
+
if (ReactiveProxy.tracker && typeof key !== 'symbol' && typeof value !== 'function') {
|
|
6921
|
+
ReactiveProxy.tracker.add(ReactiveProxy.joinPath(path, key));
|
|
6922
|
+
}
|
|
6923
|
+
// If the value is an object or array, make it reactive too
|
|
6924
|
+
if (typeof value === 'object' && value !== null) {
|
|
6925
|
+
// Don't wrap objects marked as raw (non-reactive)
|
|
6926
|
+
if (ReactiveProxy.rawObjects.has(value)) {
|
|
6927
|
+
return value;
|
|
7095
6928
|
}
|
|
7096
|
-
|
|
7097
|
-
|
|
6929
|
+
// Only deeply proxy plain data containers; hand back built-in
|
|
6930
|
+
// and host objects (Date, File, Blob, Promise, WeakMap, DOM
|
|
6931
|
+
// nodes, …) untouched so their native methods keep working.
|
|
6932
|
+
if (!ReactiveProxy.isReactivable(value)) {
|
|
6933
|
+
return value;
|
|
7098
6934
|
}
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7103
|
-
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
7104
|
-
if (node.type !== 'Identifier') {
|
|
7105
|
-
return;
|
|
7106
|
-
}
|
|
7107
|
-
// Skip if not in our identifier set
|
|
7108
|
-
if (!bindingIdentifiers.has(node.name)) {
|
|
7109
|
-
return;
|
|
6935
|
+
// Build the nested path
|
|
6936
|
+
const nestedPath = ReactiveProxy.joinPath(path, key);
|
|
6937
|
+
return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
|
|
7110
6938
|
}
|
|
7111
|
-
//
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
6939
|
+
// If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
|
|
6940
|
+
if (typeof value === 'function') {
|
|
6941
|
+
// For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
|
|
6942
|
+
if (Array.isArray(obj)) {
|
|
6943
|
+
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
6944
|
+
if (!arrayMutationMethods.includes(key)) {
|
|
6945
|
+
return value;
|
|
6946
|
+
}
|
|
6947
|
+
return function (...args) {
|
|
6948
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6949
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6950
|
+
return result;
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
// For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
|
|
6954
|
+
if (obj.constructor.name === 'Map') {
|
|
6955
|
+
const mapMutationMethods = ['set', 'delete', 'clear'];
|
|
6956
|
+
return function (...args) {
|
|
6957
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6958
|
+
if (mapMutationMethods.includes(key)) {
|
|
6959
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6960
|
+
}
|
|
6961
|
+
return result;
|
|
6962
|
+
};
|
|
6963
|
+
}
|
|
6964
|
+
// For Set, we only want to wrap mutation methods, not read methods like 'has'
|
|
6965
|
+
if (obj.constructor.name === 'Set') {
|
|
6966
|
+
const setMutationMethods = ['add', 'delete', 'clear'];
|
|
6967
|
+
return function (...args) {
|
|
6968
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6969
|
+
if (setMutationMethods.includes(key)) {
|
|
6970
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6971
|
+
}
|
|
6972
|
+
return result;
|
|
6973
|
+
};
|
|
6974
|
+
}
|
|
7134
6975
|
}
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
6976
|
+
return value;
|
|
6977
|
+
},
|
|
6978
|
+
set(obj, key, value) {
|
|
6979
|
+
const oldValue = Reflect.get(obj, key);
|
|
6980
|
+
const result = Reflect.set(obj, key, value);
|
|
6981
|
+
// Only trigger onChange if the value actually changed
|
|
6982
|
+
if (oldValue !== value) {
|
|
6983
|
+
ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
|
|
7139
6984
|
}
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
});
|
|
7147
|
-
// Sort replacements by start position (descending) to replace from end to start
|
|
7148
|
-
replacements.sort((a, b) => b.start - a.start);
|
|
7149
|
-
// Apply replacements
|
|
7150
|
-
let result = expression;
|
|
7151
|
-
for (const replacement of replacements) {
|
|
7152
|
-
const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
|
|
7153
|
-
result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
|
|
6985
|
+
return result;
|
|
6986
|
+
},
|
|
6987
|
+
deleteProperty(obj, key) {
|
|
6988
|
+
const result = Reflect.deleteProperty(obj, key);
|
|
6989
|
+
ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
|
|
6990
|
+
return result;
|
|
7154
6991
|
}
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
6992
|
+
});
|
|
6993
|
+
// Cache the proxy for this path
|
|
6994
|
+
pathMap.set(path, proxy);
|
|
6995
|
+
// Track that this proxy wraps the target to prevent double-wrapping
|
|
6996
|
+
this.proxyToTarget.set(proxy, target);
|
|
6997
|
+
// Store the path for this proxy (for computed property mapping)
|
|
6998
|
+
if (path) {
|
|
6999
|
+
this.proxyPaths.set(proxy, path);
|
|
7161
7000
|
}
|
|
7001
|
+
return proxy;
|
|
7162
7002
|
}
|
|
7163
|
-
}
|
|
7164
|
-
|
|
7165
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7166
|
-
/**
|
|
7167
|
-
* A centralized expression evaluator that normalizes expression evaluation across all directives.
|
|
7168
|
-
*
|
|
7169
|
-
* This class provides:
|
|
7170
|
-
* - Consistent identifier extraction and binding resolution
|
|
7171
|
-
* - Unified global object whitelisting (Math, Date, JSON, etc.)
|
|
7172
|
-
* - Expression caching for performance
|
|
7173
|
-
* - Centralized error handling
|
|
7174
|
-
* - Support for custom expression rewriting (for VOnDirective)
|
|
7175
|
-
*
|
|
7176
|
-
* Example usage:
|
|
7177
|
-
* ```typescript
|
|
7178
|
-
* const evaluator = ExpressionEvaluator.create(
|
|
7179
|
-
* "count + 1",
|
|
7180
|
-
* bindings,
|
|
7181
|
-
* functionDependencies
|
|
7182
|
-
* );
|
|
7183
|
-
* const result = evaluator.evaluate();
|
|
7184
|
-
* ```
|
|
7185
|
-
*/
|
|
7186
|
-
class ExpressionEvaluator {
|
|
7187
|
-
/**
|
|
7188
|
-
* Cache for compiled expression functions.
|
|
7189
|
-
* Key: expression string
|
|
7190
|
-
* Value: compiled function
|
|
7191
|
-
*/
|
|
7192
|
-
static cache = new Map();
|
|
7193
7003
|
/**
|
|
7194
|
-
*
|
|
7195
|
-
*
|
|
7004
|
+
* Looks up the dispatcher associated with (target, path), or installs a new one.
|
|
7005
|
+
* When called for a nested target during proxy walking, the enclosing dispatcher
|
|
7006
|
+
* is reused so a single subtree fans out changes through one notification path.
|
|
7196
7007
|
*/
|
|
7197
|
-
static
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
decodeURI,
|
|
7212
|
-
encodeURIComponent,
|
|
7213
|
-
decodeURIComponent
|
|
7214
|
-
};
|
|
7008
|
+
static resolveDispatcher(target, path, inheritedDispatcher) {
|
|
7009
|
+
let pathMap = this.dispatchers.get(target);
|
|
7010
|
+
if (!pathMap) {
|
|
7011
|
+
pathMap = new Map();
|
|
7012
|
+
this.dispatchers.set(target, pathMap);
|
|
7013
|
+
}
|
|
7014
|
+
const existing = pathMap.get(path);
|
|
7015
|
+
if (existing) {
|
|
7016
|
+
return existing;
|
|
7017
|
+
}
|
|
7018
|
+
const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
|
|
7019
|
+
pathMap.set(path, dispatcher);
|
|
7020
|
+
return dispatcher;
|
|
7021
|
+
}
|
|
7215
7022
|
/**
|
|
7216
|
-
*
|
|
7023
|
+
* Invokes every listener attached to the dispatcher.
|
|
7024
|
+
* Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
|
|
7217
7025
|
*/
|
|
7218
|
-
|
|
7026
|
+
static dispatch(dispatcher, changedPath) {
|
|
7027
|
+
if (dispatcher.listeners.size === 0) {
|
|
7028
|
+
return;
|
|
7029
|
+
}
|
|
7030
|
+
const snapshot = Array.from(dispatcher.listeners);
|
|
7031
|
+
for (const listener of snapshot) {
|
|
7032
|
+
listener(changedPath);
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7219
7035
|
/**
|
|
7220
|
-
*
|
|
7036
|
+
* Subscribes a listener to changes inside the subtree of an existing reactive proxy.
|
|
7037
|
+
*
|
|
7038
|
+
* The listener is scoped by the proxy's source path: only changes at or below that
|
|
7039
|
+
* path are delivered, which lets a child component receive notifications when the
|
|
7040
|
+
* nested contents of a prop change even though the prop reference itself is unchanged.
|
|
7041
|
+
*
|
|
7042
|
+
* @param proxyOrTarget A proxy returned from create(), or the underlying target object.
|
|
7043
|
+
* @param listener Called with the full source path of every relevant change.
|
|
7044
|
+
* @returns A function that removes the subscription.
|
|
7221
7045
|
*/
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7046
|
+
static subscribe(proxyOrTarget, listener) {
|
|
7047
|
+
if (typeof proxyOrTarget !== 'object' || proxyOrTarget === null) {
|
|
7048
|
+
return () => { };
|
|
7049
|
+
}
|
|
7050
|
+
const target = (this.proxyToTarget.get(proxyOrTarget) ?? proxyOrTarget);
|
|
7051
|
+
const scopePath = this.proxyPaths.get(proxyOrTarget) ?? '';
|
|
7052
|
+
const pathMap = this.dispatchers.get(target);
|
|
7053
|
+
const dispatcher = pathMap?.get(scopePath);
|
|
7054
|
+
if (!dispatcher) {
|
|
7055
|
+
return () => { };
|
|
7056
|
+
}
|
|
7057
|
+
const wrapper = scopePath
|
|
7058
|
+
? (changedPath) => {
|
|
7059
|
+
if (changedPath === scopePath ||
|
|
7060
|
+
(typeof changedPath === 'string' && (changedPath.startsWith(scopePath + '.') ||
|
|
7061
|
+
changedPath.startsWith(scopePath + '[')))) {
|
|
7062
|
+
listener(changedPath);
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
: listener;
|
|
7066
|
+
dispatcher.listeners.add(wrapper);
|
|
7067
|
+
return () => {
|
|
7068
|
+
dispatcher.listeners.delete(wrapper);
|
|
7069
|
+
};
|
|
7070
|
+
}
|
|
7225
7071
|
/**
|
|
7226
|
-
*
|
|
7072
|
+
* Checks if the given object is a reactive proxy.
|
|
7073
|
+
*
|
|
7074
|
+
* @param obj The object to check.
|
|
7075
|
+
* @returns True if the object is a reactive proxy, false otherwise.
|
|
7227
7076
|
*/
|
|
7228
|
-
|
|
7077
|
+
static isReactive(obj) {
|
|
7078
|
+
return this.proxyCache.has(obj);
|
|
7079
|
+
}
|
|
7229
7080
|
/**
|
|
7230
|
-
*
|
|
7081
|
+
* Unwraps a reactive proxy to get the original object.
|
|
7082
|
+
* If the object is not a proxy, returns it as-is.
|
|
7083
|
+
*
|
|
7084
|
+
* @param obj The object to unwrap.
|
|
7085
|
+
* @returns The original object.
|
|
7231
7086
|
*/
|
|
7232
|
-
|
|
7087
|
+
static unwrap(obj) {
|
|
7088
|
+
// This is a simplified implementation
|
|
7089
|
+
// In a full implementation, we'd need to store a reverse mapping
|
|
7090
|
+
return obj;
|
|
7091
|
+
}
|
|
7233
7092
|
/**
|
|
7234
|
-
*
|
|
7093
|
+
* Marks an object as "raw" (non-reactive).
|
|
7094
|
+
* Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
|
|
7095
|
+
* This is useful for objects that should not be reactive, such as:
|
|
7096
|
+
* - Objects with private fields (class instances with # fields)
|
|
7097
|
+
* - Third-party library instances
|
|
7098
|
+
* - Objects used only for method calls
|
|
7099
|
+
*
|
|
7100
|
+
* @param obj The object to mark as raw.
|
|
7101
|
+
* @returns The same object (for chaining).
|
|
7235
7102
|
*/
|
|
7236
|
-
|
|
7103
|
+
static markRaw(obj) {
|
|
7104
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
7105
|
+
this.rawObjects.add(obj);
|
|
7106
|
+
}
|
|
7107
|
+
return obj;
|
|
7108
|
+
}
|
|
7237
7109
|
/**
|
|
7238
|
-
*
|
|
7110
|
+
* Checks if an object is marked as raw (non-reactive).
|
|
7111
|
+
*
|
|
7112
|
+
* @param obj The object to check.
|
|
7113
|
+
* @returns True if the object is marked as raw, false otherwise.
|
|
7239
7114
|
*/
|
|
7240
|
-
|
|
7241
|
-
this.
|
|
7242
|
-
this.bindings = bindings;
|
|
7243
|
-
this.identifiers = identifiers;
|
|
7244
|
-
this.originalIdentifiers = originalIdentifiers;
|
|
7245
|
-
this.evaluatorFunc = evaluatorFunc;
|
|
7246
|
-
this.additionalContext = additionalContext;
|
|
7115
|
+
static isRaw(obj) {
|
|
7116
|
+
return typeof obj === 'object' && obj !== null && this.rawObjects.has(obj);
|
|
7247
7117
|
}
|
|
7248
7118
|
/**
|
|
7249
|
-
*
|
|
7250
|
-
*
|
|
7251
|
-
*
|
|
7252
|
-
*
|
|
7253
|
-
*
|
|
7254
|
-
* @param
|
|
7255
|
-
* @returns
|
|
7119
|
+
* Gets the source path for a proxy object.
|
|
7120
|
+
* This is used to map computed property values back to their source paths.
|
|
7121
|
+
* For example, if a computed property returns `model.elements[0]`,
|
|
7122
|
+
* this method returns "model.elements[0]" for that object.
|
|
7123
|
+
*
|
|
7124
|
+
* @param obj The proxy object to get the path for.
|
|
7125
|
+
* @returns The source path, or undefined if not found.
|
|
7256
7126
|
*/
|
|
7257
|
-
static
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
// For compilation we must only pass simple identifier names (no dots).
|
|
7261
|
-
// Use the base (left-most) segment of member chains and preserve order.
|
|
7262
|
-
const baseIdentifiers = [];
|
|
7263
|
-
for (const id of extractedIdentifiers) {
|
|
7264
|
-
const base = ExpressionUtils.baseIdentifier(id);
|
|
7265
|
-
if (!baseIdentifiers.includes(base)) {
|
|
7266
|
-
baseIdentifiers.push(base);
|
|
7267
|
-
}
|
|
7268
|
-
}
|
|
7269
|
-
// Apply custom rewrite if provided (used by VOnDirective)
|
|
7270
|
-
let processedExpression = expression;
|
|
7271
|
-
if (options?.rewriteExpression) {
|
|
7272
|
-
// Pass the original extracted identifiers (may include member chains)
|
|
7273
|
-
processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
|
|
7274
|
-
}
|
|
7275
|
-
// Build cache key including options that affect compilation
|
|
7276
|
-
const cacheKey = JSON.stringify({
|
|
7277
|
-
expr: processedExpression,
|
|
7278
|
-
additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
|
|
7279
|
-
});
|
|
7280
|
-
// Check cache
|
|
7281
|
-
let evaluatorFunc = this.cache.get(cacheKey);
|
|
7282
|
-
if (!evaluatorFunc) {
|
|
7283
|
-
// Compile the expression using base identifiers (no dots)
|
|
7284
|
-
evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
|
|
7285
|
-
// Cache the compiled function
|
|
7286
|
-
this.cache.set(cacheKey, evaluatorFunc);
|
|
7127
|
+
static getPath(obj) {
|
|
7128
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
7129
|
+
return undefined;
|
|
7287
7130
|
}
|
|
7288
|
-
return
|
|
7131
|
+
return this.proxyPaths.get(obj);
|
|
7289
7132
|
}
|
|
7290
7133
|
/**
|
|
7291
|
-
*
|
|
7134
|
+
* Registers a path alias.
|
|
7135
|
+
* This is used when a variable is assigned to reference an existing reactive object.
|
|
7136
|
+
* For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
|
|
7137
|
+
* so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
|
|
7292
7138
|
*
|
|
7293
|
-
* @param
|
|
7294
|
-
* @param
|
|
7295
|
-
* @param options Optional configuration.
|
|
7296
|
-
* @returns A compiled function.
|
|
7139
|
+
* @param aliasPath The alias path (e.g., "editingNestedStep")
|
|
7140
|
+
* @param sourcePath The source path (e.g., "routes[0].steps[0]")
|
|
7297
7141
|
*/
|
|
7298
|
-
static
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7302
|
-
const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
|
|
7303
|
-
const params = [
|
|
7304
|
-
...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7305
|
-
...filteredIdentifiers
|
|
7306
|
-
];
|
|
7307
|
-
// Add additional context parameters if provided
|
|
7308
|
-
if (options?.additionalContext) {
|
|
7309
|
-
params.push(...Object.keys(options.additionalContext));
|
|
7310
|
-
}
|
|
7311
|
-
const args = params.join(", ");
|
|
7312
|
-
const funcBody = `return (${expression});`;
|
|
7313
|
-
try {
|
|
7314
|
-
// Create the function with error handling
|
|
7315
|
-
return new Function(args, funcBody);
|
|
7316
|
-
}
|
|
7317
|
-
catch (error) {
|
|
7318
|
-
throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
|
|
7142
|
+
static registerPathAlias(aliasPath, sourcePath) {
|
|
7143
|
+
if (aliasPath && sourcePath && aliasPath !== sourcePath) {
|
|
7144
|
+
this.pathAliases.set(aliasPath, sourcePath);
|
|
7319
7145
|
}
|
|
7320
7146
|
}
|
|
7321
7147
|
/**
|
|
7322
|
-
*
|
|
7148
|
+
* Unregisters a path alias.
|
|
7323
7149
|
*
|
|
7324
|
-
* @
|
|
7150
|
+
* @param aliasPath The alias path to remove
|
|
7325
7151
|
*/
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
|
|
7332
|
-
const values = [
|
|
7333
|
-
// Global whitelist values
|
|
7334
|
-
...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7335
|
-
// Identifier values from bindings (excluding globals)
|
|
7336
|
-
...filteredIdentifiers.map(id => this.bindings.get(id))
|
|
7337
|
-
];
|
|
7338
|
-
// Add additional context values if provided
|
|
7339
|
-
if (this.additionalContext) {
|
|
7340
|
-
values.push(...Object.values(this.additionalContext));
|
|
7152
|
+
static unregisterPathAlias(aliasPath) {
|
|
7153
|
+
// Remove all aliases that start with the given path
|
|
7154
|
+
for (const key of this.pathAliases.keys()) {
|
|
7155
|
+
if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
|
|
7156
|
+
this.pathAliases.delete(key);
|
|
7341
7157
|
}
|
|
7342
|
-
// Call the compiled function with the gathered values
|
|
7343
|
-
return this.evaluatorFunc(...values);
|
|
7344
|
-
}
|
|
7345
|
-
catch (error) {
|
|
7346
|
-
console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
|
|
7347
|
-
throw error;
|
|
7348
7158
|
}
|
|
7349
7159
|
}
|
|
7350
7160
|
/**
|
|
7351
|
-
*
|
|
7161
|
+
* Resolves an alias path to its source path.
|
|
7162
|
+
* Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
|
|
7352
7163
|
*
|
|
7353
|
-
* @
|
|
7164
|
+
* @param aliasPath The alias path to resolve
|
|
7165
|
+
* @returns The source path, or undefined if no alias exists
|
|
7354
7166
|
*/
|
|
7355
|
-
|
|
7356
|
-
|
|
7167
|
+
static resolvePathAlias(aliasPath) {
|
|
7168
|
+
// Direct match
|
|
7169
|
+
if (this.pathAliases.has(aliasPath)) {
|
|
7170
|
+
return this.pathAliases.get(aliasPath);
|
|
7171
|
+
}
|
|
7172
|
+
// Check if this is a nested path of an aliased variable
|
|
7173
|
+
// e.g., "editingNestedStep.steps" when "editingNestedStep" is aliased
|
|
7174
|
+
for (const [alias, source] of this.pathAliases.entries()) {
|
|
7175
|
+
if (aliasPath.startsWith(alias + '.') || aliasPath.startsWith(alias + '[')) {
|
|
7176
|
+
const suffix = aliasPath.substring(alias.length);
|
|
7177
|
+
return source + suffix;
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
return undefined;
|
|
7357
7181
|
}
|
|
7358
7182
|
/**
|
|
7359
|
-
*
|
|
7183
|
+
* Checks if a change path matches an identifier, considering path aliases.
|
|
7184
|
+
* For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
|
|
7185
|
+
* then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
|
|
7186
|
+
*
|
|
7187
|
+
* @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
|
|
7188
|
+
* @param identifier The identifier to check (e.g., "editingNestedStep.steps")
|
|
7189
|
+
* @returns True if the change path matches the identifier
|
|
7360
7190
|
*/
|
|
7361
|
-
|
|
7362
|
-
//
|
|
7363
|
-
|
|
7191
|
+
static doesChangeMatchIdentifier(changePath, identifier) {
|
|
7192
|
+
// Direct match
|
|
7193
|
+
if (this.pathMatches(changePath, identifier)) {
|
|
7194
|
+
return true;
|
|
7195
|
+
}
|
|
7196
|
+
// Check if the change path is the resolved identifier or lies below it
|
|
7197
|
+
// (handles cases like array item changes)
|
|
7198
|
+
const resolvedIdentifier = this.resolvePathAlias(identifier);
|
|
7199
|
+
if (resolvedIdentifier && this.pathMatches(changePath, resolvedIdentifier, true)) {
|
|
7200
|
+
return true;
|
|
7201
|
+
}
|
|
7202
|
+
return false;
|
|
7364
7203
|
}
|
|
7365
7204
|
/**
|
|
7366
|
-
*
|
|
7367
|
-
*
|
|
7205
|
+
* Checks whether a change path names the location an identifier refers to.
|
|
7206
|
+
* A "[*]" index in the identifier (e.g. "items[*].name" for `items[i].name`)
|
|
7207
|
+
* stands for an index known only when the expression runs and matches any
|
|
7208
|
+
* one index or key.
|
|
7209
|
+
*
|
|
7210
|
+
* @param changePath The path that was changed (e.g., "items[2].name")
|
|
7211
|
+
* @param identifier The identifier to check (e.g., "items[*].name")
|
|
7212
|
+
* @param descendants Whether a change below the identifier also matches
|
|
7213
|
+
* @returns True if the change path matches the identifier
|
|
7368
7214
|
*/
|
|
7369
|
-
static
|
|
7370
|
-
|
|
7215
|
+
static pathMatches(changePath, identifier, descendants = false) {
|
|
7216
|
+
if (changePath === identifier) {
|
|
7217
|
+
return true;
|
|
7218
|
+
}
|
|
7219
|
+
if (!identifier.includes('[*]')) {
|
|
7220
|
+
return descendants && (changePath.startsWith(identifier + '.') || changePath.startsWith(identifier + '['));
|
|
7221
|
+
}
|
|
7222
|
+
const cacheKey = descendants ? identifier + ' ' : identifier;
|
|
7223
|
+
let pattern = this.pathPatterns.get(cacheKey);
|
|
7224
|
+
if (!pattern) {
|
|
7225
|
+
const source = identifier
|
|
7226
|
+
.split('[*]')
|
|
7227
|
+
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
7228
|
+
.join('(?:\\[[^\\]]*\\]|\\.[^.[]+)');
|
|
7229
|
+
pattern = new RegExp(`^${source}${descendants ? '(?:[.[].*)?' : ''}$`);
|
|
7230
|
+
this.pathPatterns.set(cacheKey, pattern);
|
|
7231
|
+
}
|
|
7232
|
+
return pattern.test(changePath);
|
|
7371
7233
|
}
|
|
7372
7234
|
}
|
|
7373
7235
|
|
|
7374
7236
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7375
7237
|
/**
|
|
7376
|
-
*
|
|
7377
|
-
* The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
|
|
7378
|
-
* The syntax for using the `v-bind` directive is `v-bind:attribute="expression"`, where `attribute` is the name of the HTML attribute you want to bind (e.g., `src`, `href`, `class`, etc.), and `expression` is a JavaScript expression that evaluates to the value you want to assign to that attribute.
|
|
7379
|
-
* Example usage:
|
|
7380
|
-
* <img v-bind:src="imageSrc" />
|
|
7381
|
-
* The value of `imageSrc` should be defined in the component's data object.
|
|
7382
|
-
* When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
|
|
7383
|
-
* This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
|
|
7384
|
-
* It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
|
|
7385
|
-
* The `v-bind` directive can also be used with shorthand syntax using a colon (:).
|
|
7386
|
-
* For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
|
|
7387
|
-
* This shorthand syntax is often used for brevity and improved readability in templates.
|
|
7388
|
-
* Overall, the `v-bind` directive is a powerful tool for creating dynamic and responsive web applications by allowing seamless integration between the data model and the DOM.
|
|
7238
|
+
* Utility class for analyzing JavaScript expressions to extract variable and function dependencies.
|
|
7389
7239
|
*/
|
|
7390
|
-
class
|
|
7391
|
-
/**
|
|
7392
|
-
* The virtual node to which this directive is applied.
|
|
7393
|
-
*/
|
|
7394
|
-
#vNode;
|
|
7395
|
-
/**
|
|
7396
|
-
* The expression evaluator for this directive.
|
|
7397
|
-
*/
|
|
7398
|
-
#evaluator;
|
|
7399
|
-
/**
|
|
7400
|
-
* The name of the attribute to bind (e.g., "src", "class", "style").
|
|
7401
|
-
*/
|
|
7402
|
-
#attributeName;
|
|
7403
|
-
/**
|
|
7404
|
-
* The original expression string from the directive.
|
|
7405
|
-
*/
|
|
7406
|
-
#expression;
|
|
7407
|
-
/**
|
|
7408
|
-
* The set of class names managed by this directive (used when binding to the "class" attribute).
|
|
7409
|
-
* This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
|
|
7410
|
-
*/
|
|
7411
|
-
#managedClasses = new Set();
|
|
7412
|
-
/**
|
|
7413
|
-
* The set of style properties managed by this directive (used when binding to the "style" attribute).
|
|
7414
|
-
* This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
|
|
7415
|
-
*/
|
|
7416
|
-
#managedStyles = new Set();
|
|
7240
|
+
class ExpressionUtils {
|
|
7417
7241
|
/**
|
|
7418
|
-
*
|
|
7242
|
+
* Extracts variable and function names used in the expression.
|
|
7243
|
+
* @param expression The expression string to analyze.
|
|
7244
|
+
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
7245
|
+
* @param options Optional parsing options.
|
|
7246
|
+
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
7247
|
+
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
7248
|
+
* for backward compatibility with interpolation and binding directives).
|
|
7249
|
+
* @returns An array of identifier names.
|
|
7419
7250
|
*/
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
//
|
|
7423
|
-
// e.g
|
|
7424
|
-
const
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
7438
|
-
}
|
|
7439
|
-
/**
|
|
7440
|
-
* @inheritdoc
|
|
7441
|
-
*/
|
|
7442
|
-
get name() {
|
|
7443
|
-
return StandardDirectiveName.V_BIND;
|
|
7444
|
-
}
|
|
7445
|
-
/**
|
|
7446
|
-
* @inheritdoc
|
|
7447
|
-
*/
|
|
7448
|
-
get vNode() {
|
|
7449
|
-
return this.#vNode;
|
|
7450
|
-
}
|
|
7451
|
-
/**
|
|
7452
|
-
* @inheritdoc
|
|
7453
|
-
*/
|
|
7454
|
-
get needsAnchor() {
|
|
7455
|
-
return false;
|
|
7456
|
-
}
|
|
7457
|
-
/**
|
|
7458
|
-
* @inheritdoc
|
|
7459
|
-
*/
|
|
7460
|
-
get bindingsPreparer() {
|
|
7461
|
-
return undefined;
|
|
7462
|
-
}
|
|
7463
|
-
/**
|
|
7464
|
-
* @inheritdoc
|
|
7465
|
-
*/
|
|
7466
|
-
get domUpdater() {
|
|
7467
|
-
const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
|
|
7468
|
-
// Create an updater that handles the attribute binding
|
|
7469
|
-
const updater = {
|
|
7470
|
-
get dependentIdentifiers() {
|
|
7471
|
-
return identifiers;
|
|
7472
|
-
},
|
|
7473
|
-
applyToDOM: () => {
|
|
7474
|
-
this.#render();
|
|
7251
|
+
static extractIdentifiers(expression, functionDependencies, options) {
|
|
7252
|
+
const identifiers = new Set();
|
|
7253
|
+
// In expression mode we wrap in parens so acorn parses the source as a single expression.
|
|
7254
|
+
// In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
|
|
7255
|
+
const source = options?.asScript ? expression : `(${expression})`;
|
|
7256
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
7257
|
+
// Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
|
|
7258
|
+
full(ast, (node) => {
|
|
7259
|
+
if (node.type === 'Identifier') {
|
|
7260
|
+
identifiers.add(node.name);
|
|
7261
|
+
// Check if the identifier is a function name
|
|
7262
|
+
if (functionDependencies[node.name]) {
|
|
7263
|
+
// If it is, add its dependencies to the list of identifiers
|
|
7264
|
+
for (const dependency of functionDependencies[node.name]) {
|
|
7265
|
+
identifiers.add(dependency);
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7475
7268
|
}
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
* @inheritdoc
|
|
7547
|
-
*/
|
|
7548
|
-
get onUpdate() {
|
|
7549
|
-
return undefined;
|
|
7550
|
-
}
|
|
7551
|
-
/**
|
|
7552
|
-
* @inheritdoc
|
|
7553
|
-
*/
|
|
7554
|
-
get onUpdated() {
|
|
7555
|
-
return undefined;
|
|
7556
|
-
}
|
|
7557
|
-
/**
|
|
7558
|
-
* @inheritdoc
|
|
7559
|
-
*/
|
|
7560
|
-
get onUnmount() {
|
|
7561
|
-
return undefined;
|
|
7562
|
-
}
|
|
7563
|
-
/**
|
|
7564
|
-
* @inheritdoc
|
|
7565
|
-
*/
|
|
7566
|
-
get onUnmounted() {
|
|
7567
|
-
return undefined;
|
|
7269
|
+
else if (node.type === 'MemberExpression') {
|
|
7270
|
+
// Reconstruct chain parts from a MemberExpression (e.g. a.b.c). Each part
|
|
7271
|
+
// carries its own separator so that the chain is spelled the way the reactive
|
|
7272
|
+
// proxy reports changes: array indices in brackets, properties after a dot
|
|
7273
|
+
// (e.g. "items[0].name").
|
|
7274
|
+
const parts = [];
|
|
7275
|
+
let cur = node;
|
|
7276
|
+
while (cur) {
|
|
7277
|
+
// property part (right side)
|
|
7278
|
+
if (cur.property) {
|
|
7279
|
+
if (!cur.computed && cur.property.type === 'Identifier') {
|
|
7280
|
+
parts.unshift(`.${cur.property.name}`);
|
|
7281
|
+
}
|
|
7282
|
+
else if (cur.computed && cur.property.type === 'Literal') {
|
|
7283
|
+
// items[0] and items["0"] both name an index; labels['a-b'] names a key.
|
|
7284
|
+
const key = String(cur.property.value);
|
|
7285
|
+
parts.unshift(ReactiveProxy.isIndexKey(key) ? `[${key}]` : `.${key}`);
|
|
7286
|
+
}
|
|
7287
|
+
else if (cur.computed) {
|
|
7288
|
+
// e.g. items[i] or items[i + 1]: the index is only known when the
|
|
7289
|
+
// expression runs, so "[*]" stands for any index or key. Identifiers
|
|
7290
|
+
// inside the index expression are visited separately by walk.full.
|
|
7291
|
+
parts.unshift('[*]');
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
7294
|
+
// object part (left side)
|
|
7295
|
+
if (cur.object) {
|
|
7296
|
+
if (cur.object.type === 'Identifier') {
|
|
7297
|
+
parts.unshift(cur.object.name);
|
|
7298
|
+
break;
|
|
7299
|
+
}
|
|
7300
|
+
else if (cur.object.type === 'ThisExpression') {
|
|
7301
|
+
// For `this.x` or `this.a.b` we don't want to include 'this' itself.
|
|
7302
|
+
// Stop unwrapping here; we'll drop a leading 'this' later.
|
|
7303
|
+
break;
|
|
7304
|
+
}
|
|
7305
|
+
else if (cur.object.type === 'MemberExpression') {
|
|
7306
|
+
// continue unwrapping
|
|
7307
|
+
cur = cur.object;
|
|
7308
|
+
}
|
|
7309
|
+
else {
|
|
7310
|
+
// other object types (CallExpression, etc.) — stop here
|
|
7311
|
+
break;
|
|
7312
|
+
}
|
|
7313
|
+
}
|
|
7314
|
+
else {
|
|
7315
|
+
break;
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
// A chain that does not start at a name (e.g. `this.a.b` or `f().b`) begins
|
|
7319
|
+
// with its first property; one that begins with an index names no binding.
|
|
7320
|
+
if (parts.length > 0 && parts[0].startsWith('.')) {
|
|
7321
|
+
parts[0] = parts[0].substring(1);
|
|
7322
|
+
}
|
|
7323
|
+
if (parts.length > 0 && !parts[0].startsWith('[')) {
|
|
7324
|
+
// Add progressive chains: 'a', 'a.b', 'a.b[0]', 'a.b[0].c'
|
|
7325
|
+
for (let i = 0; i < parts.length; i++) {
|
|
7326
|
+
const chain = parts.slice(0, i + 1).join('');
|
|
7327
|
+
identifiers.add(chain);
|
|
7328
|
+
// Also apply functionDependencies lookup for the chain key
|
|
7329
|
+
if (functionDependencies[chain]) {
|
|
7330
|
+
for (const dependency of functionDependencies[chain]) {
|
|
7331
|
+
identifiers.add(dependency);
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
});
|
|
7338
|
+
return Array.from(identifiers);
|
|
7568
7339
|
}
|
|
7569
7340
|
/**
|
|
7570
|
-
*
|
|
7341
|
+
* Returns the binding name an identifier chain starts with
|
|
7342
|
+
* (e.g. "items" for "items[0].name" or "user.name").
|
|
7343
|
+
* @param identifier An identifier or member chain returned by extractIdentifiers.
|
|
7344
|
+
* @returns The left-most name of the chain.
|
|
7571
7345
|
*/
|
|
7572
|
-
|
|
7573
|
-
|
|
7346
|
+
static baseIdentifier(identifier) {
|
|
7347
|
+
const end = identifier.search(/[.[]/);
|
|
7348
|
+
return end === -1 ? identifier : identifier.substring(0, end);
|
|
7574
7349
|
}
|
|
7575
7350
|
/**
|
|
7576
|
-
*
|
|
7351
|
+
* Analyzes the dependencies of functions in the provided dictionary.
|
|
7352
|
+
* @param functions A dictionary mapping function names to their implementations.
|
|
7353
|
+
* @returns A dictionary mapping function names to arrays of their dependencies.
|
|
7577
7354
|
*/
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7355
|
+
static analyzeFunctionDependencies(functions) {
|
|
7356
|
+
const functionDependencies = {};
|
|
7357
|
+
// First pass: collect direct dependencies for each function
|
|
7358
|
+
for (const [funcName, func] of Object.entries(functions)) {
|
|
7359
|
+
try {
|
|
7360
|
+
let source = func.toString();
|
|
7361
|
+
// Handle different function formats
|
|
7362
|
+
// Arrow function: () => expr or (args) => { ... }
|
|
7363
|
+
// Method shorthand: methodName() { ... }
|
|
7364
|
+
// Function expression: function() { ... }
|
|
7365
|
+
// Detect function type more accurately
|
|
7366
|
+
const isArrowFunction = /^\s*(\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(source);
|
|
7367
|
+
const isFunctionExpression = source.startsWith('function');
|
|
7368
|
+
const isAsyncFunction = source.startsWith('async');
|
|
7369
|
+
// If it's a method shorthand (e.g., "methodName() { ... }" or "async methodName() { ... }"), convert to function expression
|
|
7370
|
+
if (!isFunctionExpression && !isArrowFunction) {
|
|
7371
|
+
// It's likely a method shorthand, convert to function expression
|
|
7372
|
+
if (isAsyncFunction) {
|
|
7373
|
+
// Remove 'async' prefix and add 'async function' prefix
|
|
7374
|
+
source = `async function ${source.substring(5).trim()}`;
|
|
7375
|
+
}
|
|
7376
|
+
else {
|
|
7377
|
+
source = `function ${source}`;
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
// Wrap in parentheses for parsing
|
|
7381
|
+
source = `(${source})`;
|
|
7382
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
7383
|
+
const dependencies = new Set();
|
|
7384
|
+
const declaredVariables = new Set();
|
|
7385
|
+
const thisAliases = new Set();
|
|
7386
|
+
// First, collect all declared variables (const, let, var, function params, etc.)
|
|
7387
|
+
ancestor(ast, {
|
|
7388
|
+
VariableDeclarator(node) {
|
|
7389
|
+
if (node.id.type === 'Identifier') {
|
|
7390
|
+
declaredVariables.add(node.id.name);
|
|
7391
|
+
// Detect aliases assigned from `this`, e.g. `let vm = this;`
|
|
7392
|
+
if (node.init && node.init.type === 'ThisExpression') {
|
|
7393
|
+
thisAliases.add(node.id.name);
|
|
7394
|
+
}
|
|
7395
|
+
}
|
|
7396
|
+
},
|
|
7397
|
+
FunctionDeclaration(node) {
|
|
7398
|
+
if (node.id && node.id.type === 'Identifier') {
|
|
7399
|
+
declaredVariables.add(node.id.name);
|
|
7400
|
+
}
|
|
7401
|
+
// Add function parameters
|
|
7402
|
+
node.params.forEach((param) => {
|
|
7403
|
+
if (param.type === 'Identifier') {
|
|
7404
|
+
declaredVariables.add(param.name);
|
|
7405
|
+
}
|
|
7406
|
+
});
|
|
7407
|
+
},
|
|
7408
|
+
FunctionExpression(node, ancestors) {
|
|
7409
|
+
// Only process the outermost function (skip nested functions)
|
|
7410
|
+
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
7411
|
+
n.type === 'FunctionDeclaration' ||
|
|
7412
|
+
n.type === 'ArrowFunctionExpression');
|
|
7413
|
+
if (functionAncestors.length === 1) {
|
|
7414
|
+
// Add function parameters of the main function
|
|
7415
|
+
node.params.forEach((param) => {
|
|
7416
|
+
if (param.type === 'Identifier') {
|
|
7417
|
+
declaredVariables.add(param.name);
|
|
7418
|
+
}
|
|
7419
|
+
});
|
|
7420
|
+
}
|
|
7421
|
+
},
|
|
7422
|
+
ArrowFunctionExpression(node, ancestors) {
|
|
7423
|
+
// Only process the outermost function (skip nested functions)
|
|
7424
|
+
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
7425
|
+
n.type === 'FunctionDeclaration' ||
|
|
7426
|
+
n.type === 'ArrowFunctionExpression');
|
|
7427
|
+
if (functionAncestors.length === 1) {
|
|
7428
|
+
// Add function parameters of the main function
|
|
7429
|
+
node.params.forEach((param) => {
|
|
7430
|
+
if (param.type === 'Identifier') {
|
|
7431
|
+
declaredVariables.add(param.name);
|
|
7432
|
+
}
|
|
7433
|
+
});
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
});
|
|
7437
|
+
// Then, collect identifiers that are not declared within the function
|
|
7438
|
+
simple(ast, {
|
|
7439
|
+
Identifier(node) {
|
|
7440
|
+
// Skip the function name itself, declared variables, and common globals
|
|
7441
|
+
if (node.name !== funcName && !declaredVariables.has(node.name)) {
|
|
7442
|
+
dependencies.add(node.name);
|
|
7443
|
+
}
|
|
7444
|
+
},
|
|
7445
|
+
MemberExpression(node) {
|
|
7446
|
+
// Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
|
|
7447
|
+
if (node.property.type === 'Identifier') {
|
|
7448
|
+
if (node.object.type === 'ThisExpression') {
|
|
7449
|
+
dependencies.add(node.property.name);
|
|
7450
|
+
}
|
|
7451
|
+
else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
|
|
7452
|
+
dependencies.add(node.property.name);
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
});
|
|
7457
|
+
functionDependencies[funcName] = Array.from(dependencies);
|
|
7458
|
+
}
|
|
7459
|
+
catch (error) {
|
|
7460
|
+
console.warn(`Failed to analyze function '${funcName}':`, error);
|
|
7461
|
+
// If parsing fails, assume no dependencies
|
|
7462
|
+
functionDependencies[funcName] = [];
|
|
7463
|
+
}
|
|
7597
7464
|
}
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7465
|
+
// Second pass: resolve each function's transitive reactive dependencies.
|
|
7466
|
+
//
|
|
7467
|
+
// A function's direct dependency list mixes two kinds of identifiers: references to other
|
|
7468
|
+
// functions (call edges) and references to reactive data properties (the values we actually
|
|
7469
|
+
// care about). We want, for every function, the full set of reactive properties reachable by
|
|
7470
|
+
// following call edges to any depth.
|
|
7471
|
+
//
|
|
7472
|
+
// Functions are free to reference each other cyclically — a poller that reschedules itself
|
|
7473
|
+
// (`scheduleOperationPoll` -> `pollOperation` -> `scheduleOperationPoll`), a pair of mutually
|
|
7474
|
+
// recursive helpers, or any state machine. This is valid JavaScript, so the resolver must not
|
|
7475
|
+
// treat it as an error. Every function in a cycle (more precisely, a strongly connected
|
|
7476
|
+
// component) is reachable from the others, so they all share the same transitive dependency
|
|
7477
|
+
// set: the union of every member's direct reactive dependencies.
|
|
7478
|
+
//
|
|
7479
|
+
// A depth-first walk that bails out on back-edges cannot compute that correctly — it drops the
|
|
7480
|
+
// dependencies contributed around the cycle and caches incomplete, traversal-order-dependent
|
|
7481
|
+
// results, which would silently break reactivity for any computed that reads reactive data
|
|
7482
|
+
// through a cyclic method. We instead split each function's direct dependencies into reactive
|
|
7483
|
+
// properties and call edges, then propagate reactive properties along call edges until the sets
|
|
7484
|
+
// stop growing (a fixpoint). This converges regardless of cycles and yields the exact transitive
|
|
7485
|
+
// closure for every function, including those participating in a cycle.
|
|
7486
|
+
const directReactive = {};
|
|
7487
|
+
const callEdges = {};
|
|
7488
|
+
for (const funcName of Object.keys(functions)) {
|
|
7489
|
+
const reactive = new Set();
|
|
7490
|
+
const edges = [];
|
|
7491
|
+
for (const dep of functionDependencies[funcName] || []) {
|
|
7492
|
+
if (dep === funcName)
|
|
7493
|
+
continue; // Skip self-references.
|
|
7494
|
+
if (functions[dep]) {
|
|
7495
|
+
edges.push(dep); // Call edge to another function.
|
|
7496
|
+
}
|
|
7497
|
+
else {
|
|
7498
|
+
reactive.add(dep); // Reactive data property (terminal dependency).
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
directReactive[funcName] = reactive;
|
|
7502
|
+
callEdges[funcName] = edges;
|
|
7605
7503
|
}
|
|
7606
|
-
|
|
7607
|
-
|
|
7504
|
+
// Seed each function with its own direct reactive dependencies, then propagate along call edges
|
|
7505
|
+
// until no set changes. Each pass can only add properties, and the universe of properties is
|
|
7506
|
+
// finite, so the loop is guaranteed to terminate.
|
|
7507
|
+
const resolved = {};
|
|
7508
|
+
for (const funcName of Object.keys(functions)) {
|
|
7509
|
+
resolved[funcName] = new Set(directReactive[funcName]);
|
|
7608
7510
|
}
|
|
7609
|
-
|
|
7610
|
-
|
|
7511
|
+
let changed = true;
|
|
7512
|
+
while (changed) {
|
|
7513
|
+
changed = false;
|
|
7514
|
+
for (const funcName of Object.keys(functions)) {
|
|
7515
|
+
const target = resolved[funcName];
|
|
7516
|
+
for (const edge of callEdges[funcName]) {
|
|
7517
|
+
for (const dep of resolved[edge]) {
|
|
7518
|
+
if (!target.has(dep)) {
|
|
7519
|
+
target.add(dep);
|
|
7520
|
+
changed = true;
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7611
7525
|
}
|
|
7612
|
-
|
|
7613
|
-
|
|
7526
|
+
const resolvedDependencies = {};
|
|
7527
|
+
for (const funcName of Object.keys(functions)) {
|
|
7528
|
+
resolvedDependencies[funcName] = Array.from(resolved[funcName]);
|
|
7614
7529
|
}
|
|
7530
|
+
return resolvedDependencies;
|
|
7615
7531
|
}
|
|
7616
7532
|
/**
|
|
7617
|
-
*
|
|
7533
|
+
* Rewrites an expression to replace identifiers with 'this.identifier'.
|
|
7534
|
+
* This allows direct property access and assignment without using 'with' statement.
|
|
7535
|
+
* Uses AST parsing to accurately identify which identifiers to replace.
|
|
7536
|
+
* @param expression The original expression string.
|
|
7537
|
+
* @param identifiers The list of identifiers that are available in bindings.
|
|
7538
|
+
* @returns The rewritten expression.
|
|
7618
7539
|
*/
|
|
7619
|
-
|
|
7620
|
-
//
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7540
|
+
static rewriteExpression(expression, identifiers, options) {
|
|
7541
|
+
// Reserved words and built-in objects that should not be prefixed with 'this.'
|
|
7542
|
+
const reserved = new Set([
|
|
7543
|
+
'event', '$event', '$ctx', '$newValue',
|
|
7544
|
+
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
7545
|
+
'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
|
|
7546
|
+
'JSON', 'console', 'window', 'document', 'navigator',
|
|
7547
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
7548
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent'
|
|
7549
|
+
]);
|
|
7550
|
+
// Extract ALL identifiers from the expression (including assignment left-hand side)
|
|
7551
|
+
// This is necessary because the passed 'identifiers' parameter only includes
|
|
7552
|
+
// identifiers that are used (right-hand side), not assigned to (left-hand side)
|
|
7553
|
+
let allIdentifiersInExpression;
|
|
7554
|
+
try {
|
|
7555
|
+
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
|
|
7624
7556
|
}
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
.filter(Boolean)
|
|
7629
|
-
.flatMap(cls => {
|
|
7630
|
-
if (typeof cls === 'string') {
|
|
7631
|
-
return cls.split(/\s+/).filter(Boolean);
|
|
7632
|
-
}
|
|
7633
|
-
else if (typeof cls === 'object' && cls !== null) {
|
|
7634
|
-
// Handle object format within array: { className: condition }
|
|
7635
|
-
// Keys may contain space-separated class names
|
|
7636
|
-
return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
7637
|
-
}
|
|
7638
|
-
return [];
|
|
7639
|
-
});
|
|
7557
|
+
catch (error) {
|
|
7558
|
+
console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
|
|
7559
|
+
return expression;
|
|
7640
7560
|
}
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7561
|
+
// Create a Set of identifiers available in bindings (from data, computed, methods)
|
|
7562
|
+
// We need to know which identifiers are valid binding properties
|
|
7563
|
+
const bindingIdentifiers = new Set(identifiers.filter(id => !reserved.has(id)));
|
|
7564
|
+
// For assignment expressions, we also need to include the left-hand side identifier
|
|
7565
|
+
// even if it's not in the tracking identifiers (because it's being assigned, not read)
|
|
7566
|
+
for (const id of allIdentifiersInExpression) {
|
|
7567
|
+
if (!reserved.has(id)) {
|
|
7568
|
+
bindingIdentifiers.add(id);
|
|
7569
|
+
}
|
|
7644
7570
|
}
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
// Add newly managed classes
|
|
7648
|
-
newClasses.forEach(cls => element.classList.add(cls));
|
|
7649
|
-
// Update managed classes list
|
|
7650
|
-
this.#managedClasses = new Set(newClasses);
|
|
7651
|
-
}
|
|
7652
|
-
/**
|
|
7653
|
-
* Updates the style attribute with support for object format.
|
|
7654
|
-
*/
|
|
7655
|
-
#updateStyle(element, value) {
|
|
7656
|
-
let newStyles = [];
|
|
7657
|
-
if (typeof value === 'string') {
|
|
7658
|
-
// Directly set the style string
|
|
7659
|
-
element.style.cssText = value;
|
|
7660
|
-
// Extract managed properties
|
|
7661
|
-
newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
|
|
7571
|
+
if (bindingIdentifiers.size === 0) {
|
|
7572
|
+
return expression;
|
|
7662
7573
|
}
|
|
7663
|
-
|
|
7664
|
-
//
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7574
|
+
try {
|
|
7575
|
+
// Build a map of positions to replace: { start: number, end: number, name: string, asThisAlias?: boolean }[]
|
|
7576
|
+
const replacements = [];
|
|
7577
|
+
// In script mode we must not wrap in parens (that would make multi-statement input invalid).
|
|
7578
|
+
// Offsets from the parser therefore refer directly to the original expression, so no shift.
|
|
7579
|
+
const asScript = options?.asScript === true;
|
|
7580
|
+
const source = asScript ? expression : `(${expression})`;
|
|
7581
|
+
const offsetShift = asScript ? 0 : 1;
|
|
7582
|
+
const parsedAst = parse(source, { ecmaVersion: 'latest' });
|
|
7583
|
+
// Track identifiers that are locally declared within the handler body (let/const/var, function
|
|
7584
|
+
// params) so we don't rewrite them to `this.xxx`. Additionally detect `this` aliases such as
|
|
7585
|
+
// `let vm = this;` so that `vm.x` can be rewritten to `this.x` even though `vm` is locally declared.
|
|
7586
|
+
const locallyDeclared = new Set();
|
|
7587
|
+
const thisAliases = new Set();
|
|
7588
|
+
if (asScript) {
|
|
7589
|
+
full(parsedAst, (node) => {
|
|
7590
|
+
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7591
|
+
locallyDeclared.add(node.id.name);
|
|
7592
|
+
if (node.init && node.init.type === 'ThisExpression') {
|
|
7593
|
+
thisAliases.add(node.id.name);
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
7597
|
+
locallyDeclared.add(node.id.name);
|
|
7598
|
+
}
|
|
7599
|
+
});
|
|
7600
|
+
}
|
|
7601
|
+
// Collect all identifier nodes that should be replaced
|
|
7602
|
+
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7603
|
+
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
7604
|
+
if (node.type !== 'Identifier') {
|
|
7605
|
+
return;
|
|
7606
|
+
}
|
|
7607
|
+
// Skip if not in our identifier set
|
|
7608
|
+
if (!bindingIdentifiers.has(node.name)) {
|
|
7609
|
+
return;
|
|
7610
|
+
}
|
|
7611
|
+
// Locally declared identifiers must not be rewritten to `this.xxx`. The one
|
|
7612
|
+
// exception is a `this`-alias (e.g. `let vm = this;`): when such an alias is
|
|
7613
|
+
// used as the OBJECT of a MemberExpression (`vm.x`), we replace just the
|
|
7614
|
+
// object with `this` so that `vm.x` becomes `this.x`. In every other position
|
|
7615
|
+
// (the declaration LHS, a bare reference such as `console.log(vm)`, an
|
|
7616
|
+
// argument, etc.) the alias must be left untouched — rewriting it to
|
|
7617
|
+
// `this.vm` would either produce a syntax error (`let this.vm = this;`) or
|
|
7618
|
+
// change the meaning at runtime.
|
|
7619
|
+
// walk.fullAncestor includes the visited node itself as the last entry of
|
|
7620
|
+
// `ancestors`, so the actual parent node is at length - 2.
|
|
7621
|
+
const parentNode = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
|
|
7622
|
+
const isAliasAsMemberObject = parentNode?.type === 'MemberExpression' && parentNode.object === node;
|
|
7623
|
+
if (locallyDeclared.has(node.name)) {
|
|
7624
|
+
if (!thisAliases.has(node.name) || !isAliasAsMemberObject) {
|
|
7625
|
+
return;
|
|
7676
7626
|
}
|
|
7627
|
+
replacements.push({
|
|
7628
|
+
start: node.start - offsetShift,
|
|
7629
|
+
end: node.end - offsetShift,
|
|
7630
|
+
name: node.name,
|
|
7631
|
+
asThisAlias: true
|
|
7632
|
+
});
|
|
7633
|
+
return;
|
|
7634
|
+
}
|
|
7635
|
+
// Skip identifiers that are the property (not the object) of a non-computed
|
|
7636
|
+
// member access (e.g., the `prop` in `obj.prop`).
|
|
7637
|
+
if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
|
|
7638
|
+
return;
|
|
7677
7639
|
}
|
|
7640
|
+
// Add to replacements list (adjust for the wrapping parentheses in expression mode)
|
|
7641
|
+
replacements.push({
|
|
7642
|
+
start: node.start - offsetShift,
|
|
7643
|
+
end: node.end - offsetShift,
|
|
7644
|
+
name: node.name
|
|
7645
|
+
});
|
|
7646
|
+
});
|
|
7647
|
+
// Sort replacements by start position (descending) to replace from end to start
|
|
7648
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
7649
|
+
// Apply replacements
|
|
7650
|
+
let result = expression;
|
|
7651
|
+
for (const replacement of replacements) {
|
|
7652
|
+
const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
|
|
7653
|
+
result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
|
|
7678
7654
|
}
|
|
7655
|
+
return result;
|
|
7656
|
+
}
|
|
7657
|
+
catch (error) {
|
|
7658
|
+
// If AST parsing fails, fall back to the original expression
|
|
7659
|
+
console.warn('Failed to rewrite expression:', expression, error);
|
|
7660
|
+
return expression;
|
|
7679
7661
|
}
|
|
7680
|
-
// Update managed properties list
|
|
7681
|
-
this.#managedStyles = new Set(newStyles);
|
|
7682
7662
|
}
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7666
|
+
/**
|
|
7667
|
+
* A centralized expression evaluator that normalizes expression evaluation across all directives.
|
|
7668
|
+
*
|
|
7669
|
+
* This class provides:
|
|
7670
|
+
* - Consistent identifier extraction and binding resolution
|
|
7671
|
+
* - Unified global object whitelisting (Math, Date, JSON, etc.)
|
|
7672
|
+
* - Expression caching for performance
|
|
7673
|
+
* - Centralized error handling
|
|
7674
|
+
* - Support for custom expression rewriting (for VOnDirective)
|
|
7675
|
+
*
|
|
7676
|
+
* Example usage:
|
|
7677
|
+
* ```typescript
|
|
7678
|
+
* const evaluator = ExpressionEvaluator.create(
|
|
7679
|
+
* "count + 1",
|
|
7680
|
+
* bindings,
|
|
7681
|
+
* functionDependencies
|
|
7682
|
+
* );
|
|
7683
|
+
* const result = evaluator.evaluate();
|
|
7684
|
+
* ```
|
|
7685
|
+
*/
|
|
7686
|
+
class ExpressionEvaluator {
|
|
7687
|
+
/**
|
|
7688
|
+
* Cache for compiled expression functions.
|
|
7689
|
+
* Key: expression string
|
|
7690
|
+
* Value: compiled function
|
|
7691
|
+
*/
|
|
7692
|
+
static cache = new Map();
|
|
7693
|
+
/**
|
|
7694
|
+
* Global objects that are whitelisted for use in expressions.
|
|
7695
|
+
* These objects are available to all expressions for convenience and compatibility.
|
|
7696
|
+
*/
|
|
7697
|
+
static GLOBAL_WHITELIST = {
|
|
7698
|
+
Math,
|
|
7699
|
+
Date,
|
|
7700
|
+
JSON,
|
|
7701
|
+
Array,
|
|
7702
|
+
Object,
|
|
7703
|
+
String,
|
|
7704
|
+
Number,
|
|
7705
|
+
Boolean,
|
|
7706
|
+
parseInt,
|
|
7707
|
+
parseFloat,
|
|
7708
|
+
isNaN,
|
|
7709
|
+
isFinite,
|
|
7710
|
+
encodeURI,
|
|
7711
|
+
decodeURI,
|
|
7712
|
+
encodeURIComponent,
|
|
7713
|
+
decodeURIComponent
|
|
7714
|
+
};
|
|
7683
7715
|
/**
|
|
7684
|
-
*
|
|
7716
|
+
* The original expression string.
|
|
7685
7717
|
*/
|
|
7686
|
-
|
|
7687
|
-
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7688
|
-
}
|
|
7718
|
+
expression;
|
|
7689
7719
|
/**
|
|
7690
|
-
*
|
|
7691
|
-
* delivered as a property rather than an HTML attribute.
|
|
7692
|
-
*
|
|
7693
|
-
* Three conditions trigger property delivery:
|
|
7694
|
-
* 1. The value is an object or array — serialising these to a string attribute
|
|
7695
|
-
* would lose type information.
|
|
7696
|
-
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
7697
|
-
* defineComponent), even when the value is a primitive.
|
|
7698
|
-
* 3. The element declares a matching prop in its static _props (case-insensitive
|
|
7699
|
-
* and kebab-case → camelCase tolerant), so primitive values reach the
|
|
7700
|
-
* reactive binding via the generated setter instead of being lost as a
|
|
7701
|
-
* plain HTML attribute that no attributeChangedCallback observes.
|
|
7720
|
+
* The identifiers extracted from the expression.
|
|
7702
7721
|
*/
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
}
|
|
7707
|
-
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
7708
|
-
if (isObjectOrArray || name in element) {
|
|
7709
|
-
return true;
|
|
7710
|
-
}
|
|
7711
|
-
return this.#findDeclaredProp(element, name) !== undefined;
|
|
7712
|
-
}
|
|
7722
|
+
identifiers;
|
|
7723
|
+
// The original extracted identifiers (may include member chains like 'a.b')
|
|
7724
|
+
originalIdentifiers;
|
|
7713
7725
|
/**
|
|
7714
|
-
*
|
|
7715
|
-
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
7716
|
-
* but custom element props are typically camelCase. This method checks the
|
|
7717
|
-
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
7718
|
-
* match against both the raw attribute name and its kebab-case → camelCase
|
|
7719
|
-
* conversion (so `user-name` resolves to `userName`), falling back to the
|
|
7720
|
-
* original name when no match is found.
|
|
7726
|
+
* The bindings to use for evaluating the expression.
|
|
7721
7727
|
*/
|
|
7722
|
-
|
|
7723
|
-
// Fast path: exact match already exists
|
|
7724
|
-
if (name in element) {
|
|
7725
|
-
return name;
|
|
7726
|
-
}
|
|
7727
|
-
return this.#findDeclaredProp(element, name) ?? name;
|
|
7728
|
-
}
|
|
7728
|
+
bindings;
|
|
7729
7729
|
/**
|
|
7730
|
-
*
|
|
7731
|
-
* the given attribute name, treating the comparison as case-insensitive and
|
|
7732
|
-
* tolerant of kebab-case → camelCase conversion. Returns the canonical prop
|
|
7733
|
-
* name (as declared) when a match is found, or undefined otherwise.
|
|
7730
|
+
* The compiled function for evaluating the expression.
|
|
7734
7731
|
*/
|
|
7735
|
-
|
|
7736
|
-
const props = element.constructor._props;
|
|
7737
|
-
if (!Array.isArray(props)) {
|
|
7738
|
-
return undefined;
|
|
7739
|
-
}
|
|
7740
|
-
const lowerName = name.toLowerCase();
|
|
7741
|
-
const camelLower = this.#kebabToCamel(lowerName).toLowerCase();
|
|
7742
|
-
return props.find(p => {
|
|
7743
|
-
const lowerProp = p.toLowerCase();
|
|
7744
|
-
return lowerProp === lowerName || lowerProp === camelLower;
|
|
7745
|
-
});
|
|
7746
|
-
}
|
|
7732
|
+
evaluatorFunc;
|
|
7747
7733
|
/**
|
|
7748
|
-
*
|
|
7734
|
+
* Additional context variables (e.g., 'event', '$ctx').
|
|
7749
7735
|
*/
|
|
7750
|
-
|
|
7751
|
-
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
7752
|
-
}
|
|
7736
|
+
additionalContext;
|
|
7753
7737
|
/**
|
|
7754
|
-
*
|
|
7738
|
+
* Whether any identifier has an index known only at run time ("items[*].name").
|
|
7739
|
+
* Only then are the paths read during evaluation worth recording.
|
|
7755
7740
|
*/
|
|
7756
|
-
|
|
7757
|
-
const domProperties = ['value', 'checked', 'selected', 'innerHTML', 'textContent'];
|
|
7758
|
-
return domProperties.includes(name);
|
|
7759
|
-
}
|
|
7741
|
+
hasDynamicIndex;
|
|
7760
7742
|
/**
|
|
7761
|
-
*
|
|
7743
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
7744
|
+
* or undefined before the first evaluation and when nothing is recorded.
|
|
7762
7745
|
*/
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7746
|
+
trackedPathSet;
|
|
7747
|
+
/**
|
|
7748
|
+
* Private constructor. Use ExpressionEvaluator.create() to create instances.
|
|
7749
|
+
*/
|
|
7750
|
+
constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
|
|
7751
|
+
this.expression = expression;
|
|
7752
|
+
this.bindings = bindings;
|
|
7753
|
+
this.identifiers = identifiers;
|
|
7754
|
+
this.originalIdentifiers = originalIdentifiers;
|
|
7755
|
+
this.evaluatorFunc = evaluatorFunc;
|
|
7756
|
+
this.additionalContext = additionalContext;
|
|
7757
|
+
this.hasDynamicIndex = originalIdentifiers.some(id => id.includes('[*]'));
|
|
7770
7758
|
}
|
|
7771
7759
|
/**
|
|
7772
|
-
*
|
|
7773
|
-
*
|
|
7774
|
-
*
|
|
7760
|
+
* Creates an ExpressionEvaluator instance.
|
|
7761
|
+
*
|
|
7762
|
+
* @param expression The expression string to evaluate.
|
|
7763
|
+
* @param bindings The bindings to use for evaluating the expression.
|
|
7764
|
+
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
7765
|
+
* @param options Optional configuration for the evaluator.
|
|
7766
|
+
* @returns An ExpressionEvaluator instance.
|
|
7775
7767
|
*/
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7768
|
+
static create(expression, bindings, functionDependencies, options) {
|
|
7769
|
+
// Extract identifiers from the expression (may include member chains like 'a.b')
|
|
7770
|
+
const extractedIdentifiers = ExpressionUtils.extractIdentifiers(expression, functionDependencies);
|
|
7771
|
+
// For compilation we must only pass simple identifier names (no dots).
|
|
7772
|
+
// Use the base (left-most) segment of member chains and preserve order.
|
|
7773
|
+
const baseIdentifiers = [];
|
|
7774
|
+
for (const id of extractedIdentifiers) {
|
|
7775
|
+
const base = ExpressionUtils.baseIdentifier(id);
|
|
7776
|
+
if (!baseIdentifiers.includes(base)) {
|
|
7777
|
+
baseIdentifiers.push(base);
|
|
7781
7778
|
}
|
|
7782
7779
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
element._value = value;
|
|
7789
|
-
}
|
|
7780
|
+
// Apply custom rewrite if provided (used by VOnDirective)
|
|
7781
|
+
let processedExpression = expression;
|
|
7782
|
+
if (options?.rewriteExpression) {
|
|
7783
|
+
// Pass the original extracted identifiers (may include member chains)
|
|
7784
|
+
processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
|
|
7790
7785
|
}
|
|
7786
|
+
// Build cache key including options that affect compilation
|
|
7787
|
+
const cacheKey = JSON.stringify({
|
|
7788
|
+
expr: processedExpression,
|
|
7789
|
+
additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
|
|
7790
|
+
});
|
|
7791
|
+
// Check cache
|
|
7792
|
+
let evaluatorFunc = this.cache.get(cacheKey);
|
|
7793
|
+
if (!evaluatorFunc) {
|
|
7794
|
+
// Compile the expression using base identifiers (no dots)
|
|
7795
|
+
evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
|
|
7796
|
+
// Cache the compiled function
|
|
7797
|
+
this.cache.set(cacheKey, evaluatorFunc);
|
|
7798
|
+
}
|
|
7799
|
+
return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
|
|
7791
7800
|
}
|
|
7792
7801
|
/**
|
|
7793
|
-
*
|
|
7802
|
+
* Compiles an expression into a function.
|
|
7803
|
+
*
|
|
7804
|
+
* @param expression The expression string to compile.
|
|
7805
|
+
* @param identifiers The identifiers extracted from the expression.
|
|
7806
|
+
* @param options Optional configuration.
|
|
7807
|
+
* @returns A compiled function.
|
|
7794
7808
|
*/
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7809
|
+
static compileExpression(expression, identifiers, options) {
|
|
7810
|
+
// Build parameter list: globals first, then identifiers (base names), then additional context
|
|
7811
|
+
// Filter out identifiers that are already in the global whitelist to avoid duplicate parameters
|
|
7812
|
+
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7813
|
+
const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
|
|
7814
|
+
const params = [
|
|
7815
|
+
...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7816
|
+
...filteredIdentifiers
|
|
7817
|
+
];
|
|
7818
|
+
// Add additional context parameters if provided
|
|
7819
|
+
if (options?.additionalContext) {
|
|
7820
|
+
params.push(...Object.keys(options.additionalContext));
|
|
7798
7821
|
}
|
|
7799
|
-
|
|
7800
|
-
|
|
7822
|
+
const args = params.join(", ");
|
|
7823
|
+
const funcBody = `return (${expression});`;
|
|
7824
|
+
try {
|
|
7825
|
+
// Create the function with error handling
|
|
7826
|
+
return new Function(args, funcBody);
|
|
7827
|
+
}
|
|
7828
|
+
catch (error) {
|
|
7829
|
+
throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
|
|
7801
7830
|
}
|
|
7802
7831
|
}
|
|
7803
7832
|
/**
|
|
7804
|
-
*
|
|
7805
|
-
*
|
|
7806
|
-
*
|
|
7833
|
+
* Evaluates the expression with the current bindings.
|
|
7834
|
+
*
|
|
7835
|
+
* @returns The result of the expression evaluation.
|
|
7807
7836
|
*/
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7837
|
+
evaluate() {
|
|
7838
|
+
try {
|
|
7839
|
+
// Build arguments: globals first, then identifiers, then additional context
|
|
7840
|
+
// Filter out identifiers that are already in the global whitelist to match compileExpression
|
|
7841
|
+
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7842
|
+
const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
|
|
7843
|
+
const values = [
|
|
7844
|
+
// Global whitelist values
|
|
7845
|
+
...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7846
|
+
// Identifier values from bindings (excluding globals)
|
|
7847
|
+
...filteredIdentifiers.map(id => this.bindings.get(id))
|
|
7848
|
+
];
|
|
7849
|
+
// Add additional context values if provided
|
|
7850
|
+
if (this.additionalContext) {
|
|
7851
|
+
values.push(...Object.values(this.additionalContext));
|
|
7812
7852
|
}
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
if (
|
|
7817
|
-
|
|
7853
|
+
// Call the compiled function with the gathered values. When an index is
|
|
7854
|
+
// known only at run time, record which concrete paths this evaluation
|
|
7855
|
+
// read so that change matching can be narrowed to them (see trackedPaths).
|
|
7856
|
+
if (!this.hasDynamicIndex) {
|
|
7857
|
+
return this.evaluatorFunc(...values);
|
|
7818
7858
|
}
|
|
7859
|
+
const paths = new Set();
|
|
7860
|
+
this.trackedPathSet = undefined;
|
|
7861
|
+
const result = ReactiveProxy.track(paths, () => this.evaluatorFunc(...values));
|
|
7862
|
+
this.trackedPathSet = paths;
|
|
7863
|
+
return result;
|
|
7864
|
+
}
|
|
7865
|
+
catch (error) {
|
|
7866
|
+
console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
|
|
7867
|
+
throw error;
|
|
7819
7868
|
}
|
|
7820
7869
|
}
|
|
7870
|
+
/**
|
|
7871
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
7872
|
+
* recorded only for an expression with a run-time index ("items[i].name").
|
|
7873
|
+
* Each evaluation replaces the previous set, so a path read for a former
|
|
7874
|
+
* index value is dropped as soon as the expression runs with the new one.
|
|
7875
|
+
* Undefined before the first evaluation and for expressions without such an index.
|
|
7876
|
+
*/
|
|
7877
|
+
get trackedPaths() {
|
|
7878
|
+
return this.trackedPathSet;
|
|
7879
|
+
}
|
|
7880
|
+
/**
|
|
7881
|
+
* Evaluates the expression and returns the result as a boolean.
|
|
7882
|
+
*
|
|
7883
|
+
* @returns The boolean result of the expression evaluation.
|
|
7884
|
+
*/
|
|
7885
|
+
evaluateAsBoolean() {
|
|
7886
|
+
return Boolean(this.evaluate());
|
|
7887
|
+
}
|
|
7888
|
+
/**
|
|
7889
|
+
* Gets the list of identifiers extracted from the expression.
|
|
7890
|
+
*/
|
|
7891
|
+
get dependentIdentifiers() {
|
|
7892
|
+
// Return the original extracted identifiers (may include member chains like 'a.b')
|
|
7893
|
+
return this.originalIdentifiers;
|
|
7894
|
+
}
|
|
7895
|
+
/**
|
|
7896
|
+
* Clears the expression cache.
|
|
7897
|
+
* This should be called sparingly, as it will force recompilation of all expressions.
|
|
7898
|
+
*/
|
|
7899
|
+
static clearCache() {
|
|
7900
|
+
this.cache.clear();
|
|
7901
|
+
}
|
|
7821
7902
|
}
|
|
7822
7903
|
|
|
7823
7904
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7824
7905
|
/**
|
|
7825
|
-
* Directive for
|
|
7826
|
-
*
|
|
7827
|
-
*
|
|
7828
|
-
*
|
|
7829
|
-
*
|
|
7830
|
-
*
|
|
7906
|
+
* Directive for binding dynamic attributes to DOM elements.
|
|
7907
|
+
* The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
|
|
7908
|
+
* The syntax for using the `v-bind` directive is `v-bind:attribute="expression"`, where `attribute` is the name of the HTML attribute you want to bind (e.g., `src`, `href`, `class`, etc.), and `expression` is a JavaScript expression that evaluates to the value you want to assign to that attribute.
|
|
7909
|
+
* Example usage:
|
|
7910
|
+
* <img v-bind:src="imageSrc" />
|
|
7911
|
+
* The value of `imageSrc` should be defined in the component's data object.
|
|
7912
|
+
* When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
|
|
7913
|
+
* This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
|
|
7914
|
+
* It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
|
|
7915
|
+
* The `v-bind` directive can also be used with shorthand syntax using a colon (:).
|
|
7916
|
+
* For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
|
|
7917
|
+
* This shorthand syntax is often used for brevity and improved readability in templates.
|
|
7918
|
+
* Overall, the `v-bind` directive is a powerful tool for creating dynamic and responsive web applications by allowing seamless integration between the data model and the DOM.
|
|
7831
7919
|
*/
|
|
7832
|
-
class
|
|
7920
|
+
class VBindDirective {
|
|
7833
7921
|
/**
|
|
7834
7922
|
* The virtual node to which this directive is applied.
|
|
7835
7923
|
*/
|
|
7836
7924
|
#vNode;
|
|
7837
7925
|
/**
|
|
7838
|
-
* The
|
|
7926
|
+
* The expression evaluator for this directive.
|
|
7839
7927
|
*/
|
|
7840
|
-
#
|
|
7928
|
+
#evaluator;
|
|
7841
7929
|
/**
|
|
7842
|
-
* The
|
|
7930
|
+
* The name of the attribute to bind (e.g., "src", "class", "style").
|
|
7931
|
+
*/
|
|
7932
|
+
#attributeName;
|
|
7933
|
+
/**
|
|
7934
|
+
* The original expression string from the directive.
|
|
7935
|
+
*/
|
|
7936
|
+
#expression;
|
|
7937
|
+
/**
|
|
7938
|
+
* The set of class names managed by this directive (used when binding to the "class" attribute).
|
|
7939
|
+
* This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
|
|
7940
|
+
*/
|
|
7941
|
+
#managedClasses = new Set();
|
|
7942
|
+
/**
|
|
7943
|
+
* The set of style properties managed by this directive (used when binding to the "style" attribute).
|
|
7944
|
+
* This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
|
|
7945
|
+
*/
|
|
7946
|
+
#managedStyles = new Set();
|
|
7947
|
+
/**
|
|
7948
|
+
* @param context The context for parsing the directive.
|
|
7843
7949
|
*/
|
|
7844
|
-
#componentApp;
|
|
7845
7950
|
constructor(context) {
|
|
7846
7951
|
this.#vNode = context.vNode;
|
|
7847
|
-
|
|
7952
|
+
// Extract the attribute name from the directive
|
|
7953
|
+
// e.g., "v-bind:src" -> "src", ":href" -> "href"
|
|
7954
|
+
const attrName = context.attribute.name;
|
|
7955
|
+
if (attrName.startsWith('v-bind:')) {
|
|
7956
|
+
this.#attributeName = attrName.substring(7);
|
|
7957
|
+
}
|
|
7958
|
+
else if (attrName.startsWith(':')) {
|
|
7959
|
+
this.#attributeName = attrName.substring(1);
|
|
7960
|
+
}
|
|
7961
|
+
// Parse the expression and create the evaluator
|
|
7962
|
+
this.#expression = context.attribute.value;
|
|
7963
|
+
if (this.#expression && context.vNode.bindings) {
|
|
7964
|
+
this.#evaluator = ExpressionEvaluator.create(this.#expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
|
|
7965
|
+
}
|
|
7848
7966
|
// Remove the directive attribute from the element
|
|
7849
7967
|
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
7850
7968
|
}
|
|
@@ -7852,7 +7970,7 @@
|
|
|
7852
7970
|
* @inheritdoc
|
|
7853
7971
|
*/
|
|
7854
7972
|
get name() {
|
|
7855
|
-
return StandardDirectiveName.
|
|
7973
|
+
return StandardDirectiveName.V_BIND;
|
|
7856
7974
|
}
|
|
7857
7975
|
/**
|
|
7858
7976
|
* @inheritdoc
|
|
@@ -7864,7 +7982,7 @@
|
|
|
7864
7982
|
* @inheritdoc
|
|
7865
7983
|
*/
|
|
7866
7984
|
get needsAnchor() {
|
|
7867
|
-
return
|
|
7985
|
+
return false;
|
|
7868
7986
|
}
|
|
7869
7987
|
/**
|
|
7870
7988
|
* @inheritdoc
|
|
@@ -7876,10 +7994,15 @@
|
|
|
7876
7994
|
* @inheritdoc
|
|
7877
7995
|
*/
|
|
7878
7996
|
get domUpdater() {
|
|
7879
|
-
|
|
7997
|
+
const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
|
|
7998
|
+
const evaluator = this.#evaluator;
|
|
7999
|
+
// Create an updater that handles the attribute binding
|
|
7880
8000
|
const updater = {
|
|
7881
8001
|
get dependentIdentifiers() {
|
|
7882
|
-
return
|
|
8002
|
+
return identifiers;
|
|
8003
|
+
},
|
|
8004
|
+
get trackedPaths() {
|
|
8005
|
+
return evaluator?.trackedPaths;
|
|
7883
8006
|
},
|
|
7884
8007
|
applyToDOM: () => {
|
|
7885
8008
|
this.#render();
|
|
@@ -7891,577 +8014,539 @@
|
|
|
7891
8014
|
* @inheritdoc
|
|
7892
8015
|
*/
|
|
7893
8016
|
get templatize() {
|
|
7894
|
-
return
|
|
8017
|
+
return false;
|
|
7895
8018
|
}
|
|
7896
8019
|
/**
|
|
7897
8020
|
* @inheritdoc
|
|
7898
8021
|
*/
|
|
7899
8022
|
get dependentIdentifiers() {
|
|
7900
|
-
return [];
|
|
8023
|
+
return this.#evaluator?.dependentIdentifiers ?? [];
|
|
7901
8024
|
}
|
|
7902
8025
|
/**
|
|
7903
|
-
*
|
|
8026
|
+
* Indicates if this directive is binding the "key" attribute.
|
|
8027
|
+
* The "key" attribute is special and is used for optimizing rendering of lists.
|
|
8028
|
+
* If this directive is binding the "key" attribute, it will be handled by the VForDirective.
|
|
7904
8029
|
*/
|
|
7905
|
-
get
|
|
7906
|
-
return
|
|
8030
|
+
get isKey() {
|
|
8031
|
+
return (this.#attributeName === 'key');
|
|
7907
8032
|
}
|
|
7908
8033
|
/**
|
|
7909
|
-
*
|
|
8034
|
+
* Indicates if this directive is binding the "options" attribute or any of its sub-properties (e.g., "options.intersection").
|
|
8035
|
+
* The "options" attribute is special and is used for passing options to certain directives like VIntersectionDirective.
|
|
7910
8036
|
*/
|
|
7911
|
-
get
|
|
7912
|
-
return
|
|
8037
|
+
get isOptions() {
|
|
8038
|
+
return (this.#attributeName === 'options' || this.#attributeName?.startsWith('options.') === true);
|
|
7913
8039
|
}
|
|
7914
8040
|
/**
|
|
7915
|
-
*
|
|
8041
|
+
* Gets the name of the attribute being bound (e.g., "src", "class", "options", "options.intersection").
|
|
7916
8042
|
*/
|
|
7917
|
-
get
|
|
7918
|
-
return
|
|
8043
|
+
get attributeName() {
|
|
8044
|
+
return this.#attributeName;
|
|
7919
8045
|
}
|
|
7920
8046
|
/**
|
|
7921
|
-
*
|
|
8047
|
+
* Gets the original expression string from the directive.
|
|
7922
8048
|
*/
|
|
7923
|
-
get
|
|
7924
|
-
return
|
|
8049
|
+
get expression() {
|
|
8050
|
+
return this.#expression;
|
|
7925
8051
|
}
|
|
7926
8052
|
/**
|
|
7927
|
-
*
|
|
8053
|
+
* Evaluates the bound expression and returns the current value.
|
|
8054
|
+
* Returns undefined when no evaluator is available (e.g., empty expression).
|
|
7928
8055
|
*/
|
|
7929
|
-
|
|
7930
|
-
return
|
|
8056
|
+
evaluate() {
|
|
8057
|
+
return this.#evaluator?.evaluate();
|
|
7931
8058
|
}
|
|
7932
8059
|
/**
|
|
7933
8060
|
* @inheritdoc
|
|
7934
8061
|
*/
|
|
7935
|
-
get
|
|
8062
|
+
get onMount() {
|
|
8063
|
+
// A constant expression (e.g. :label="'Apple'") has no reactive
|
|
8064
|
+
// identifiers, so the identifier-driven update cycle never applies it.
|
|
8065
|
+
// Render it once at mount instead.
|
|
8066
|
+
if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
|
|
8067
|
+
return () => {
|
|
8068
|
+
this.#render();
|
|
8069
|
+
};
|
|
8070
|
+
}
|
|
7936
8071
|
return undefined;
|
|
7937
8072
|
}
|
|
7938
8073
|
/**
|
|
7939
8074
|
* @inheritdoc
|
|
7940
8075
|
*/
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
// Not rendered, no action needed
|
|
7944
|
-
return;
|
|
7945
|
-
}
|
|
7946
|
-
// Get the component's root VNode
|
|
7947
|
-
const componentVNode = this.#componentApp.rootVNode;
|
|
7948
|
-
// Destroy component application first (calls @unmount hooks while DOM is still accessible)
|
|
7949
|
-
this.#componentApp.unmount();
|
|
7950
|
-
// Remove the component's root node from the DOM
|
|
7951
|
-
if (componentVNode?.node.parentNode) {
|
|
7952
|
-
componentVNode.node.parentNode.removeChild(componentVNode.node);
|
|
7953
|
-
}
|
|
7954
|
-
this.#componentApp = undefined;
|
|
8076
|
+
get onMounted() {
|
|
8077
|
+
return undefined;
|
|
7955
8078
|
}
|
|
7956
8079
|
/**
|
|
7957
|
-
*
|
|
8080
|
+
* @inheritdoc
|
|
7958
8081
|
*/
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
// Already rendered, no action needed
|
|
7962
|
-
return;
|
|
7963
|
-
}
|
|
7964
|
-
// Clone the component's template and replace the original node
|
|
7965
|
-
const clone = this.#cloneNode();
|
|
7966
|
-
this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
|
|
7967
|
-
// Get properties from :options or :options.component directive
|
|
7968
|
-
let properties = {};
|
|
7969
|
-
const optionsDirective = this.#vNode.directiveManager?.optionsDirective('component');
|
|
7970
|
-
if (optionsDirective && optionsDirective.expression && this.#vNode.bindings) {
|
|
7971
|
-
// Evaluate the options expression using ExpressionEvaluator
|
|
7972
|
-
const evaluator = ExpressionEvaluator.create(optionsDirective.expression, this.#vNode.bindings, this.#vNode.vApplication.functionDependencies);
|
|
7973
|
-
const result = evaluator.evaluate();
|
|
7974
|
-
if (typeof result === 'object' && result !== null) {
|
|
7975
|
-
properties = result;
|
|
7976
|
-
}
|
|
7977
|
-
}
|
|
7978
|
-
// Get component definition from the application's component registry
|
|
7979
|
-
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
7980
|
-
if (!component) {
|
|
7981
|
-
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
7982
|
-
}
|
|
7983
|
-
// Create component instance
|
|
7984
|
-
const instance = component.createInstance(properties);
|
|
7985
|
-
// Create and mount child application using the parent application's registries
|
|
7986
|
-
this.#componentApp = this.#vNode.vApplication.createChildApp(instance);
|
|
7987
|
-
this.#componentApp.mount(clone);
|
|
8082
|
+
get onUpdate() {
|
|
8083
|
+
return undefined;
|
|
7988
8084
|
}
|
|
7989
8085
|
/**
|
|
7990
|
-
*
|
|
7991
|
-
* @returns The cloned root HTMLElement of the component.
|
|
7992
|
-
* @throws Error if the component or its template is not found.
|
|
8086
|
+
* @inheritdoc
|
|
7993
8087
|
*/
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
7997
|
-
if (!component) {
|
|
7998
|
-
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
7999
|
-
}
|
|
8000
|
-
// Get template element
|
|
8001
|
-
const finalTemplateID = component.templateID || component.id;
|
|
8002
|
-
const templateElement = document.querySelector(`#${finalTemplateID}`);
|
|
8003
|
-
if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
|
|
8004
|
-
throw new Error(`Template element '#${finalTemplateID}' not found`);
|
|
8005
|
-
}
|
|
8006
|
-
// Clone template content
|
|
8007
|
-
const fragment = templateElement.content.cloneNode(true);
|
|
8008
|
-
const childNodes = Array.from(fragment.childNodes);
|
|
8009
|
-
// Find the first element node
|
|
8010
|
-
for (const node of childNodes) {
|
|
8011
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
8012
|
-
return node;
|
|
8013
|
-
}
|
|
8014
|
-
}
|
|
8015
|
-
throw new Error(`No element found in template '#${finalTemplateID}'`);
|
|
8088
|
+
get onUpdated() {
|
|
8089
|
+
return undefined;
|
|
8016
8090
|
}
|
|
8017
|
-
}
|
|
8018
|
-
|
|
8019
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8020
|
-
/**
|
|
8021
|
-
* Utility class for creating reactive proxies that automatically track changes.
|
|
8022
|
-
*/
|
|
8023
|
-
class ReactiveProxy {
|
|
8024
|
-
/**
|
|
8025
|
-
* A WeakMap to store the proxy for each target object and path combination.
|
|
8026
|
-
* This prevents creating multiple proxies for the same object accessed from different paths.
|
|
8027
|
-
*/
|
|
8028
|
-
static proxyCache = new WeakMap();
|
|
8029
|
-
/**
|
|
8030
|
-
* A WeakMap to track which objects are proxies, mapping proxy -> original target.
|
|
8031
|
-
* This prevents double-wrapping of already proxied objects.
|
|
8032
|
-
*/
|
|
8033
|
-
static proxyToTarget = new WeakMap();
|
|
8034
|
-
/**
|
|
8035
|
-
* A WeakSet to track objects marked as "raw" (non-reactive).
|
|
8036
|
-
* These objects will not be wrapped with Proxy.
|
|
8037
|
-
*/
|
|
8038
|
-
static rawObjects = new WeakSet();
|
|
8039
|
-
/**
|
|
8040
|
-
* A WeakMap to store the path for each proxy object.
|
|
8041
|
-
* This allows retrieving the source path of an object for computed property mapping.
|
|
8042
|
-
*/
|
|
8043
|
-
static proxyPaths = new WeakMap();
|
|
8044
|
-
/**
|
|
8045
|
-
* Dispatchers per (target, path). Every target reached while walking a proxy
|
|
8046
|
-
* subtree registers an entry pointing at the same dispatcher as the outermost
|
|
8047
|
-
* proxy of that subtree, so callers can look up the dispatcher from any
|
|
8048
|
-
* intermediate proxy when subscribing.
|
|
8049
|
-
*/
|
|
8050
|
-
static dispatchers = new WeakMap();
|
|
8051
|
-
/**
|
|
8052
|
-
* A Map to store path aliases.
|
|
8053
|
-
* Key: alias path (e.g., "editingNestedStep.steps")
|
|
8054
|
-
* Value: source path (e.g., "routes[0].steps[0].steps")
|
|
8055
|
-
* This allows mapping variable names to their actual source paths for dependency tracking.
|
|
8056
|
-
*/
|
|
8057
|
-
static pathAliases = new Map();
|
|
8058
8091
|
/**
|
|
8059
|
-
*
|
|
8060
|
-
* identifier and whether descendants of the path also match.
|
|
8092
|
+
* @inheritdoc
|
|
8061
8093
|
*/
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
*
|
|
8067
|
-
* Rationale: a Proxy can only safely intercept plain data containers. Built-in
|
|
8068
|
-
* and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
|
|
8069
|
-
* `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
|
|
8070
|
-
* slots, and invoking their native methods with a Proxy as the `this`/receiver
|
|
8071
|
-
* throws "Illegal invocation". They must therefore never be wrapped. This is an
|
|
8072
|
-
* allow-list rather than a deny-list so that *every* such exotic type is
|
|
8073
|
-
* excluded by default, not just the handful we happened to enumerate.
|
|
8074
|
-
*
|
|
8075
|
-
* Plain user-defined class instances report `[object Object]` (unless they set
|
|
8076
|
-
* `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
|
|
8077
|
-
* `Map`/`Set` stay reactive because the proxy specially wraps their mutation
|
|
8078
|
-
* methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
|
|
8079
|
-
* since they cannot be wrapped that way and would break the same as `File`.
|
|
8080
|
-
*
|
|
8081
|
-
* Callers that want a plain object kept non-reactive can still opt out
|
|
8082
|
-
* explicitly via {@link markRaw}.
|
|
8094
|
+
get onUnmount() {
|
|
8095
|
+
return undefined;
|
|
8096
|
+
}
|
|
8097
|
+
/**
|
|
8098
|
+
* @inheritdoc
|
|
8083
8099
|
*/
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
'[object Arguments]',
|
|
8088
|
-
'[object Map]',
|
|
8089
|
-
'[object Set]',
|
|
8090
|
-
]);
|
|
8100
|
+
get onUnmounted() {
|
|
8101
|
+
return undefined;
|
|
8102
|
+
}
|
|
8091
8103
|
/**
|
|
8092
|
-
*
|
|
8093
|
-
* See {@link REACTIVE_TYPE_TAGS} for the reasoning.
|
|
8104
|
+
* @inheritdoc
|
|
8094
8105
|
*/
|
|
8095
|
-
|
|
8096
|
-
|
|
8106
|
+
destroy() {
|
|
8107
|
+
// Do nothing. No special cleanup needed.
|
|
8097
8108
|
}
|
|
8098
8109
|
/**
|
|
8099
|
-
*
|
|
8100
|
-
* The proxy will call the onChange callback whenever a property is modified.
|
|
8101
|
-
*
|
|
8102
|
-
* @param target The object to make reactive.
|
|
8103
|
-
* @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
|
|
8104
|
-
* @param path The current path in the object tree (used internally for nested objects).
|
|
8105
|
-
* @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
|
|
8106
|
-
* @returns A reactive proxy of the target object.
|
|
8110
|
+
* Renders the bound attribute by evaluating the expression and updating the DOM element.
|
|
8107
8111
|
*/
|
|
8108
|
-
|
|
8109
|
-
//
|
|
8110
|
-
if (
|
|
8111
|
-
return
|
|
8112
|
+
#render() {
|
|
8113
|
+
// Do nothing for special attributes
|
|
8114
|
+
if (this.isKey || this.isOptions) {
|
|
8115
|
+
return;
|
|
8112
8116
|
}
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8117
|
+
const element = this.#vNode.node;
|
|
8118
|
+
const attributeName = this.#attributeName;
|
|
8119
|
+
// If there's no evaluator or attribute name, do nothing
|
|
8120
|
+
if (!this.#evaluator || !attributeName) {
|
|
8121
|
+
return;
|
|
8116
8122
|
}
|
|
8117
|
-
//
|
|
8118
|
-
|
|
8119
|
-
//
|
|
8120
|
-
if (
|
|
8121
|
-
|
|
8123
|
+
// Evaluate the expression to get the value
|
|
8124
|
+
const value = this.#evaluator.evaluate();
|
|
8125
|
+
// Handle different attribute types
|
|
8126
|
+
if (attributeName === 'class') {
|
|
8127
|
+
this.#updateClass(element, value);
|
|
8122
8128
|
}
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
return target;
|
|
8129
|
+
else if (attributeName === 'style') {
|
|
8130
|
+
this.#updateStyle(element, value);
|
|
8126
8131
|
}
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8132
|
+
else if (this.#isCustomElementProperty(element, attributeName, value)) {
|
|
8133
|
+
// Custom elements: set objects/arrays (and declared props) as properties
|
|
8134
|
+
// so complex values are not serialized to strings via setAttribute.
|
|
8135
|
+
// HTML attributes are lowercased by the browser, so resolve to the
|
|
8136
|
+
// actual camelCase property name declared on the custom element.
|
|
8137
|
+
const propName = this.#resolveCustomElementPropertyName(element, attributeName);
|
|
8138
|
+
element[propName] = value;
|
|
8133
8139
|
}
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
return existingProxy;
|
|
8140
|
-
}
|
|
8140
|
+
else if (this.#isDOMProperty(attributeName)) {
|
|
8141
|
+
this.#updateProperty(element, attributeName, value);
|
|
8142
|
+
}
|
|
8143
|
+
else if (this.#isBooleanAttribute(attributeName)) {
|
|
8144
|
+
this.#updateBooleanAttribute(element, attributeName, value);
|
|
8141
8145
|
}
|
|
8142
8146
|
else {
|
|
8143
|
-
|
|
8144
|
-
this.proxyCache.set(target, pathMap);
|
|
8147
|
+
this.#updateAttribute(element, attributeName, value);
|
|
8145
8148
|
}
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
const keyStr = String(key);
|
|
8164
|
-
const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
8165
|
-
return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
|
|
8149
|
+
}
|
|
8150
|
+
/**
|
|
8151
|
+
* Updates the class attribute with support for string, array, and object formats.
|
|
8152
|
+
*/
|
|
8153
|
+
#updateClass(element, value) {
|
|
8154
|
+
// Determine the new set of classes to apply
|
|
8155
|
+
let newClasses = [];
|
|
8156
|
+
if (typeof value === 'string') {
|
|
8157
|
+
newClasses = value.split(/\s+/).filter(Boolean);
|
|
8158
|
+
}
|
|
8159
|
+
else if (Array.isArray(value)) {
|
|
8160
|
+
// Flatten array elements that may contain strings or objects
|
|
8161
|
+
newClasses = value
|
|
8162
|
+
.filter(Boolean)
|
|
8163
|
+
.flatMap(cls => {
|
|
8164
|
+
if (typeof cls === 'string') {
|
|
8165
|
+
return cls.split(/\s+/).filter(Boolean);
|
|
8166
8166
|
}
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
//
|
|
8170
|
-
|
|
8171
|
-
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
8172
|
-
if (!arrayMutationMethods.includes(key)) {
|
|
8173
|
-
return value;
|
|
8174
|
-
}
|
|
8175
|
-
return function (...args) {
|
|
8176
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8177
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8178
|
-
return result;
|
|
8179
|
-
};
|
|
8180
|
-
}
|
|
8181
|
-
// For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
|
|
8182
|
-
if (obj.constructor.name === 'Map') {
|
|
8183
|
-
const mapMutationMethods = ['set', 'delete', 'clear'];
|
|
8184
|
-
return function (...args) {
|
|
8185
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8186
|
-
if (mapMutationMethods.includes(key)) {
|
|
8187
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8188
|
-
}
|
|
8189
|
-
return result;
|
|
8190
|
-
};
|
|
8191
|
-
}
|
|
8192
|
-
// For Set, we only want to wrap mutation methods, not read methods like 'has'
|
|
8193
|
-
if (obj.constructor.name === 'Set') {
|
|
8194
|
-
const setMutationMethods = ['add', 'delete', 'clear'];
|
|
8195
|
-
return function (...args) {
|
|
8196
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8197
|
-
if (setMutationMethods.includes(key)) {
|
|
8198
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8199
|
-
}
|
|
8200
|
-
return result;
|
|
8201
|
-
};
|
|
8202
|
-
}
|
|
8167
|
+
else if (typeof cls === 'object' && cls !== null) {
|
|
8168
|
+
// Handle object format within array: { className: condition }
|
|
8169
|
+
// Keys may contain space-separated class names
|
|
8170
|
+
return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
8203
8171
|
}
|
|
8204
|
-
return
|
|
8205
|
-
}
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8172
|
+
return [];
|
|
8173
|
+
});
|
|
8174
|
+
}
|
|
8175
|
+
else if (typeof value === 'object' && value !== null) {
|
|
8176
|
+
// Keys may contain space-separated class names
|
|
8177
|
+
newClasses = Object.keys(value).filter(key => value[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
8178
|
+
}
|
|
8179
|
+
// Remove previously managed classes
|
|
8180
|
+
this.#managedClasses.forEach(cls => element.classList.remove(cls));
|
|
8181
|
+
// Add newly managed classes
|
|
8182
|
+
newClasses.forEach(cls => element.classList.add(cls));
|
|
8183
|
+
// Update managed classes list
|
|
8184
|
+
this.#managedClasses = new Set(newClasses);
|
|
8185
|
+
}
|
|
8186
|
+
/**
|
|
8187
|
+
* Updates the style attribute with support for object format.
|
|
8188
|
+
*/
|
|
8189
|
+
#updateStyle(element, value) {
|
|
8190
|
+
let newStyles = [];
|
|
8191
|
+
if (typeof value === 'string') {
|
|
8192
|
+
// Directly set the style string
|
|
8193
|
+
element.style.cssText = value;
|
|
8194
|
+
// Extract managed properties
|
|
8195
|
+
newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
|
|
8196
|
+
}
|
|
8197
|
+
else if (typeof value === 'object' && value !== null) {
|
|
8198
|
+
// Remove all previously managed properties
|
|
8199
|
+
this.#managedStyles.forEach(prop => {
|
|
8200
|
+
element.style.removeProperty(this.#camelToKebab(prop));
|
|
8201
|
+
});
|
|
8202
|
+
// Add newly managed properties
|
|
8203
|
+
for (const key in value) {
|
|
8204
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
8205
|
+
const cssKey = this.#camelToKebab(key);
|
|
8206
|
+
const cssValue = value[key];
|
|
8207
|
+
if (cssValue != null) {
|
|
8208
|
+
element.style.setProperty(cssKey, String(cssValue));
|
|
8209
|
+
newStyles.push(key);
|
|
8210
|
+
}
|
|
8214
8211
|
}
|
|
8215
|
-
return result;
|
|
8216
|
-
},
|
|
8217
|
-
deleteProperty(obj, key) {
|
|
8218
|
-
const result = Reflect.deleteProperty(obj, key);
|
|
8219
|
-
const keyStr = String(key);
|
|
8220
|
-
const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
8221
|
-
ReactiveProxy.dispatch(dispatcher, fullPath);
|
|
8222
|
-
return result;
|
|
8223
8212
|
}
|
|
8224
|
-
});
|
|
8225
|
-
// Cache the proxy for this path
|
|
8226
|
-
pathMap.set(path, proxy);
|
|
8227
|
-
// Track that this proxy wraps the target to prevent double-wrapping
|
|
8228
|
-
this.proxyToTarget.set(proxy, target);
|
|
8229
|
-
// Store the path for this proxy (for computed property mapping)
|
|
8230
|
-
if (path) {
|
|
8231
|
-
this.proxyPaths.set(proxy, path);
|
|
8232
8213
|
}
|
|
8233
|
-
|
|
8214
|
+
// Update managed properties list
|
|
8215
|
+
this.#managedStyles = new Set(newStyles);
|
|
8216
|
+
}
|
|
8217
|
+
/**
|
|
8218
|
+
* Converts camelCase to kebab-case for CSS properties.
|
|
8219
|
+
*/
|
|
8220
|
+
#camelToKebab(str) {
|
|
8221
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
8222
|
+
}
|
|
8223
|
+
/**
|
|
8224
|
+
* Returns true when the target is a custom element and the binding should be
|
|
8225
|
+
* delivered as a property rather than an HTML attribute.
|
|
8226
|
+
*
|
|
8227
|
+
* Three conditions trigger property delivery:
|
|
8228
|
+
* 1. The value is an object or array — serialising these to a string attribute
|
|
8229
|
+
* would lose type information.
|
|
8230
|
+
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
8231
|
+
* defineComponent), even when the value is a primitive.
|
|
8232
|
+
* 3. The element declares a matching prop in its static _props (case-insensitive
|
|
8233
|
+
* and kebab-case → camelCase tolerant), so primitive values reach the
|
|
8234
|
+
* reactive binding via the generated setter instead of being lost as a
|
|
8235
|
+
* plain HTML attribute that no attributeChangedCallback observes.
|
|
8236
|
+
*/
|
|
8237
|
+
#isCustomElementProperty(element, name, value) {
|
|
8238
|
+
if (!element.tagName.includes('-')) {
|
|
8239
|
+
return false;
|
|
8240
|
+
}
|
|
8241
|
+
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
8242
|
+
if (isObjectOrArray || name in element) {
|
|
8243
|
+
return true;
|
|
8244
|
+
}
|
|
8245
|
+
return this.#findDeclaredProp(element, name) !== undefined;
|
|
8246
|
+
}
|
|
8247
|
+
/**
|
|
8248
|
+
* Resolves the actual property name on a custom element for a given
|
|
8249
|
+
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
8250
|
+
* but custom element props are typically camelCase. This method checks the
|
|
8251
|
+
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
8252
|
+
* match against both the raw attribute name and its kebab-case → camelCase
|
|
8253
|
+
* conversion (so `user-name` resolves to `userName`), falling back to the
|
|
8254
|
+
* original name when no match is found.
|
|
8255
|
+
*/
|
|
8256
|
+
#resolveCustomElementPropertyName(element, name) {
|
|
8257
|
+
// Fast path: exact match already exists
|
|
8258
|
+
if (name in element) {
|
|
8259
|
+
return name;
|
|
8260
|
+
}
|
|
8261
|
+
return this.#findDeclaredProp(element, name) ?? name;
|
|
8234
8262
|
}
|
|
8235
8263
|
/**
|
|
8236
|
-
* Looks up
|
|
8237
|
-
*
|
|
8238
|
-
*
|
|
8264
|
+
* Looks up a declared prop on a custom element constructor whose name matches
|
|
8265
|
+
* the given attribute name, treating the comparison as case-insensitive and
|
|
8266
|
+
* tolerant of kebab-case → camelCase conversion. Returns the canonical prop
|
|
8267
|
+
* name (as declared) when a match is found, or undefined otherwise.
|
|
8239
8268
|
*/
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
if (!
|
|
8243
|
-
|
|
8244
|
-
this.dispatchers.set(target, pathMap);
|
|
8269
|
+
#findDeclaredProp(element, name) {
|
|
8270
|
+
const props = element.constructor._props;
|
|
8271
|
+
if (!Array.isArray(props)) {
|
|
8272
|
+
return undefined;
|
|
8245
8273
|
}
|
|
8246
|
-
const
|
|
8247
|
-
|
|
8248
|
-
|
|
8274
|
+
const lowerName = name.toLowerCase();
|
|
8275
|
+
const camelLower = this.#kebabToCamel(lowerName).toLowerCase();
|
|
8276
|
+
return props.find(p => {
|
|
8277
|
+
const lowerProp = p.toLowerCase();
|
|
8278
|
+
return lowerProp === lowerName || lowerProp === camelLower;
|
|
8279
|
+
});
|
|
8280
|
+
}
|
|
8281
|
+
/**
|
|
8282
|
+
* Converts kebab-case to camelCase (e.g. `user-name` → `userName`).
|
|
8283
|
+
*/
|
|
8284
|
+
#kebabToCamel(str) {
|
|
8285
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
8286
|
+
}
|
|
8287
|
+
/**
|
|
8288
|
+
* Checks if the attribute should be set as a DOM property.
|
|
8289
|
+
*/
|
|
8290
|
+
#isDOMProperty(name) {
|
|
8291
|
+
const domProperties = ['value', 'checked', 'selected', 'innerHTML', 'textContent'];
|
|
8292
|
+
return domProperties.includes(name);
|
|
8293
|
+
}
|
|
8294
|
+
/**
|
|
8295
|
+
* Checks if the attribute is a boolean attribute.
|
|
8296
|
+
*/
|
|
8297
|
+
#isBooleanAttribute(name) {
|
|
8298
|
+
const booleanAttributes = [
|
|
8299
|
+
'disabled', 'readonly', 'required', 'checked', 'selected',
|
|
8300
|
+
'multiple', 'autofocus', 'autoplay', 'controls', 'loop',
|
|
8301
|
+
'muted', 'open', 'hidden'
|
|
8302
|
+
];
|
|
8303
|
+
return booleanAttributes.includes(name);
|
|
8304
|
+
}
|
|
8305
|
+
/**
|
|
8306
|
+
* Updates a DOM property.
|
|
8307
|
+
* For value-like properties (value, textContent, innerHTML), null/undefined is converted to empty string
|
|
8308
|
+
* to match Vue.js behavior and prevent "undefined" from being displayed.
|
|
8309
|
+
*/
|
|
8310
|
+
#updateProperty(element, name, value) {
|
|
8311
|
+
if (value == null && (name === 'value' || name === 'textContent' || name === 'innerHTML')) {
|
|
8312
|
+
element[name] = '';
|
|
8313
|
+
if (name === 'value') {
|
|
8314
|
+
element._value = null;
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
else {
|
|
8318
|
+
element[name] = value;
|
|
8319
|
+
if (name === 'value') {
|
|
8320
|
+
// Store the original typed value so that v-model on radio buttons
|
|
8321
|
+
// can preserve the type (e.g., boolean false instead of string "false").
|
|
8322
|
+
element._value = value;
|
|
8323
|
+
}
|
|
8249
8324
|
}
|
|
8250
|
-
const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
|
|
8251
|
-
pathMap.set(path, dispatcher);
|
|
8252
|
-
return dispatcher;
|
|
8253
8325
|
}
|
|
8254
8326
|
/**
|
|
8255
|
-
*
|
|
8256
|
-
* Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
|
|
8327
|
+
* Updates a boolean attribute.
|
|
8257
8328
|
*/
|
|
8258
|
-
|
|
8259
|
-
if (
|
|
8260
|
-
|
|
8329
|
+
#updateBooleanAttribute(element, name, value) {
|
|
8330
|
+
if (value) {
|
|
8331
|
+
element.setAttribute(name, '');
|
|
8261
8332
|
}
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
listener(changedPath);
|
|
8333
|
+
else {
|
|
8334
|
+
element.removeAttribute(name);
|
|
8265
8335
|
}
|
|
8266
8336
|
}
|
|
8267
8337
|
/**
|
|
8268
|
-
*
|
|
8269
|
-
*
|
|
8270
|
-
*
|
|
8271
|
-
* path are delivered, which lets a child component receive notifications when the
|
|
8272
|
-
* nested contents of a prop change even though the prop reference itself is unchanged.
|
|
8273
|
-
*
|
|
8274
|
-
* @param proxyOrTarget A proxy returned from create(), or the underlying target object.
|
|
8275
|
-
* @param listener Called with the full source path of every relevant change.
|
|
8276
|
-
* @returns A function that removes the subscription.
|
|
8338
|
+
* Updates a regular attribute.
|
|
8339
|
+
* An attribute that already holds the value is left alone: setting `src` on
|
|
8340
|
+
* an <iframe>, even to the same URL, loads its page again.
|
|
8277
8341
|
*/
|
|
8278
|
-
|
|
8279
|
-
if (
|
|
8280
|
-
|
|
8342
|
+
#updateAttribute(element, name, value) {
|
|
8343
|
+
if (value == null || value === false) {
|
|
8344
|
+
if (element.hasAttribute(name)) {
|
|
8345
|
+
element.removeAttribute(name);
|
|
8346
|
+
}
|
|
8281
8347
|
}
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
return () => { };
|
|
8348
|
+
else {
|
|
8349
|
+
const text = String(value);
|
|
8350
|
+
if (element.getAttribute(name) !== text) {
|
|
8351
|
+
element.setAttribute(name, text);
|
|
8352
|
+
}
|
|
8288
8353
|
}
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8358
|
+
/**
|
|
8359
|
+
* Directive for rendering components.
|
|
8360
|
+
* Usage: <div v-component="componentId" :options="props"></div>
|
|
8361
|
+
*
|
|
8362
|
+
* The :options binding is used to pass properties to the component.
|
|
8363
|
+
* Example:
|
|
8364
|
+
* <div v-component="my-component" :options="{message: 'Hello'}"></div>
|
|
8365
|
+
*/
|
|
8366
|
+
class VComponentDirective {
|
|
8367
|
+
/**
|
|
8368
|
+
* The virtual node to which this directive is applied.
|
|
8369
|
+
*/
|
|
8370
|
+
#vNode;
|
|
8371
|
+
/**
|
|
8372
|
+
* The component ID to render.
|
|
8373
|
+
*/
|
|
8374
|
+
#componentId;
|
|
8375
|
+
/**
|
|
8376
|
+
* The application instance for the component.
|
|
8377
|
+
*/
|
|
8378
|
+
#componentApp;
|
|
8379
|
+
constructor(context) {
|
|
8380
|
+
this.#vNode = context.vNode;
|
|
8381
|
+
this.#componentId = context.attribute.value.trim();
|
|
8382
|
+
// Remove the directive attribute from the element
|
|
8383
|
+
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
8384
|
+
}
|
|
8385
|
+
/**
|
|
8386
|
+
* @inheritdoc
|
|
8387
|
+
*/
|
|
8388
|
+
get name() {
|
|
8389
|
+
return StandardDirectiveName.V_COMPONENT;
|
|
8390
|
+
}
|
|
8391
|
+
/**
|
|
8392
|
+
* @inheritdoc
|
|
8393
|
+
*/
|
|
8394
|
+
get vNode() {
|
|
8395
|
+
return this.#vNode;
|
|
8396
|
+
}
|
|
8397
|
+
/**
|
|
8398
|
+
* @inheritdoc
|
|
8399
|
+
*/
|
|
8400
|
+
get needsAnchor() {
|
|
8401
|
+
return true;
|
|
8402
|
+
}
|
|
8403
|
+
/**
|
|
8404
|
+
* @inheritdoc
|
|
8405
|
+
*/
|
|
8406
|
+
get bindingsPreparer() {
|
|
8407
|
+
return undefined;
|
|
8408
|
+
}
|
|
8409
|
+
/**
|
|
8410
|
+
* @inheritdoc
|
|
8411
|
+
*/
|
|
8412
|
+
get domUpdater() {
|
|
8413
|
+
// Create and return the DOM updater
|
|
8414
|
+
const updater = {
|
|
8415
|
+
get dependentIdentifiers() {
|
|
8416
|
+
return [];
|
|
8417
|
+
},
|
|
8418
|
+
applyToDOM: () => {
|
|
8419
|
+
this.#render();
|
|
8296
8420
|
}
|
|
8297
|
-
: listener;
|
|
8298
|
-
dispatcher.listeners.add(wrapper);
|
|
8299
|
-
return () => {
|
|
8300
|
-
dispatcher.listeners.delete(wrapper);
|
|
8301
8421
|
};
|
|
8422
|
+
return updater;
|
|
8302
8423
|
}
|
|
8303
8424
|
/**
|
|
8304
|
-
*
|
|
8305
|
-
*
|
|
8306
|
-
* @param obj The object to check.
|
|
8307
|
-
* @returns True if the object is a reactive proxy, false otherwise.
|
|
8425
|
+
* @inheritdoc
|
|
8308
8426
|
*/
|
|
8309
|
-
|
|
8310
|
-
return
|
|
8427
|
+
get templatize() {
|
|
8428
|
+
return true;
|
|
8311
8429
|
}
|
|
8312
8430
|
/**
|
|
8313
|
-
*
|
|
8314
|
-
* If the object is not a proxy, returns it as-is.
|
|
8315
|
-
*
|
|
8316
|
-
* @param obj The object to unwrap.
|
|
8317
|
-
* @returns The original object.
|
|
8431
|
+
* @inheritdoc
|
|
8318
8432
|
*/
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
// In a full implementation, we'd need to store a reverse mapping
|
|
8322
|
-
return obj;
|
|
8433
|
+
get dependentIdentifiers() {
|
|
8434
|
+
return [];
|
|
8323
8435
|
}
|
|
8324
8436
|
/**
|
|
8325
|
-
*
|
|
8326
|
-
* Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
|
|
8327
|
-
* This is useful for objects that should not be reactive, such as:
|
|
8328
|
-
* - Objects with private fields (class instances with # fields)
|
|
8329
|
-
* - Third-party library instances
|
|
8330
|
-
* - Objects used only for method calls
|
|
8331
|
-
*
|
|
8332
|
-
* @param obj The object to mark as raw.
|
|
8333
|
-
* @returns The same object (for chaining).
|
|
8437
|
+
* @inheritdoc
|
|
8334
8438
|
*/
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8439
|
+
get onMount() {
|
|
8440
|
+
return undefined;
|
|
8441
|
+
}
|
|
8442
|
+
/**
|
|
8443
|
+
* @inheritdoc
|
|
8444
|
+
*/
|
|
8445
|
+
get onMounted() {
|
|
8446
|
+
return undefined;
|
|
8340
8447
|
}
|
|
8341
8448
|
/**
|
|
8342
|
-
*
|
|
8343
|
-
*
|
|
8344
|
-
* @param obj The object to check.
|
|
8345
|
-
* @returns True if the object is marked as raw, false otherwise.
|
|
8449
|
+
* @inheritdoc
|
|
8346
8450
|
*/
|
|
8347
|
-
|
|
8348
|
-
return
|
|
8451
|
+
get onUpdate() {
|
|
8452
|
+
return undefined;
|
|
8349
8453
|
}
|
|
8350
8454
|
/**
|
|
8351
|
-
*
|
|
8352
|
-
* This is used to map computed property values back to their source paths.
|
|
8353
|
-
* For example, if a computed property returns `model.elements[0]`,
|
|
8354
|
-
* this method returns "model.elements[0]" for that object.
|
|
8355
|
-
*
|
|
8356
|
-
* @param obj The proxy object to get the path for.
|
|
8357
|
-
* @returns The source path, or undefined if not found.
|
|
8455
|
+
* @inheritdoc
|
|
8358
8456
|
*/
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
return undefined;
|
|
8362
|
-
}
|
|
8363
|
-
return this.proxyPaths.get(obj);
|
|
8457
|
+
get onUpdated() {
|
|
8458
|
+
return undefined;
|
|
8364
8459
|
}
|
|
8365
8460
|
/**
|
|
8366
|
-
*
|
|
8367
|
-
* This is used when a variable is assigned to reference an existing reactive object.
|
|
8368
|
-
* For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
|
|
8369
|
-
* so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
|
|
8370
|
-
*
|
|
8371
|
-
* @param aliasPath The alias path (e.g., "editingNestedStep")
|
|
8372
|
-
* @param sourcePath The source path (e.g., "routes[0].steps[0]")
|
|
8461
|
+
* @inheritdoc
|
|
8373
8462
|
*/
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
this.pathAliases.set(aliasPath, sourcePath);
|
|
8377
|
-
}
|
|
8463
|
+
get onUnmount() {
|
|
8464
|
+
return undefined;
|
|
8378
8465
|
}
|
|
8379
8466
|
/**
|
|
8380
|
-
*
|
|
8381
|
-
*
|
|
8382
|
-
* @param aliasPath The alias path to remove
|
|
8467
|
+
* @inheritdoc
|
|
8383
8468
|
*/
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
for (const key of this.pathAliases.keys()) {
|
|
8387
|
-
if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
|
|
8388
|
-
this.pathAliases.delete(key);
|
|
8389
|
-
}
|
|
8390
|
-
}
|
|
8469
|
+
get onUnmounted() {
|
|
8470
|
+
return undefined;
|
|
8391
8471
|
}
|
|
8392
8472
|
/**
|
|
8393
|
-
*
|
|
8394
|
-
* Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
|
|
8395
|
-
*
|
|
8396
|
-
* @param aliasPath The alias path to resolve
|
|
8397
|
-
* @returns The source path, or undefined if no alias exists
|
|
8473
|
+
* @inheritdoc
|
|
8398
8474
|
*/
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
return
|
|
8475
|
+
destroy() {
|
|
8476
|
+
if (!this.#componentApp) {
|
|
8477
|
+
// Not rendered, no action needed
|
|
8478
|
+
return;
|
|
8403
8479
|
}
|
|
8404
|
-
//
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8480
|
+
// Get the component's root VNode
|
|
8481
|
+
const componentVNode = this.#componentApp.rootVNode;
|
|
8482
|
+
// Destroy component application first (calls @unmount hooks while DOM is still accessible)
|
|
8483
|
+
this.#componentApp.unmount();
|
|
8484
|
+
// Remove the component's root node from the DOM
|
|
8485
|
+
if (componentVNode?.node.parentNode) {
|
|
8486
|
+
componentVNode.node.parentNode.removeChild(componentVNode.node);
|
|
8411
8487
|
}
|
|
8412
|
-
|
|
8488
|
+
this.#componentApp = undefined;
|
|
8413
8489
|
}
|
|
8414
8490
|
/**
|
|
8415
|
-
*
|
|
8416
|
-
* For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
|
|
8417
|
-
* then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
|
|
8418
|
-
*
|
|
8419
|
-
* @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
|
|
8420
|
-
* @param identifier The identifier to check (e.g., "editingNestedStep.steps")
|
|
8421
|
-
* @returns True if the change path matches the identifier
|
|
8491
|
+
* Renders the component.
|
|
8422
8492
|
*/
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
return
|
|
8493
|
+
#render() {
|
|
8494
|
+
if (this.#componentApp) {
|
|
8495
|
+
// Already rendered, no action needed
|
|
8496
|
+
return;
|
|
8427
8497
|
}
|
|
8428
|
-
//
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8498
|
+
// Clone the component's template and replace the original node
|
|
8499
|
+
const clone = this.#cloneNode();
|
|
8500
|
+
this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
|
|
8501
|
+
// Get properties from :options or :options.component directive
|
|
8502
|
+
let properties = {};
|
|
8503
|
+
const optionsDirective = this.#vNode.directiveManager?.optionsDirective('component');
|
|
8504
|
+
if (optionsDirective && optionsDirective.expression && this.#vNode.bindings) {
|
|
8505
|
+
// Evaluate the options expression using ExpressionEvaluator
|
|
8506
|
+
const evaluator = ExpressionEvaluator.create(optionsDirective.expression, this.#vNode.bindings, this.#vNode.vApplication.functionDependencies);
|
|
8507
|
+
const result = evaluator.evaluate();
|
|
8508
|
+
if (typeof result === 'object' && result !== null) {
|
|
8509
|
+
properties = result;
|
|
8510
|
+
}
|
|
8433
8511
|
}
|
|
8434
|
-
|
|
8512
|
+
// Get component definition from the application's component registry
|
|
8513
|
+
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
8514
|
+
if (!component) {
|
|
8515
|
+
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
8516
|
+
}
|
|
8517
|
+
// Create component instance
|
|
8518
|
+
const instance = component.createInstance(properties);
|
|
8519
|
+
// Create and mount child application using the parent application's registries
|
|
8520
|
+
this.#componentApp = this.#vNode.vApplication.createChildApp(instance);
|
|
8521
|
+
this.#componentApp.mount(clone);
|
|
8435
8522
|
}
|
|
8436
8523
|
/**
|
|
8437
|
-
*
|
|
8438
|
-
*
|
|
8439
|
-
*
|
|
8440
|
-
* one index or key.
|
|
8441
|
-
*
|
|
8442
|
-
* @param changePath The path that was changed (e.g., "items[2].name")
|
|
8443
|
-
* @param identifier The identifier to check (e.g., "items[*].name")
|
|
8444
|
-
* @param descendants Whether a change below the identifier also matches
|
|
8445
|
-
* @returns True if the change path matches the identifier
|
|
8524
|
+
* Clones the component's template and returns the root element.
|
|
8525
|
+
* @returns The cloned root HTMLElement of the component.
|
|
8526
|
+
* @throws Error if the component or its template is not found.
|
|
8446
8527
|
*/
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8528
|
+
#cloneNode() {
|
|
8529
|
+
// Get component definition from the application's component registry
|
|
8530
|
+
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
8531
|
+
if (!component) {
|
|
8532
|
+
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
8450
8533
|
}
|
|
8451
|
-
|
|
8452
|
-
|
|
8534
|
+
// Get template element
|
|
8535
|
+
const finalTemplateID = component.templateID || component.id;
|
|
8536
|
+
const templateElement = document.querySelector(`#${finalTemplateID}`);
|
|
8537
|
+
if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
|
|
8538
|
+
throw new Error(`Template element '#${finalTemplateID}' not found`);
|
|
8453
8539
|
}
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
this.pathPatterns.set(cacheKey, pattern);
|
|
8540
|
+
// Clone template content
|
|
8541
|
+
const fragment = templateElement.content.cloneNode(true);
|
|
8542
|
+
const childNodes = Array.from(fragment.childNodes);
|
|
8543
|
+
// Find the first element node
|
|
8544
|
+
for (const node of childNodes) {
|
|
8545
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
8546
|
+
return node;
|
|
8547
|
+
}
|
|
8463
8548
|
}
|
|
8464
|
-
|
|
8549
|
+
throw new Error(`No element found in template '#${finalTemplateID}'`);
|
|
8465
8550
|
}
|
|
8466
8551
|
}
|
|
8467
8552
|
|
|
@@ -8492,6 +8577,13 @@
|
|
|
8492
8577
|
* The set of changed identifiers.
|
|
8493
8578
|
*/
|
|
8494
8579
|
#changes = new Set();
|
|
8580
|
+
/**
|
|
8581
|
+
* On the root bindings: every descendant scope (a v-for row, a component) that
|
|
8582
|
+
* recorded a change since the last clearChanges(). The root clears their change
|
|
8583
|
+
* sets together with its own at the end of the update cycle, so a loop variable
|
|
8584
|
+
* set when a row was created does not count as changed in every later cycle.
|
|
8585
|
+
*/
|
|
8586
|
+
#dirtyScopes;
|
|
8495
8587
|
/**
|
|
8496
8588
|
* Cache for array lengths to detect length changes when the same object reference is used.
|
|
8497
8589
|
*/
|
|
@@ -8634,7 +8726,7 @@
|
|
|
8634
8726
|
for (const part of changedPath?.split('.') || []) {
|
|
8635
8727
|
path = path ? `${path}.${part}` : part;
|
|
8636
8728
|
this.#logger?.debug(`Binding changed: ${path}`);
|
|
8637
|
-
this.#
|
|
8729
|
+
this.#addChange(path);
|
|
8638
8730
|
}
|
|
8639
8731
|
if (!this.#suppressOnChange) {
|
|
8640
8732
|
this.#onChange?.(changedPath);
|
|
@@ -8674,7 +8766,7 @@
|
|
|
8674
8766
|
for (const part of changedPath.split('.')) {
|
|
8675
8767
|
path = path ? `${path}.${part}` : part;
|
|
8676
8768
|
this.#logger?.debug(`Binding changed (external): ${path}`);
|
|
8677
|
-
this.#
|
|
8769
|
+
this.#addChange(path);
|
|
8678
8770
|
}
|
|
8679
8771
|
if (!this.#suppressOnChange) {
|
|
8680
8772
|
this.#onChange?.(changedPath);
|
|
@@ -8701,7 +8793,7 @@
|
|
|
8701
8793
|
const newValuePreview = newValueString.length > 100 ? `${newValueString.substring(0, 100)}...` : newValueString;
|
|
8702
8794
|
this.#logger.debug(`Binding set on ${target === obj ? 'local' : 'parent'}: ${key}: ${oldValuePreview} -> ${newValuePreview}`);
|
|
8703
8795
|
}
|
|
8704
|
-
this.#
|
|
8796
|
+
this.#addChange(key);
|
|
8705
8797
|
if (!this.#suppressOnChange) {
|
|
8706
8798
|
this.#onChange?.(key);
|
|
8707
8799
|
}
|
|
@@ -8716,7 +8808,7 @@
|
|
|
8716
8808
|
this.#externalSubscriptions.delete(key);
|
|
8717
8809
|
}
|
|
8718
8810
|
this.#logger?.debug(`Binding deleted: ${key}`);
|
|
8719
|
-
this.#
|
|
8811
|
+
this.#addChange(key);
|
|
8720
8812
|
this.#onChange?.(key);
|
|
8721
8813
|
return result;
|
|
8722
8814
|
}
|
|
@@ -8757,6 +8849,53 @@
|
|
|
8757
8849
|
*/
|
|
8758
8850
|
clearChanges() {
|
|
8759
8851
|
this.#changes.clear();
|
|
8852
|
+
this.#changeLog.length = 0;
|
|
8853
|
+
if (this.#dirtyScopes) {
|
|
8854
|
+
for (const scope of this.#dirtyScopes) {
|
|
8855
|
+
scope.#changes.clear();
|
|
8856
|
+
}
|
|
8857
|
+
this.#dirtyScopes.clear();
|
|
8858
|
+
}
|
|
8859
|
+
}
|
|
8860
|
+
/**
|
|
8861
|
+
* Records a changed identifier on this scope and, for a non-root scope, registers the
|
|
8862
|
+
* scope with the root so that the change is cleared at the end of the update cycle.
|
|
8863
|
+
*/
|
|
8864
|
+
#addChange(key) {
|
|
8865
|
+
this.#changes.add(key);
|
|
8866
|
+
let root = this;
|
|
8867
|
+
while (root.#parent) {
|
|
8868
|
+
root = root.#parent;
|
|
8869
|
+
}
|
|
8870
|
+
root.#changeLog.push(key);
|
|
8871
|
+
if (root !== this) {
|
|
8872
|
+
(root.#dirtyScopes ??= new Set()).add(this);
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
/**
|
|
8876
|
+
* Every change recorded since the last clearChanges(), in order and with repeats, on the
|
|
8877
|
+
* root; the change set itself cannot tell a write made during an update cycle from the one
|
|
8878
|
+
* the cycle was scheduled for when both touch the same identifier.
|
|
8879
|
+
*/
|
|
8880
|
+
#changeLog = [];
|
|
8881
|
+
/**
|
|
8882
|
+
* A marker for {@link changesSince}: the number of changes recorded so far.
|
|
8883
|
+
*/
|
|
8884
|
+
get changeLogLength() {
|
|
8885
|
+
return this.#root.#changeLog.length;
|
|
8886
|
+
}
|
|
8887
|
+
/**
|
|
8888
|
+
* The distinct identifiers recorded after the marker was taken.
|
|
8889
|
+
*/
|
|
8890
|
+
changesSince(marker) {
|
|
8891
|
+
return Array.from(new Set(this.#root.#changeLog.slice(marker)));
|
|
8892
|
+
}
|
|
8893
|
+
get #root() {
|
|
8894
|
+
let root = this;
|
|
8895
|
+
while (root.#parent) {
|
|
8896
|
+
root = root.#parent;
|
|
8897
|
+
}
|
|
8898
|
+
return root;
|
|
8760
8899
|
}
|
|
8761
8900
|
/**
|
|
8762
8901
|
* Sets a binding value.
|
|
@@ -8932,7 +9071,7 @@
|
|
|
8932
9071
|
* @param key The identifier to mark as changed.
|
|
8933
9072
|
*/
|
|
8934
9073
|
markChanged(key) {
|
|
8935
|
-
this.#
|
|
9074
|
+
this.#addChange(key);
|
|
8936
9075
|
}
|
|
8937
9076
|
/**
|
|
8938
9077
|
* Resolves a local path alias for the given identifier by walking up the bindings chain.
|
|
@@ -8979,6 +9118,54 @@
|
|
|
8979
9118
|
// Fall back to global alias resolution (computed properties, etc.)
|
|
8980
9119
|
return ReactiveProxy.doesChangeMatchIdentifier(changePath, identifier);
|
|
8981
9120
|
}
|
|
9121
|
+
/**
|
|
9122
|
+
* Checks whether any of the changes affects an expression with the given identifiers.
|
|
9123
|
+
*
|
|
9124
|
+
* An identifier with a run-time index ("frames[*].url" for frames[i].url) normally
|
|
9125
|
+
* matches a change at any index. When trackedPaths holds the paths the expression read
|
|
9126
|
+
* during its last evaluation, the identifier is narrowed to the concrete paths among
|
|
9127
|
+
* them ("frames[0].url"), so a write to another element is ignored. When no recorded
|
|
9128
|
+
* path fits the identifier (not evaluated yet, a raw object, a short-circuited read),
|
|
9129
|
+
* the identifier keeps matching every index.
|
|
9130
|
+
*
|
|
9131
|
+
* @param changes The change paths of the current update cycle.
|
|
9132
|
+
* @param identifiers The identifiers the expression depends on.
|
|
9133
|
+
* @param trackedPaths The paths read during the expression's last evaluation, if recorded.
|
|
9134
|
+
* @returns True if some change matches some identifier.
|
|
9135
|
+
*/
|
|
9136
|
+
isAffected(changes, identifiers, trackedPaths) {
|
|
9137
|
+
if (changes.length === 0) {
|
|
9138
|
+
return false;
|
|
9139
|
+
}
|
|
9140
|
+
for (const identifier of identifiers) {
|
|
9141
|
+
const candidates = this.#concretePaths(identifier, trackedPaths) ?? [identifier];
|
|
9142
|
+
for (const candidate of candidates) {
|
|
9143
|
+
if (changes.some(change => this.doesChangeMatchIdentifier(change, candidate))) {
|
|
9144
|
+
return true;
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
}
|
|
9148
|
+
return false;
|
|
9149
|
+
}
|
|
9150
|
+
/**
|
|
9151
|
+
* Returns the recorded paths that an identifier with a run-time index stood for during
|
|
9152
|
+
* the last evaluation, or undefined when the identifier has no such index or nothing
|
|
9153
|
+
* recorded fits it. The identifier is tried both as spelled and resolved through path
|
|
9154
|
+
* aliases, since recorded paths are source paths.
|
|
9155
|
+
*/
|
|
9156
|
+
#concretePaths(identifier, trackedPaths) {
|
|
9157
|
+
if (!trackedPaths || trackedPaths.size === 0 || !identifier.includes('[*]')) {
|
|
9158
|
+
return undefined;
|
|
9159
|
+
}
|
|
9160
|
+
const resolved = this.resolveAlias(identifier);
|
|
9161
|
+
const paths = [];
|
|
9162
|
+
for (const path of trackedPaths) {
|
|
9163
|
+
if (ReactiveProxy.pathMatches(path, identifier) || (resolved !== undefined && ReactiveProxy.pathMatches(path, resolved))) {
|
|
9164
|
+
paths.push(path);
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
return paths.length > 0 ? paths : undefined;
|
|
9168
|
+
}
|
|
8982
9169
|
}
|
|
8983
9170
|
|
|
8984
9171
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
@@ -9173,10 +9360,14 @@
|
|
|
9173
9360
|
*/
|
|
9174
9361
|
get domUpdater() {
|
|
9175
9362
|
const identifiers = this.dependentIdentifiers;
|
|
9363
|
+
const directive = this;
|
|
9176
9364
|
const updater = {
|
|
9177
9365
|
get dependentIdentifiers() {
|
|
9178
9366
|
return identifiers;
|
|
9179
9367
|
},
|
|
9368
|
+
get trackedPaths() {
|
|
9369
|
+
return directive.#collectTrackedPaths();
|
|
9370
|
+
},
|
|
9180
9371
|
applyToDOM: () => {
|
|
9181
9372
|
this.#refresh();
|
|
9182
9373
|
}
|
|
@@ -9199,6 +9390,21 @@
|
|
|
9199
9390
|
}
|
|
9200
9391
|
return Array.from(ids);
|
|
9201
9392
|
}
|
|
9393
|
+
/**
|
|
9394
|
+
* The paths the prop expressions read during their last evaluation, united across
|
|
9395
|
+
* props, or undefined when no prop expression has a run-time index.
|
|
9396
|
+
*/
|
|
9397
|
+
#collectTrackedPaths() {
|
|
9398
|
+
let paths;
|
|
9399
|
+
for (const prop of this.#props) {
|
|
9400
|
+
const tracked = prop.evaluator?.trackedPaths;
|
|
9401
|
+
if (tracked) {
|
|
9402
|
+
paths ??= new Set();
|
|
9403
|
+
tracked.forEach(path => paths.add(path));
|
|
9404
|
+
}
|
|
9405
|
+
}
|
|
9406
|
+
return paths;
|
|
9407
|
+
}
|
|
9202
9408
|
/**
|
|
9203
9409
|
* @inheritdoc
|
|
9204
9410
|
*
|
|
@@ -10004,6 +10210,11 @@
|
|
|
10004
10210
|
* The function used to evaluate the text with current bindings.
|
|
10005
10211
|
*/
|
|
10006
10212
|
#evaluate;
|
|
10213
|
+
/**
|
|
10214
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
10215
|
+
* recorded only when some expression has a run-time index ("items[i].name").
|
|
10216
|
+
*/
|
|
10217
|
+
#trackedPaths;
|
|
10007
10218
|
/**
|
|
10008
10219
|
* Constructs a VTextEvaluator instance.
|
|
10009
10220
|
* @param text The text containing embedded expressions in the form of {{...}}.
|
|
@@ -10044,8 +10255,10 @@
|
|
|
10044
10255
|
func: new Function(args, funcBody)
|
|
10045
10256
|
};
|
|
10046
10257
|
});
|
|
10258
|
+
// Only an expression with an index known at run time needs its reads recorded
|
|
10259
|
+
const hasDynamicIndex = this.#identifiers.some(id => id.includes('[*]'));
|
|
10047
10260
|
// Create a function to reconstruct the text with evaluated expressions
|
|
10048
|
-
|
|
10261
|
+
const evaluateText = (bindings) => {
|
|
10049
10262
|
let result = text;
|
|
10050
10263
|
evaluators.forEach((evaluator, i) => {
|
|
10051
10264
|
// Build values for baseArgs (no dots) in the same order as function parameters
|
|
@@ -10063,6 +10276,15 @@
|
|
|
10063
10276
|
});
|
|
10064
10277
|
return result;
|
|
10065
10278
|
};
|
|
10279
|
+
this.#evaluate = hasDynamicIndex
|
|
10280
|
+
? (bindings) => {
|
|
10281
|
+
const paths = new Set();
|
|
10282
|
+
this.#trackedPaths = undefined;
|
|
10283
|
+
const result = ReactiveProxy.track(paths, () => evaluateText(bindings));
|
|
10284
|
+
this.#trackedPaths = paths;
|
|
10285
|
+
return result;
|
|
10286
|
+
}
|
|
10287
|
+
: evaluateText;
|
|
10066
10288
|
}
|
|
10067
10289
|
/**
|
|
10068
10290
|
* Checks if the given text contains any expressions in the form of {{...}}.
|
|
@@ -10079,6 +10301,14 @@
|
|
|
10079
10301
|
get identifiers() {
|
|
10080
10302
|
return this.#identifiers;
|
|
10081
10303
|
}
|
|
10304
|
+
/**
|
|
10305
|
+
* The source paths read through reactive proxies during the last evaluation.
|
|
10306
|
+
* Recorded only when an expression has a run-time index ("items[i].name");
|
|
10307
|
+
* undefined before the first evaluation and for texts without such an index.
|
|
10308
|
+
*/
|
|
10309
|
+
get trackedPaths() {
|
|
10310
|
+
return this.#trackedPaths;
|
|
10311
|
+
}
|
|
10082
10312
|
/**
|
|
10083
10313
|
* Evaluates the text with the provided bindings and returns the resulting string.
|
|
10084
10314
|
* @param bindings The bindings to use for evaluating the expressions.
|
|
@@ -10568,7 +10798,7 @@
|
|
|
10568
10798
|
if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
|
|
10569
10799
|
// If this is a text node with a text evaluator, update its content if needed
|
|
10570
10800
|
// Check if any of the identifiers are in the changed identifiers (considering path aliases)
|
|
10571
|
-
const changed = this
|
|
10801
|
+
const changed = this.bindings.isAffected(changes, this.#textEvaluator.identifiers, this.#textEvaluator.trackedPaths);
|
|
10572
10802
|
// If the text node has changed, update its content
|
|
10573
10803
|
if (changed) {
|
|
10574
10804
|
const text = this.#node;
|
|
@@ -10591,7 +10821,7 @@
|
|
|
10591
10821
|
}
|
|
10592
10822
|
// Prepare bindings for each preparer if relevant identifiers have changed
|
|
10593
10823
|
for (const preparer of this.#directiveManager.bindingsPreparers) {
|
|
10594
|
-
const changed =
|
|
10824
|
+
const changed = this.bindings.isAffected(changes, preparer.dependentIdentifiers, preparer.trackedPaths);
|
|
10595
10825
|
if (changed) {
|
|
10596
10826
|
preparer.prepareBindings();
|
|
10597
10827
|
}
|
|
@@ -10600,7 +10830,7 @@
|
|
|
10600
10830
|
// Apply DOM updaters from directives, if any
|
|
10601
10831
|
if (this.#directiveManager?.domUpdaters) {
|
|
10602
10832
|
for (const updater of this.#directiveManager.domUpdaters) {
|
|
10603
|
-
const changed =
|
|
10833
|
+
const changed = this.bindings.isAffected(changes, updater.dependentIdentifiers, updater.trackedPaths);
|
|
10604
10834
|
if (changed) {
|
|
10605
10835
|
updater.applyToDOM();
|
|
10606
10836
|
}
|
|
@@ -10853,6 +11083,21 @@
|
|
|
10853
11083
|
get allDependentIdentifiers() {
|
|
10854
11084
|
return this.#allDependentIdentifiers;
|
|
10855
11085
|
}
|
|
11086
|
+
/**
|
|
11087
|
+
* The paths the conditions of the chain read during their last evaluation, united
|
|
11088
|
+
* across directives, or undefined when no condition has a run-time index.
|
|
11089
|
+
*/
|
|
11090
|
+
get trackedPaths() {
|
|
11091
|
+
let paths;
|
|
11092
|
+
for (const directive of this.#directives) {
|
|
11093
|
+
const tracked = directive.trackedPaths;
|
|
11094
|
+
if (tracked) {
|
|
11095
|
+
paths ??= new Set();
|
|
11096
|
+
tracked.forEach(path => paths.add(path));
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
return paths;
|
|
11100
|
+
}
|
|
10856
11101
|
/**
|
|
10857
11102
|
* Adds a directive (v-else-if or v-else) to the conditional context.
|
|
10858
11103
|
* @param directive The directive to add.
|
|
@@ -11049,11 +11294,15 @@
|
|
|
11049
11294
|
*/
|
|
11050
11295
|
get domUpdater() {
|
|
11051
11296
|
const identifiers = this.#conditionalContext.allDependentIdentifiers;
|
|
11297
|
+
const conditionalContext = this.#conditionalContext;
|
|
11052
11298
|
// Create an updater that handles the conditional rendering
|
|
11053
11299
|
const updater = {
|
|
11054
11300
|
get dependentIdentifiers() {
|
|
11055
11301
|
return identifiers;
|
|
11056
11302
|
},
|
|
11303
|
+
get trackedPaths() {
|
|
11304
|
+
return conditionalContext.trackedPaths;
|
|
11305
|
+
},
|
|
11057
11306
|
applyToDOM: () => {
|
|
11058
11307
|
this.#render();
|
|
11059
11308
|
}
|
|
@@ -11072,6 +11321,13 @@
|
|
|
11072
11321
|
get dependentIdentifiers() {
|
|
11073
11322
|
return this.#evaluator?.dependentIdentifiers ?? [];
|
|
11074
11323
|
}
|
|
11324
|
+
/**
|
|
11325
|
+
* The paths the condition read during its last evaluation, when it has an index
|
|
11326
|
+
* known only at run time (see ExpressionEvaluator.trackedPaths).
|
|
11327
|
+
*/
|
|
11328
|
+
get trackedPaths() {
|
|
11329
|
+
return this.#evaluator?.trackedPaths;
|
|
11330
|
+
}
|
|
11075
11331
|
/**
|
|
11076
11332
|
* The context for managing related conditional directives (v-if, v-else-if, v-else).
|
|
11077
11333
|
*/
|
|
@@ -11438,11 +11694,15 @@
|
|
|
11438
11694
|
*/
|
|
11439
11695
|
get domUpdater() {
|
|
11440
11696
|
const identifiers = this.#sourceEvaluator?.dependentIdentifiers ?? [];
|
|
11697
|
+
const sourceEvaluator = this.#sourceEvaluator;
|
|
11441
11698
|
// Create and return the DOM updater
|
|
11442
11699
|
const updater = {
|
|
11443
11700
|
get dependentIdentifiers() {
|
|
11444
11701
|
return identifiers;
|
|
11445
11702
|
},
|
|
11703
|
+
get trackedPaths() {
|
|
11704
|
+
return sourceEvaluator?.trackedPaths;
|
|
11705
|
+
},
|
|
11446
11706
|
applyToDOM: () => {
|
|
11447
11707
|
this.#render();
|
|
11448
11708
|
}
|
|
@@ -12162,6 +12422,9 @@
|
|
|
12162
12422
|
get dependentIdentifiers() {
|
|
12163
12423
|
return identifiers;
|
|
12164
12424
|
},
|
|
12425
|
+
get trackedPaths() {
|
|
12426
|
+
return evaluator.trackedPaths;
|
|
12427
|
+
},
|
|
12165
12428
|
applyToDOM: () => {
|
|
12166
12429
|
const element = vNode.node;
|
|
12167
12430
|
const htmlContent = evaluator.evaluate() ?? '';
|
|
@@ -12588,6 +12851,9 @@
|
|
|
12588
12851
|
get dependentIdentifiers() {
|
|
12589
12852
|
return self.#collectDependentIdentifiers();
|
|
12590
12853
|
},
|
|
12854
|
+
get trackedPaths() {
|
|
12855
|
+
return self.#evaluator?.trackedPaths;
|
|
12856
|
+
},
|
|
12591
12857
|
applyToDOM: () => {
|
|
12592
12858
|
self.#render();
|
|
12593
12859
|
}
|
|
@@ -13986,6 +14252,9 @@
|
|
|
13986
14252
|
get dependentIdentifiers() {
|
|
13987
14253
|
return identifiers;
|
|
13988
14254
|
},
|
|
14255
|
+
get trackedPaths() {
|
|
14256
|
+
return evaluator.trackedPaths;
|
|
14257
|
+
},
|
|
13989
14258
|
applyToDOM() {
|
|
13990
14259
|
const shouldRender = evaluator.evaluateAsBoolean();
|
|
13991
14260
|
if (shouldRender) {
|
|
@@ -14153,6 +14422,9 @@
|
|
|
14153
14422
|
get dependentIdentifiers() {
|
|
14154
14423
|
return identifiers;
|
|
14155
14424
|
},
|
|
14425
|
+
get trackedPaths() {
|
|
14426
|
+
return evaluator.trackedPaths;
|
|
14427
|
+
},
|
|
14156
14428
|
applyToDOM() {
|
|
14157
14429
|
const element = vNode.node;
|
|
14158
14430
|
element.textContent = toDisplayString(evaluator.evaluate());
|
|
@@ -14315,6 +14587,9 @@
|
|
|
14315
14587
|
get dependentIdentifiers() {
|
|
14316
14588
|
return identifiers;
|
|
14317
14589
|
},
|
|
14590
|
+
get trackedPaths() {
|
|
14591
|
+
return evaluator.trackedPaths;
|
|
14592
|
+
},
|
|
14318
14593
|
applyToDOM: () => {
|
|
14319
14594
|
const value = evaluator.evaluateAsBoolean();
|
|
14320
14595
|
const previous = this.#previousValue;
|
|
@@ -15203,6 +15478,14 @@
|
|
|
15203
15478
|
* Flag to indicate if an update is already scheduled.
|
|
15204
15479
|
*/
|
|
15205
15480
|
#updateScheduled = false;
|
|
15481
|
+
/**
|
|
15482
|
+
* How many update cycles in a row ended with changes made during the cycle (by a watcher
|
|
15483
|
+
* or a lifecycle hook), each of which runs another cycle. Reset when a cycle ends clean;
|
|
15484
|
+
* past MAX_CASCADED_UPDATES the late changes are dropped, so a watcher that writes on every
|
|
15485
|
+
* cycle cannot keep the application updating forever.
|
|
15486
|
+
*/
|
|
15487
|
+
#cascadedUpdates = 0;
|
|
15488
|
+
static #MAX_CASCADED_UPDATES = 100;
|
|
15206
15489
|
/**
|
|
15207
15490
|
* The custom element hosting this application when it powers a component
|
|
15208
15491
|
* defined via defineComponent(). Set by IchigoElement before mounting.
|
|
@@ -15646,8 +15929,10 @@
|
|
|
15646
15929
|
}
|
|
15647
15930
|
this.#updateScheduled = true;
|
|
15648
15931
|
queueMicrotask(() => {
|
|
15649
|
-
|
|
15932
|
+
// Cleared before the update runs, not after: a change made during the update (by a
|
|
15933
|
+
// watcher or a lifecycle hook) then schedules the next cycle instead of being dropped.
|
|
15650
15934
|
this.#updateScheduled = false;
|
|
15935
|
+
this.#update();
|
|
15651
15936
|
});
|
|
15652
15937
|
}
|
|
15653
15938
|
/**
|
|
@@ -15661,14 +15946,46 @@
|
|
|
15661
15946
|
// This converts paths like "model.elements[0].executionListeners"
|
|
15662
15947
|
// to "selectedElement.executionListeners" when selectedElement points to model.elements[0]
|
|
15663
15948
|
this.#applyComputedPathMappings();
|
|
15949
|
+
// The changes this cycle was scheduled for. Whatever is recorded after this point was
|
|
15950
|
+
// written during the cycle itself (see below); the marker tells the two apart even when
|
|
15951
|
+
// a watcher writes the very identifier it watches.
|
|
15952
|
+
const scheduledChanges = this.#bindings ? this.#bindings.changes : [];
|
|
15953
|
+
const lateMarker = this.#bindings ? this.#bindings.changeLogLength : 0;
|
|
15664
15954
|
// Notify watchers before DOM update
|
|
15665
15955
|
if (this.#bindings) {
|
|
15666
|
-
this.#watcher.notify(
|
|
15956
|
+
this.#watcher.notify(scheduledChanges, (p) => this.#getValueByPath(p), this.#bindings.raw);
|
|
15957
|
+
// A watcher may have written data. The computed properties that depend on it were
|
|
15958
|
+
// only marked dirty, which the DOM diff below does not see; resolve them now, so this
|
|
15959
|
+
// render already shows what the watcher did, and so the computed names are in the
|
|
15960
|
+
// change set when the diff runs.
|
|
15961
|
+
this.#flushDirtyComputeds();
|
|
15962
|
+
this.#applyComputedPathMappings();
|
|
15667
15963
|
}
|
|
15668
15964
|
// Update the DOM
|
|
15669
15965
|
this.#vNode?.update();
|
|
15670
|
-
// Clear the set of changed identifiers after the update
|
|
15671
|
-
|
|
15966
|
+
// Clear the set of changed identifiers after the update. Changes recorded during the
|
|
15967
|
+
// cycle (by watchers or lifecycle hooks) are carried into the next cycle instead: the DOM
|
|
15968
|
+
// diff above has seen them, but watchers that depend on them have not, since watcher
|
|
15969
|
+
// notification ran before they were written.
|
|
15970
|
+
if (this.#bindings) {
|
|
15971
|
+
const late = this.#bindings.changesSince(lateMarker);
|
|
15972
|
+
this.#bindings.clearChanges();
|
|
15973
|
+
if (late.length > 0) {
|
|
15974
|
+
this.#cascadedUpdates++;
|
|
15975
|
+
if (this.#cascadedUpdates > VApplication.#MAX_CASCADED_UPDATES) {
|
|
15976
|
+
this.#logger.warn(`Update cycle did not settle after ${VApplication.#MAX_CASCADED_UPDATES} cascaded updates; dropping changes: ${late.join(', ')}`);
|
|
15977
|
+
this.#cascadedUpdates = 0;
|
|
15978
|
+
return;
|
|
15979
|
+
}
|
|
15980
|
+
for (const change of late) {
|
|
15981
|
+
this.#bindings.markChanged(change);
|
|
15982
|
+
}
|
|
15983
|
+
this.#scheduleUpdate();
|
|
15984
|
+
}
|
|
15985
|
+
else {
|
|
15986
|
+
this.#cascadedUpdates = 0;
|
|
15987
|
+
}
|
|
15988
|
+
}
|
|
15672
15989
|
}
|
|
15673
15990
|
/**
|
|
15674
15991
|
* Applies computed source path mappings to the current changes.
|