@mintjamsinc/ichigojs 0.1.86 → 0.1.87
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 +1668 -1419
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +1668 -1419
- 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 +1668 -1419
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VBindings.d.ts +16 -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.esm.js
CHANGED
|
@@ -6728,1117 +6728,1235 @@ base.MethodDefinition = base.PropertyDefinition = base.Property = function (node
|
|
|
6728
6728
|
|
|
6729
6729
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
6730
6730
|
/**
|
|
6731
|
-
* Utility class for
|
|
6732
|
-
*/
|
|
6733
|
-
class ExpressionUtils {
|
|
6734
|
-
/**
|
|
6735
|
-
* Extracts variable and function names used in the expression.
|
|
6736
|
-
* @param expression The expression string to analyze.
|
|
6737
|
-
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
6738
|
-
* @param options Optional parsing options.
|
|
6739
|
-
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
6740
|
-
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
6741
|
-
* for backward compatibility with interpolation and binding directives).
|
|
6742
|
-
* @returns An array of identifier names.
|
|
6743
|
-
*/
|
|
6744
|
-
static extractIdentifiers(expression, functionDependencies, options) {
|
|
6745
|
-
const identifiers = new Set();
|
|
6746
|
-
// In expression mode we wrap in parens so acorn parses the source as a single expression.
|
|
6747
|
-
// In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
|
|
6748
|
-
const source = options?.asScript ? expression : `(${expression})`;
|
|
6749
|
-
const ast = parse(source, { ecmaVersion: "latest" });
|
|
6750
|
-
// Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
|
|
6751
|
-
full(ast, (node) => {
|
|
6752
|
-
if (node.type === 'Identifier') {
|
|
6753
|
-
identifiers.add(node.name);
|
|
6754
|
-
// Check if the identifier is a function name
|
|
6755
|
-
if (functionDependencies[node.name]) {
|
|
6756
|
-
// If it is, add its dependencies to the list of identifiers
|
|
6757
|
-
for (const dependency of functionDependencies[node.name]) {
|
|
6758
|
-
identifiers.add(dependency);
|
|
6759
|
-
}
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
else if (node.type === 'MemberExpression') {
|
|
6763
|
-
// Reconstruct chain parts from a MemberExpression (e.g. a.b.c). Each part
|
|
6764
|
-
// carries its own separator so that the chain is spelled the way the reactive
|
|
6765
|
-
// proxy reports changes: array indices in brackets, properties after a dot
|
|
6766
|
-
// (e.g. "items[0].name").
|
|
6767
|
-
const parts = [];
|
|
6768
|
-
let cur = node;
|
|
6769
|
-
while (cur) {
|
|
6770
|
-
// property part (right side)
|
|
6771
|
-
if (cur.property) {
|
|
6772
|
-
if (!cur.computed && cur.property.type === 'Identifier') {
|
|
6773
|
-
parts.unshift(`.${cur.property.name}`);
|
|
6774
|
-
}
|
|
6775
|
-
else if (cur.computed && cur.property.type === 'Literal' && typeof cur.property.value === 'number') {
|
|
6776
|
-
parts.unshift(`[${cur.property.value}]`);
|
|
6777
|
-
}
|
|
6778
|
-
else if (cur.computed && cur.property.type === 'Literal') {
|
|
6779
|
-
parts.unshift(`.${String(cur.property.value)}`);
|
|
6780
|
-
}
|
|
6781
|
-
else if (cur.computed) {
|
|
6782
|
-
// e.g. items[i] or items[i + 1]: the index is only known when the
|
|
6783
|
-
// expression runs, so "[*]" stands for any index or key. Identifiers
|
|
6784
|
-
// inside the index expression are visited separately by walk.full.
|
|
6785
|
-
parts.unshift('[*]');
|
|
6786
|
-
}
|
|
6787
|
-
}
|
|
6788
|
-
// object part (left side)
|
|
6789
|
-
if (cur.object) {
|
|
6790
|
-
if (cur.object.type === 'Identifier') {
|
|
6791
|
-
parts.unshift(cur.object.name);
|
|
6792
|
-
break;
|
|
6793
|
-
}
|
|
6794
|
-
else if (cur.object.type === 'ThisExpression') {
|
|
6795
|
-
// For `this.x` or `this.a.b` we don't want to include 'this' itself.
|
|
6796
|
-
// Stop unwrapping here; we'll drop a leading 'this' later.
|
|
6797
|
-
break;
|
|
6798
|
-
}
|
|
6799
|
-
else if (cur.object.type === 'MemberExpression') {
|
|
6800
|
-
// continue unwrapping
|
|
6801
|
-
cur = cur.object;
|
|
6802
|
-
}
|
|
6803
|
-
else {
|
|
6804
|
-
// other object types (CallExpression, etc.) — stop here
|
|
6805
|
-
break;
|
|
6806
|
-
}
|
|
6807
|
-
}
|
|
6808
|
-
else {
|
|
6809
|
-
break;
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
// A chain that does not start at a name (e.g. `this.a.b` or `f().b`) begins
|
|
6813
|
-
// with its first property; one that begins with an index names no binding.
|
|
6814
|
-
if (parts.length > 0 && parts[0].startsWith('.')) {
|
|
6815
|
-
parts[0] = parts[0].substring(1);
|
|
6816
|
-
}
|
|
6817
|
-
if (parts.length > 0 && !parts[0].startsWith('[')) {
|
|
6818
|
-
// Add progressive chains: 'a', 'a.b', 'a.b[0]', 'a.b[0].c'
|
|
6819
|
-
for (let i = 0; i < parts.length; i++) {
|
|
6820
|
-
const chain = parts.slice(0, i + 1).join('');
|
|
6821
|
-
identifiers.add(chain);
|
|
6822
|
-
// Also apply functionDependencies lookup for the chain key
|
|
6823
|
-
if (functionDependencies[chain]) {
|
|
6824
|
-
for (const dependency of functionDependencies[chain]) {
|
|
6825
|
-
identifiers.add(dependency);
|
|
6826
|
-
}
|
|
6827
|
-
}
|
|
6828
|
-
}
|
|
6829
|
-
}
|
|
6830
|
-
}
|
|
6831
|
-
});
|
|
6832
|
-
return Array.from(identifiers);
|
|
6833
|
-
}
|
|
6834
|
-
/**
|
|
6835
|
-
* Returns the binding name an identifier chain starts with
|
|
6836
|
-
* (e.g. "items" for "items[0].name" or "user.name").
|
|
6837
|
-
* @param identifier An identifier or member chain returned by extractIdentifiers.
|
|
6838
|
-
* @returns The left-most name of the chain.
|
|
6839
|
-
*/
|
|
6840
|
-
static baseIdentifier(identifier) {
|
|
6841
|
-
const end = identifier.search(/[.[]/);
|
|
6842
|
-
return end === -1 ? identifier : identifier.substring(0, end);
|
|
6843
|
-
}
|
|
6844
|
-
/**
|
|
6845
|
-
* Analyzes the dependencies of functions in the provided dictionary.
|
|
6846
|
-
* @param functions A dictionary mapping function names to their implementations.
|
|
6847
|
-
* @returns A dictionary mapping function names to arrays of their dependencies.
|
|
6848
|
-
*/
|
|
6849
|
-
static analyzeFunctionDependencies(functions) {
|
|
6850
|
-
const functionDependencies = {};
|
|
6851
|
-
// First pass: collect direct dependencies for each function
|
|
6852
|
-
for (const [funcName, func] of Object.entries(functions)) {
|
|
6853
|
-
try {
|
|
6854
|
-
let source = func.toString();
|
|
6855
|
-
// Handle different function formats
|
|
6856
|
-
// Arrow function: () => expr or (args) => { ... }
|
|
6857
|
-
// Method shorthand: methodName() { ... }
|
|
6858
|
-
// Function expression: function() { ... }
|
|
6859
|
-
// Detect function type more accurately
|
|
6860
|
-
const isArrowFunction = /^\s*(\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(source);
|
|
6861
|
-
const isFunctionExpression = source.startsWith('function');
|
|
6862
|
-
const isAsyncFunction = source.startsWith('async');
|
|
6863
|
-
// If it's a method shorthand (e.g., "methodName() { ... }" or "async methodName() { ... }"), convert to function expression
|
|
6864
|
-
if (!isFunctionExpression && !isArrowFunction) {
|
|
6865
|
-
// It's likely a method shorthand, convert to function expression
|
|
6866
|
-
if (isAsyncFunction) {
|
|
6867
|
-
// Remove 'async' prefix and add 'async function' prefix
|
|
6868
|
-
source = `async function ${source.substring(5).trim()}`;
|
|
6869
|
-
}
|
|
6870
|
-
else {
|
|
6871
|
-
source = `function ${source}`;
|
|
6872
|
-
}
|
|
6873
|
-
}
|
|
6874
|
-
// Wrap in parentheses for parsing
|
|
6875
|
-
source = `(${source})`;
|
|
6876
|
-
const ast = parse(source, { ecmaVersion: "latest" });
|
|
6877
|
-
const dependencies = new Set();
|
|
6878
|
-
const declaredVariables = new Set();
|
|
6879
|
-
const thisAliases = new Set();
|
|
6880
|
-
// First, collect all declared variables (const, let, var, function params, etc.)
|
|
6881
|
-
ancestor(ast, {
|
|
6882
|
-
VariableDeclarator(node) {
|
|
6883
|
-
if (node.id.type === 'Identifier') {
|
|
6884
|
-
declaredVariables.add(node.id.name);
|
|
6885
|
-
// Detect aliases assigned from `this`, e.g. `let vm = this;`
|
|
6886
|
-
if (node.init && node.init.type === 'ThisExpression') {
|
|
6887
|
-
thisAliases.add(node.id.name);
|
|
6888
|
-
}
|
|
6889
|
-
}
|
|
6890
|
-
},
|
|
6891
|
-
FunctionDeclaration(node) {
|
|
6892
|
-
if (node.id && node.id.type === 'Identifier') {
|
|
6893
|
-
declaredVariables.add(node.id.name);
|
|
6894
|
-
}
|
|
6895
|
-
// Add function parameters
|
|
6896
|
-
node.params.forEach((param) => {
|
|
6897
|
-
if (param.type === 'Identifier') {
|
|
6898
|
-
declaredVariables.add(param.name);
|
|
6899
|
-
}
|
|
6900
|
-
});
|
|
6901
|
-
},
|
|
6902
|
-
FunctionExpression(node, ancestors) {
|
|
6903
|
-
// Only process the outermost function (skip nested functions)
|
|
6904
|
-
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
6905
|
-
n.type === 'FunctionDeclaration' ||
|
|
6906
|
-
n.type === 'ArrowFunctionExpression');
|
|
6907
|
-
if (functionAncestors.length === 1) {
|
|
6908
|
-
// Add function parameters of the main function
|
|
6909
|
-
node.params.forEach((param) => {
|
|
6910
|
-
if (param.type === 'Identifier') {
|
|
6911
|
-
declaredVariables.add(param.name);
|
|
6912
|
-
}
|
|
6913
|
-
});
|
|
6914
|
-
}
|
|
6915
|
-
},
|
|
6916
|
-
ArrowFunctionExpression(node, ancestors) {
|
|
6917
|
-
// Only process the outermost function (skip nested functions)
|
|
6918
|
-
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
6919
|
-
n.type === 'FunctionDeclaration' ||
|
|
6920
|
-
n.type === 'ArrowFunctionExpression');
|
|
6921
|
-
if (functionAncestors.length === 1) {
|
|
6922
|
-
// Add function parameters of the main function
|
|
6923
|
-
node.params.forEach((param) => {
|
|
6924
|
-
if (param.type === 'Identifier') {
|
|
6925
|
-
declaredVariables.add(param.name);
|
|
6926
|
-
}
|
|
6927
|
-
});
|
|
6928
|
-
}
|
|
6929
|
-
}
|
|
6930
|
-
});
|
|
6931
|
-
// Then, collect identifiers that are not declared within the function
|
|
6932
|
-
simple(ast, {
|
|
6933
|
-
Identifier(node) {
|
|
6934
|
-
// Skip the function name itself, declared variables, and common globals
|
|
6935
|
-
if (node.name !== funcName && !declaredVariables.has(node.name)) {
|
|
6936
|
-
dependencies.add(node.name);
|
|
6937
|
-
}
|
|
6938
|
-
},
|
|
6939
|
-
MemberExpression(node) {
|
|
6940
|
-
// Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
|
|
6941
|
-
if (node.property.type === 'Identifier') {
|
|
6942
|
-
if (node.object.type === 'ThisExpression') {
|
|
6943
|
-
dependencies.add(node.property.name);
|
|
6944
|
-
}
|
|
6945
|
-
else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
|
|
6946
|
-
dependencies.add(node.property.name);
|
|
6947
|
-
}
|
|
6948
|
-
}
|
|
6949
|
-
}
|
|
6950
|
-
});
|
|
6951
|
-
functionDependencies[funcName] = Array.from(dependencies);
|
|
6952
|
-
}
|
|
6953
|
-
catch (error) {
|
|
6954
|
-
console.warn(`Failed to analyze function '${funcName}':`, error);
|
|
6955
|
-
// If parsing fails, assume no dependencies
|
|
6956
|
-
functionDependencies[funcName] = [];
|
|
6957
|
-
}
|
|
6958
|
-
}
|
|
6959
|
-
// Second pass: resolve each function's transitive reactive dependencies.
|
|
6960
|
-
//
|
|
6961
|
-
// A function's direct dependency list mixes two kinds of identifiers: references to other
|
|
6962
|
-
// functions (call edges) and references to reactive data properties (the values we actually
|
|
6963
|
-
// care about). We want, for every function, the full set of reactive properties reachable by
|
|
6964
|
-
// following call edges to any depth.
|
|
6965
|
-
//
|
|
6966
|
-
// Functions are free to reference each other cyclically — a poller that reschedules itself
|
|
6967
|
-
// (`scheduleOperationPoll` -> `pollOperation` -> `scheduleOperationPoll`), a pair of mutually
|
|
6968
|
-
// recursive helpers, or any state machine. This is valid JavaScript, so the resolver must not
|
|
6969
|
-
// treat it as an error. Every function in a cycle (more precisely, a strongly connected
|
|
6970
|
-
// component) is reachable from the others, so they all share the same transitive dependency
|
|
6971
|
-
// set: the union of every member's direct reactive dependencies.
|
|
6972
|
-
//
|
|
6973
|
-
// A depth-first walk that bails out on back-edges cannot compute that correctly — it drops the
|
|
6974
|
-
// dependencies contributed around the cycle and caches incomplete, traversal-order-dependent
|
|
6975
|
-
// results, which would silently break reactivity for any computed that reads reactive data
|
|
6976
|
-
// through a cyclic method. We instead split each function's direct dependencies into reactive
|
|
6977
|
-
// properties and call edges, then propagate reactive properties along call edges until the sets
|
|
6978
|
-
// stop growing (a fixpoint). This converges regardless of cycles and yields the exact transitive
|
|
6979
|
-
// closure for every function, including those participating in a cycle.
|
|
6980
|
-
const directReactive = {};
|
|
6981
|
-
const callEdges = {};
|
|
6982
|
-
for (const funcName of Object.keys(functions)) {
|
|
6983
|
-
const reactive = new Set();
|
|
6984
|
-
const edges = [];
|
|
6985
|
-
for (const dep of functionDependencies[funcName] || []) {
|
|
6986
|
-
if (dep === funcName)
|
|
6987
|
-
continue; // Skip self-references.
|
|
6988
|
-
if (functions[dep]) {
|
|
6989
|
-
edges.push(dep); // Call edge to another function.
|
|
6990
|
-
}
|
|
6991
|
-
else {
|
|
6992
|
-
reactive.add(dep); // Reactive data property (terminal dependency).
|
|
6993
|
-
}
|
|
6994
|
-
}
|
|
6995
|
-
directReactive[funcName] = reactive;
|
|
6996
|
-
callEdges[funcName] = edges;
|
|
6997
|
-
}
|
|
6998
|
-
// Seed each function with its own direct reactive dependencies, then propagate along call edges
|
|
6999
|
-
// until no set changes. Each pass can only add properties, and the universe of properties is
|
|
7000
|
-
// finite, so the loop is guaranteed to terminate.
|
|
7001
|
-
const resolved = {};
|
|
7002
|
-
for (const funcName of Object.keys(functions)) {
|
|
7003
|
-
resolved[funcName] = new Set(directReactive[funcName]);
|
|
7004
|
-
}
|
|
7005
|
-
let changed = true;
|
|
7006
|
-
while (changed) {
|
|
7007
|
-
changed = false;
|
|
7008
|
-
for (const funcName of Object.keys(functions)) {
|
|
7009
|
-
const target = resolved[funcName];
|
|
7010
|
-
for (const edge of callEdges[funcName]) {
|
|
7011
|
-
for (const dep of resolved[edge]) {
|
|
7012
|
-
if (!target.has(dep)) {
|
|
7013
|
-
target.add(dep);
|
|
7014
|
-
changed = true;
|
|
7015
|
-
}
|
|
7016
|
-
}
|
|
7017
|
-
}
|
|
7018
|
-
}
|
|
7019
|
-
}
|
|
7020
|
-
const resolvedDependencies = {};
|
|
7021
|
-
for (const funcName of Object.keys(functions)) {
|
|
7022
|
-
resolvedDependencies[funcName] = Array.from(resolved[funcName]);
|
|
7023
|
-
}
|
|
7024
|
-
return resolvedDependencies;
|
|
7025
|
-
}
|
|
7026
|
-
/**
|
|
7027
|
-
* Rewrites an expression to replace identifiers with 'this.identifier'.
|
|
7028
|
-
* This allows direct property access and assignment without using 'with' statement.
|
|
7029
|
-
* Uses AST parsing to accurately identify which identifiers to replace.
|
|
7030
|
-
* @param expression The original expression string.
|
|
7031
|
-
* @param identifiers The list of identifiers that are available in bindings.
|
|
7032
|
-
* @returns The rewritten expression.
|
|
7033
|
-
*/
|
|
7034
|
-
static rewriteExpression(expression, identifiers, options) {
|
|
7035
|
-
// Reserved words and built-in objects that should not be prefixed with 'this.'
|
|
7036
|
-
const reserved = new Set([
|
|
7037
|
-
'event', '$event', '$ctx', '$newValue',
|
|
7038
|
-
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
7039
|
-
'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
|
|
7040
|
-
'JSON', 'console', 'window', 'document', 'navigator',
|
|
7041
|
-
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
7042
|
-
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent'
|
|
7043
|
-
]);
|
|
7044
|
-
// Extract ALL identifiers from the expression (including assignment left-hand side)
|
|
7045
|
-
// This is necessary because the passed 'identifiers' parameter only includes
|
|
7046
|
-
// identifiers that are used (right-hand side), not assigned to (left-hand side)
|
|
7047
|
-
let allIdentifiersInExpression;
|
|
7048
|
-
try {
|
|
7049
|
-
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
|
|
7050
|
-
}
|
|
7051
|
-
catch (error) {
|
|
7052
|
-
console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
|
|
7053
|
-
return expression;
|
|
7054
|
-
}
|
|
7055
|
-
// Create a Set of identifiers available in bindings (from data, computed, methods)
|
|
7056
|
-
// We need to know which identifiers are valid binding properties
|
|
7057
|
-
const bindingIdentifiers = new Set(identifiers.filter(id => !reserved.has(id)));
|
|
7058
|
-
// For assignment expressions, we also need to include the left-hand side identifier
|
|
7059
|
-
// even if it's not in the tracking identifiers (because it's being assigned, not read)
|
|
7060
|
-
for (const id of allIdentifiersInExpression) {
|
|
7061
|
-
if (!reserved.has(id)) {
|
|
7062
|
-
bindingIdentifiers.add(id);
|
|
7063
|
-
}
|
|
7064
|
-
}
|
|
7065
|
-
if (bindingIdentifiers.size === 0) {
|
|
7066
|
-
return expression;
|
|
7067
|
-
}
|
|
7068
|
-
try {
|
|
7069
|
-
// Build a map of positions to replace: { start: number, end: number, name: string, asThisAlias?: boolean }[]
|
|
7070
|
-
const replacements = [];
|
|
7071
|
-
// In script mode we must not wrap in parens (that would make multi-statement input invalid).
|
|
7072
|
-
// Offsets from the parser therefore refer directly to the original expression, so no shift.
|
|
7073
|
-
const asScript = options?.asScript === true;
|
|
7074
|
-
const source = asScript ? expression : `(${expression})`;
|
|
7075
|
-
const offsetShift = asScript ? 0 : 1;
|
|
7076
|
-
const parsedAst = parse(source, { ecmaVersion: 'latest' });
|
|
7077
|
-
// Track identifiers that are locally declared within the handler body (let/const/var, function
|
|
7078
|
-
// params) so we don't rewrite them to `this.xxx`. Additionally detect `this` aliases such as
|
|
7079
|
-
// `let vm = this;` so that `vm.x` can be rewritten to `this.x` even though `vm` is locally declared.
|
|
7080
|
-
const locallyDeclared = new Set();
|
|
7081
|
-
const thisAliases = new Set();
|
|
7082
|
-
if (asScript) {
|
|
7083
|
-
full(parsedAst, (node) => {
|
|
7084
|
-
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7085
|
-
locallyDeclared.add(node.id.name);
|
|
7086
|
-
if (node.init && node.init.type === 'ThisExpression') {
|
|
7087
|
-
thisAliases.add(node.id.name);
|
|
7088
|
-
}
|
|
7089
|
-
}
|
|
7090
|
-
else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
7091
|
-
locallyDeclared.add(node.id.name);
|
|
7092
|
-
}
|
|
7093
|
-
});
|
|
7094
|
-
}
|
|
7095
|
-
// Collect all identifier nodes that should be replaced
|
|
7096
|
-
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7097
|
-
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
7098
|
-
if (node.type !== 'Identifier') {
|
|
7099
|
-
return;
|
|
7100
|
-
}
|
|
7101
|
-
// Skip if not in our identifier set
|
|
7102
|
-
if (!bindingIdentifiers.has(node.name)) {
|
|
7103
|
-
return;
|
|
7104
|
-
}
|
|
7105
|
-
// Locally declared identifiers must not be rewritten to `this.xxx`. The one
|
|
7106
|
-
// exception is a `this`-alias (e.g. `let vm = this;`): when such an alias is
|
|
7107
|
-
// used as the OBJECT of a MemberExpression (`vm.x`), we replace just the
|
|
7108
|
-
// object with `this` so that `vm.x` becomes `this.x`. In every other position
|
|
7109
|
-
// (the declaration LHS, a bare reference such as `console.log(vm)`, an
|
|
7110
|
-
// argument, etc.) the alias must be left untouched — rewriting it to
|
|
7111
|
-
// `this.vm` would either produce a syntax error (`let this.vm = this;`) or
|
|
7112
|
-
// change the meaning at runtime.
|
|
7113
|
-
// walk.fullAncestor includes the visited node itself as the last entry of
|
|
7114
|
-
// `ancestors`, so the actual parent node is at length - 2.
|
|
7115
|
-
const parentNode = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
|
|
7116
|
-
const isAliasAsMemberObject = parentNode?.type === 'MemberExpression' && parentNode.object === node;
|
|
7117
|
-
if (locallyDeclared.has(node.name)) {
|
|
7118
|
-
if (!thisAliases.has(node.name) || !isAliasAsMemberObject) {
|
|
7119
|
-
return;
|
|
7120
|
-
}
|
|
7121
|
-
replacements.push({
|
|
7122
|
-
start: node.start - offsetShift,
|
|
7123
|
-
end: node.end - offsetShift,
|
|
7124
|
-
name: node.name,
|
|
7125
|
-
asThisAlias: true
|
|
7126
|
-
});
|
|
7127
|
-
return;
|
|
7128
|
-
}
|
|
7129
|
-
// Skip identifiers that are the property (not the object) of a non-computed
|
|
7130
|
-
// member access (e.g., the `prop` in `obj.prop`).
|
|
7131
|
-
if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
|
|
7132
|
-
return;
|
|
7133
|
-
}
|
|
7134
|
-
// Add to replacements list (adjust for the wrapping parentheses in expression mode)
|
|
7135
|
-
replacements.push({
|
|
7136
|
-
start: node.start - offsetShift,
|
|
7137
|
-
end: node.end - offsetShift,
|
|
7138
|
-
name: node.name
|
|
7139
|
-
});
|
|
7140
|
-
});
|
|
7141
|
-
// Sort replacements by start position (descending) to replace from end to start
|
|
7142
|
-
replacements.sort((a, b) => b.start - a.start);
|
|
7143
|
-
// Apply replacements
|
|
7144
|
-
let result = expression;
|
|
7145
|
-
for (const replacement of replacements) {
|
|
7146
|
-
const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
|
|
7147
|
-
result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
|
|
7148
|
-
}
|
|
7149
|
-
return result;
|
|
7150
|
-
}
|
|
7151
|
-
catch (error) {
|
|
7152
|
-
// If AST parsing fails, fall back to the original expression
|
|
7153
|
-
console.warn('Failed to rewrite expression:', expression, error);
|
|
7154
|
-
return expression;
|
|
7155
|
-
}
|
|
7156
|
-
}
|
|
7157
|
-
}
|
|
7158
|
-
|
|
7159
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7160
|
-
/**
|
|
7161
|
-
* A centralized expression evaluator that normalizes expression evaluation across all directives.
|
|
7162
|
-
*
|
|
7163
|
-
* This class provides:
|
|
7164
|
-
* - Consistent identifier extraction and binding resolution
|
|
7165
|
-
* - Unified global object whitelisting (Math, Date, JSON, etc.)
|
|
7166
|
-
* - Expression caching for performance
|
|
7167
|
-
* - Centralized error handling
|
|
7168
|
-
* - Support for custom expression rewriting (for VOnDirective)
|
|
7169
|
-
*
|
|
7170
|
-
* Example usage:
|
|
7171
|
-
* ```typescript
|
|
7172
|
-
* const evaluator = ExpressionEvaluator.create(
|
|
7173
|
-
* "count + 1",
|
|
7174
|
-
* bindings,
|
|
7175
|
-
* functionDependencies
|
|
7176
|
-
* );
|
|
7177
|
-
* const result = evaluator.evaluate();
|
|
7178
|
-
* ```
|
|
6731
|
+
* Utility class for creating reactive proxies that automatically track changes.
|
|
7179
6732
|
*/
|
|
7180
|
-
class
|
|
7181
|
-
/**
|
|
7182
|
-
* Cache for compiled expression functions.
|
|
7183
|
-
* Key: expression string
|
|
7184
|
-
* Value: compiled function
|
|
7185
|
-
*/
|
|
7186
|
-
static cache = new Map();
|
|
7187
|
-
/**
|
|
7188
|
-
* Global objects that are whitelisted for use in expressions.
|
|
7189
|
-
* These objects are available to all expressions for convenience and compatibility.
|
|
7190
|
-
*/
|
|
7191
|
-
static GLOBAL_WHITELIST = {
|
|
7192
|
-
Math,
|
|
7193
|
-
Date,
|
|
7194
|
-
JSON,
|
|
7195
|
-
Array,
|
|
7196
|
-
Object,
|
|
7197
|
-
String,
|
|
7198
|
-
Number,
|
|
7199
|
-
Boolean,
|
|
7200
|
-
parseInt,
|
|
7201
|
-
parseFloat,
|
|
7202
|
-
isNaN,
|
|
7203
|
-
isFinite,
|
|
7204
|
-
encodeURI,
|
|
7205
|
-
decodeURI,
|
|
7206
|
-
encodeURIComponent,
|
|
7207
|
-
decodeURIComponent
|
|
7208
|
-
};
|
|
7209
|
-
/**
|
|
7210
|
-
* The original expression string.
|
|
7211
|
-
*/
|
|
7212
|
-
expression;
|
|
7213
|
-
/**
|
|
7214
|
-
* The identifiers extracted from the expression.
|
|
7215
|
-
*/
|
|
7216
|
-
identifiers;
|
|
7217
|
-
// The original extracted identifiers (may include member chains like 'a.b')
|
|
7218
|
-
originalIdentifiers;
|
|
7219
|
-
/**
|
|
7220
|
-
* The bindings to use for evaluating the expression.
|
|
7221
|
-
*/
|
|
7222
|
-
bindings;
|
|
7223
|
-
/**
|
|
7224
|
-
* The compiled function for evaluating the expression.
|
|
7225
|
-
*/
|
|
7226
|
-
evaluatorFunc;
|
|
7227
|
-
/**
|
|
7228
|
-
* Additional context variables (e.g., 'event', '$ctx').
|
|
7229
|
-
*/
|
|
7230
|
-
additionalContext;
|
|
7231
|
-
/**
|
|
7232
|
-
* Private constructor. Use ExpressionEvaluator.create() to create instances.
|
|
7233
|
-
*/
|
|
7234
|
-
constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
|
|
7235
|
-
this.expression = expression;
|
|
7236
|
-
this.bindings = bindings;
|
|
7237
|
-
this.identifiers = identifiers;
|
|
7238
|
-
this.originalIdentifiers = originalIdentifiers;
|
|
7239
|
-
this.evaluatorFunc = evaluatorFunc;
|
|
7240
|
-
this.additionalContext = additionalContext;
|
|
7241
|
-
}
|
|
7242
|
-
/**
|
|
7243
|
-
* Creates an ExpressionEvaluator instance.
|
|
7244
|
-
*
|
|
7245
|
-
* @param expression The expression string to evaluate.
|
|
7246
|
-
* @param bindings The bindings to use for evaluating the expression.
|
|
7247
|
-
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
7248
|
-
* @param options Optional configuration for the evaluator.
|
|
7249
|
-
* @returns An ExpressionEvaluator instance.
|
|
7250
|
-
*/
|
|
7251
|
-
static create(expression, bindings, functionDependencies, options) {
|
|
7252
|
-
// Extract identifiers from the expression (may include member chains like 'a.b')
|
|
7253
|
-
const extractedIdentifiers = ExpressionUtils.extractIdentifiers(expression, functionDependencies);
|
|
7254
|
-
// For compilation we must only pass simple identifier names (no dots).
|
|
7255
|
-
// Use the base (left-most) segment of member chains and preserve order.
|
|
7256
|
-
const baseIdentifiers = [];
|
|
7257
|
-
for (const id of extractedIdentifiers) {
|
|
7258
|
-
const base = ExpressionUtils.baseIdentifier(id);
|
|
7259
|
-
if (!baseIdentifiers.includes(base)) {
|
|
7260
|
-
baseIdentifiers.push(base);
|
|
7261
|
-
}
|
|
7262
|
-
}
|
|
7263
|
-
// Apply custom rewrite if provided (used by VOnDirective)
|
|
7264
|
-
let processedExpression = expression;
|
|
7265
|
-
if (options?.rewriteExpression) {
|
|
7266
|
-
// Pass the original extracted identifiers (may include member chains)
|
|
7267
|
-
processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
|
|
7268
|
-
}
|
|
7269
|
-
// Build cache key including options that affect compilation
|
|
7270
|
-
const cacheKey = JSON.stringify({
|
|
7271
|
-
expr: processedExpression,
|
|
7272
|
-
additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
|
|
7273
|
-
});
|
|
7274
|
-
// Check cache
|
|
7275
|
-
let evaluatorFunc = this.cache.get(cacheKey);
|
|
7276
|
-
if (!evaluatorFunc) {
|
|
7277
|
-
// Compile the expression using base identifiers (no dots)
|
|
7278
|
-
evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
|
|
7279
|
-
// Cache the compiled function
|
|
7280
|
-
this.cache.set(cacheKey, evaluatorFunc);
|
|
7281
|
-
}
|
|
7282
|
-
return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
|
|
7283
|
-
}
|
|
6733
|
+
class ReactiveProxy {
|
|
7284
6734
|
/**
|
|
7285
|
-
*
|
|
7286
|
-
*
|
|
7287
|
-
* @param expression The expression string to compile.
|
|
7288
|
-
* @param identifiers The identifiers extracted from the expression.
|
|
7289
|
-
* @param options Optional configuration.
|
|
7290
|
-
* @returns A compiled function.
|
|
6735
|
+
* A WeakMap to store the proxy for each target object and path combination.
|
|
6736
|
+
* This prevents creating multiple proxies for the same object accessed from different paths.
|
|
7291
6737
|
*/
|
|
7292
|
-
static
|
|
7293
|
-
// Build parameter list: globals first, then identifiers (base names), then additional context
|
|
7294
|
-
// Filter out identifiers that are already in the global whitelist to avoid duplicate parameters
|
|
7295
|
-
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7296
|
-
const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
|
|
7297
|
-
const params = [
|
|
7298
|
-
...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7299
|
-
...filteredIdentifiers
|
|
7300
|
-
];
|
|
7301
|
-
// Add additional context parameters if provided
|
|
7302
|
-
if (options?.additionalContext) {
|
|
7303
|
-
params.push(...Object.keys(options.additionalContext));
|
|
7304
|
-
}
|
|
7305
|
-
const args = params.join(", ");
|
|
7306
|
-
const funcBody = `return (${expression});`;
|
|
7307
|
-
try {
|
|
7308
|
-
// Create the function with error handling
|
|
7309
|
-
return new Function(args, funcBody);
|
|
7310
|
-
}
|
|
7311
|
-
catch (error) {
|
|
7312
|
-
throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
|
|
7313
|
-
}
|
|
7314
|
-
}
|
|
6738
|
+
static proxyCache = new WeakMap();
|
|
7315
6739
|
/**
|
|
7316
|
-
*
|
|
7317
|
-
*
|
|
7318
|
-
* @returns The result of the expression evaluation.
|
|
6740
|
+
* A WeakMap to track which objects are proxies, mapping proxy -> original target.
|
|
6741
|
+
* This prevents double-wrapping of already proxied objects.
|
|
7319
6742
|
*/
|
|
7320
|
-
|
|
7321
|
-
try {
|
|
7322
|
-
// Build arguments: globals first, then identifiers, then additional context
|
|
7323
|
-
// Filter out identifiers that are already in the global whitelist to match compileExpression
|
|
7324
|
-
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7325
|
-
const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
|
|
7326
|
-
const values = [
|
|
7327
|
-
// Global whitelist values
|
|
7328
|
-
...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7329
|
-
// Identifier values from bindings (excluding globals)
|
|
7330
|
-
...filteredIdentifiers.map(id => this.bindings.get(id))
|
|
7331
|
-
];
|
|
7332
|
-
// Add additional context values if provided
|
|
7333
|
-
if (this.additionalContext) {
|
|
7334
|
-
values.push(...Object.values(this.additionalContext));
|
|
7335
|
-
}
|
|
7336
|
-
// Call the compiled function with the gathered values
|
|
7337
|
-
return this.evaluatorFunc(...values);
|
|
7338
|
-
}
|
|
7339
|
-
catch (error) {
|
|
7340
|
-
console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
|
|
7341
|
-
throw error;
|
|
7342
|
-
}
|
|
7343
|
-
}
|
|
6743
|
+
static proxyToTarget = new WeakMap();
|
|
7344
6744
|
/**
|
|
7345
|
-
*
|
|
7346
|
-
*
|
|
7347
|
-
* @returns The boolean result of the expression evaluation.
|
|
6745
|
+
* A WeakSet to track objects marked as "raw" (non-reactive).
|
|
6746
|
+
* These objects will not be wrapped with Proxy.
|
|
7348
6747
|
*/
|
|
7349
|
-
|
|
7350
|
-
return Boolean(this.evaluate());
|
|
7351
|
-
}
|
|
6748
|
+
static rawObjects = new WeakSet();
|
|
7352
6749
|
/**
|
|
7353
|
-
*
|
|
6750
|
+
* A WeakMap to store the path for each proxy object.
|
|
6751
|
+
* This allows retrieving the source path of an object for computed property mapping.
|
|
7354
6752
|
*/
|
|
7355
|
-
|
|
7356
|
-
// Return the original extracted identifiers (may include member chains like 'a.b')
|
|
7357
|
-
return this.originalIdentifiers;
|
|
7358
|
-
}
|
|
6753
|
+
static proxyPaths = new WeakMap();
|
|
7359
6754
|
/**
|
|
7360
|
-
*
|
|
7361
|
-
*
|
|
6755
|
+
* Dispatchers per (target, path). Every target reached while walking a proxy
|
|
6756
|
+
* subtree registers an entry pointing at the same dispatcher as the outermost
|
|
6757
|
+
* proxy of that subtree, so callers can look up the dispatcher from any
|
|
6758
|
+
* intermediate proxy when subscribing.
|
|
7362
6759
|
*/
|
|
7363
|
-
static
|
|
7364
|
-
this.cache.clear();
|
|
7365
|
-
}
|
|
7366
|
-
}
|
|
7367
|
-
|
|
7368
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7369
|
-
/**
|
|
7370
|
-
* Directive for binding dynamic attributes to DOM elements.
|
|
7371
|
-
* The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
|
|
7372
|
-
* 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.
|
|
7373
|
-
* Example usage:
|
|
7374
|
-
* <img v-bind:src="imageSrc" />
|
|
7375
|
-
* The value of `imageSrc` should be defined in the component's data object.
|
|
7376
|
-
* When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
|
|
7377
|
-
* This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
|
|
7378
|
-
* It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
|
|
7379
|
-
* The `v-bind` directive can also be used with shorthand syntax using a colon (:).
|
|
7380
|
-
* For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
|
|
7381
|
-
* This shorthand syntax is often used for brevity and improved readability in templates.
|
|
7382
|
-
* 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.
|
|
7383
|
-
*/
|
|
7384
|
-
class VBindDirective {
|
|
6760
|
+
static dispatchers = new WeakMap();
|
|
7385
6761
|
/**
|
|
7386
|
-
*
|
|
6762
|
+
* A Map to store path aliases.
|
|
6763
|
+
* Key: alias path (e.g., "editingNestedStep.steps")
|
|
6764
|
+
* Value: source path (e.g., "routes[0].steps[0].steps")
|
|
6765
|
+
* This allows mapping variable names to their actual source paths for dependency tracking.
|
|
7387
6766
|
*/
|
|
7388
|
-
|
|
6767
|
+
static pathAliases = new Map();
|
|
7389
6768
|
/**
|
|
7390
|
-
*
|
|
6769
|
+
* Compiled patterns for identifiers that contain a "[*]" index, keyed by the
|
|
6770
|
+
* identifier and whether descendants of the path also match.
|
|
7391
6771
|
*/
|
|
7392
|
-
|
|
6772
|
+
static pathPatterns = new Map();
|
|
7393
6773
|
/**
|
|
7394
|
-
* The
|
|
6774
|
+
* The set collecting the paths read through any proxy while track() runs, or
|
|
6775
|
+
* undefined when no expression is being tracked.
|
|
7395
6776
|
*/
|
|
7396
|
-
|
|
6777
|
+
static tracker;
|
|
7397
6778
|
/**
|
|
7398
|
-
*
|
|
6779
|
+
* Whether a property key is spelled as an array index ("0", "12") rather than a
|
|
6780
|
+
* property name. Both the proxy and the expression analyzer decide between
|
|
6781
|
+
* "items[0]" and "items.name" with this test alone, so a numeric key on a plain
|
|
6782
|
+
* object and a numeric key on an array get the same spelling.
|
|
6783
|
+
*
|
|
6784
|
+
* @param key The property key as a string.
|
|
6785
|
+
* @returns True if the key is a canonical non-negative integer.
|
|
7399
6786
|
*/
|
|
7400
|
-
|
|
6787
|
+
static isIndexKey(key) {
|
|
6788
|
+
return /^(?:0|[1-9]\d*)$/.test(key);
|
|
6789
|
+
}
|
|
7401
6790
|
/**
|
|
7402
|
-
*
|
|
7403
|
-
*
|
|
6791
|
+
* Appends a property key to a source path the way change paths are reported:
|
|
6792
|
+
* an index in brackets ("items[0]"), a name after a dot ("user.name").
|
|
6793
|
+
*
|
|
6794
|
+
* @param path The path of the containing object, or "" for a root binding.
|
|
6795
|
+
* @param key The property key being appended.
|
|
6796
|
+
* @returns The joined path.
|
|
7404
6797
|
*/
|
|
7405
|
-
|
|
6798
|
+
static joinPath(path, key) {
|
|
6799
|
+
const keyStr = String(key);
|
|
6800
|
+
if (!path) {
|
|
6801
|
+
return keyStr;
|
|
6802
|
+
}
|
|
6803
|
+
return this.isIndexKey(keyStr) ? `${path}[${keyStr}]` : `${path}.${keyStr}`;
|
|
6804
|
+
}
|
|
7406
6805
|
/**
|
|
7407
|
-
*
|
|
7408
|
-
*
|
|
6806
|
+
* Runs fn while recording, into paths, the source path of every property read
|
|
6807
|
+
* through a reactive proxy. Nested calls record into the innermost set only;
|
|
6808
|
+
* the previous set is restored when fn returns or throws.
|
|
6809
|
+
*
|
|
6810
|
+
* @param paths The set that receives the paths read.
|
|
6811
|
+
* @param fn The function to run.
|
|
6812
|
+
* @returns The value fn returns.
|
|
7409
6813
|
*/
|
|
7410
|
-
|
|
6814
|
+
static track(paths, fn) {
|
|
6815
|
+
const previous = this.tracker;
|
|
6816
|
+
this.tracker = paths;
|
|
6817
|
+
try {
|
|
6818
|
+
return fn();
|
|
6819
|
+
}
|
|
6820
|
+
finally {
|
|
6821
|
+
this.tracker = previous;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
7411
6824
|
/**
|
|
7412
|
-
*
|
|
6825
|
+
* The `Object.prototype.toString` tags of the *only* value types we deeply
|
|
6826
|
+
* proxy. Everything else is returned untouched (raw).
|
|
6827
|
+
*
|
|
6828
|
+
* Rationale: a Proxy can only safely intercept plain data containers. Built-in
|
|
6829
|
+
* and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
|
|
6830
|
+
* `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
|
|
6831
|
+
* slots, and invoking their native methods with a Proxy as the `this`/receiver
|
|
6832
|
+
* throws "Illegal invocation". They must therefore never be wrapped. This is an
|
|
6833
|
+
* allow-list rather than a deny-list so that *every* such exotic type is
|
|
6834
|
+
* excluded by default, not just the handful we happened to enumerate.
|
|
6835
|
+
*
|
|
6836
|
+
* Plain user-defined class instances report `[object Object]` (unless they set
|
|
6837
|
+
* `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
|
|
6838
|
+
* `Map`/`Set` stay reactive because the proxy specially wraps their mutation
|
|
6839
|
+
* methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
|
|
6840
|
+
* since they cannot be wrapped that way and would break the same as `File`.
|
|
6841
|
+
*
|
|
6842
|
+
* Callers that want a plain object kept non-reactive can still opt out
|
|
6843
|
+
* explicitly via {@link markRaw}.
|
|
7413
6844
|
*/
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
6845
|
+
static REACTIVE_TYPE_TAGS = new Set([
|
|
6846
|
+
'[object Object]',
|
|
6847
|
+
'[object Array]',
|
|
6848
|
+
'[object Arguments]',
|
|
6849
|
+
'[object Map]',
|
|
6850
|
+
'[object Set]',
|
|
6851
|
+
]);
|
|
6852
|
+
/**
|
|
6853
|
+
* Whether the given non-null object value is one we deeply proxy.
|
|
6854
|
+
* See {@link REACTIVE_TYPE_TAGS} for the reasoning.
|
|
6855
|
+
*/
|
|
6856
|
+
static isReactivable(value) {
|
|
6857
|
+
return this.REACTIVE_TYPE_TAGS.has(Object.prototype.toString.call(value));
|
|
6858
|
+
}
|
|
6859
|
+
/**
|
|
6860
|
+
* Creates a reactive proxy for the given object.
|
|
6861
|
+
* The proxy will call the onChange callback whenever a property is modified.
|
|
6862
|
+
*
|
|
6863
|
+
* @param target The object to make reactive.
|
|
6864
|
+
* @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
|
|
6865
|
+
* @param path The current path in the object tree (used internally for nested objects).
|
|
6866
|
+
* @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
|
|
6867
|
+
* @returns A reactive proxy of the target object.
|
|
6868
|
+
*/
|
|
6869
|
+
static create(target, onChange, path = '', inheritedDispatcher) {
|
|
6870
|
+
// If the target is not an object or is null, return it as-is
|
|
6871
|
+
if (typeof target !== 'object' || target === null) {
|
|
6872
|
+
return target;
|
|
7421
6873
|
}
|
|
7422
|
-
|
|
7423
|
-
|
|
6874
|
+
// Don't wrap objects marked as raw (non-reactive)
|
|
6875
|
+
if (this.rawObjects.has(target)) {
|
|
6876
|
+
return target;
|
|
7424
6877
|
}
|
|
7425
|
-
//
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
6878
|
+
// Only deeply proxy plain data containers; return every built-in/host
|
|
6879
|
+
// object (Date, File, Blob, Promise, WeakMap, DOM nodes, …) as-is, since
|
|
6880
|
+
// their native methods break when invoked through a Proxy receiver.
|
|
6881
|
+
if (!this.isReactivable(target)) {
|
|
6882
|
+
return target;
|
|
6883
|
+
}
|
|
6884
|
+
// Check if the target is already a proxy - if so, return it as-is to prevent double-wrapping
|
|
6885
|
+
if (this.proxyToTarget.has(target)) {
|
|
6886
|
+
return target;
|
|
6887
|
+
}
|
|
6888
|
+
// Resolve (and register) the dispatcher for this (target, path).
|
|
6889
|
+
// Nested create() calls inherit the dispatcher from the enclosing call so
|
|
6890
|
+
// that a single dispatcher fans out changes at any depth of the subtree.
|
|
6891
|
+
const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
|
|
6892
|
+
if (onChange) {
|
|
6893
|
+
dispatcher.listeners.add(onChange);
|
|
6894
|
+
}
|
|
6895
|
+
// Check if we already have a proxy for this target with this path
|
|
6896
|
+
let pathMap = this.proxyCache.get(target);
|
|
6897
|
+
if (pathMap) {
|
|
6898
|
+
const existingProxy = pathMap.get(path);
|
|
6899
|
+
if (existingProxy) {
|
|
6900
|
+
return existingProxy;
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
else {
|
|
6904
|
+
pathMap = new Map();
|
|
6905
|
+
this.proxyCache.set(target, pathMap);
|
|
6906
|
+
}
|
|
6907
|
+
// Create the proxy with path captured in closure
|
|
6908
|
+
const proxy = new Proxy(target, {
|
|
6909
|
+
get(obj, key, receiver) {
|
|
6910
|
+
const value = Reflect.get(obj, key);
|
|
6911
|
+
// Record the read for an expression being tracked (see track()).
|
|
6912
|
+
// Function-valued members (push, Map#get, …) are accesses to behaviour,
|
|
6913
|
+
// not data, so they are left out.
|
|
6914
|
+
if (ReactiveProxy.tracker && typeof key !== 'symbol' && typeof value !== 'function') {
|
|
6915
|
+
ReactiveProxy.tracker.add(ReactiveProxy.joinPath(path, key));
|
|
6916
|
+
}
|
|
6917
|
+
// If the value is an object or array, make it reactive too
|
|
6918
|
+
if (typeof value === 'object' && value !== null) {
|
|
6919
|
+
// Don't wrap objects marked as raw (non-reactive)
|
|
6920
|
+
if (ReactiveProxy.rawObjects.has(value)) {
|
|
6921
|
+
return value;
|
|
6922
|
+
}
|
|
6923
|
+
// Only deeply proxy plain data containers; hand back built-in
|
|
6924
|
+
// and host objects (Date, File, Blob, Promise, WeakMap, DOM
|
|
6925
|
+
// nodes, …) untouched so their native methods keep working.
|
|
6926
|
+
if (!ReactiveProxy.isReactivable(value)) {
|
|
6927
|
+
return value;
|
|
6928
|
+
}
|
|
6929
|
+
// Build the nested path
|
|
6930
|
+
const nestedPath = ReactiveProxy.joinPath(path, key);
|
|
6931
|
+
return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
|
|
6932
|
+
}
|
|
6933
|
+
// If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
|
|
6934
|
+
if (typeof value === 'function') {
|
|
6935
|
+
// For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
|
|
6936
|
+
if (Array.isArray(obj)) {
|
|
6937
|
+
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
6938
|
+
if (!arrayMutationMethods.includes(key)) {
|
|
6939
|
+
return value;
|
|
6940
|
+
}
|
|
6941
|
+
return function (...args) {
|
|
6942
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6943
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6944
|
+
return result;
|
|
6945
|
+
};
|
|
6946
|
+
}
|
|
6947
|
+
// For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
|
|
6948
|
+
if (obj.constructor.name === 'Map') {
|
|
6949
|
+
const mapMutationMethods = ['set', 'delete', 'clear'];
|
|
6950
|
+
return function (...args) {
|
|
6951
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6952
|
+
if (mapMutationMethods.includes(key)) {
|
|
6953
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6954
|
+
}
|
|
6955
|
+
return result;
|
|
6956
|
+
};
|
|
6957
|
+
}
|
|
6958
|
+
// For Set, we only want to wrap mutation methods, not read methods like 'has'
|
|
6959
|
+
if (obj.constructor.name === 'Set') {
|
|
6960
|
+
const setMutationMethods = ['add', 'delete', 'clear'];
|
|
6961
|
+
return function (...args) {
|
|
6962
|
+
const result = value.apply(this === receiver ? obj : this, args);
|
|
6963
|
+
if (setMutationMethods.includes(key)) {
|
|
6964
|
+
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
6965
|
+
}
|
|
6966
|
+
return result;
|
|
6967
|
+
};
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
return value;
|
|
6971
|
+
},
|
|
6972
|
+
set(obj, key, value) {
|
|
6973
|
+
const oldValue = Reflect.get(obj, key);
|
|
6974
|
+
const result = Reflect.set(obj, key, value);
|
|
6975
|
+
// Only trigger onChange if the value actually changed
|
|
6976
|
+
if (oldValue !== value) {
|
|
6977
|
+
ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
|
|
6978
|
+
}
|
|
6979
|
+
return result;
|
|
6980
|
+
},
|
|
6981
|
+
deleteProperty(obj, key) {
|
|
6982
|
+
const result = Reflect.deleteProperty(obj, key);
|
|
6983
|
+
ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
|
|
6984
|
+
return result;
|
|
6985
|
+
}
|
|
6986
|
+
});
|
|
6987
|
+
// Cache the proxy for this path
|
|
6988
|
+
pathMap.set(path, proxy);
|
|
6989
|
+
// Track that this proxy wraps the target to prevent double-wrapping
|
|
6990
|
+
this.proxyToTarget.set(proxy, target);
|
|
6991
|
+
// Store the path for this proxy (for computed property mapping)
|
|
6992
|
+
if (path) {
|
|
6993
|
+
this.proxyPaths.set(proxy, path);
|
|
7429
6994
|
}
|
|
7430
|
-
|
|
7431
|
-
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
7432
|
-
}
|
|
7433
|
-
/**
|
|
7434
|
-
* @inheritdoc
|
|
7435
|
-
*/
|
|
7436
|
-
get name() {
|
|
7437
|
-
return StandardDirectiveName.V_BIND;
|
|
7438
|
-
}
|
|
7439
|
-
/**
|
|
7440
|
-
* @inheritdoc
|
|
7441
|
-
*/
|
|
7442
|
-
get vNode() {
|
|
7443
|
-
return this.#vNode;
|
|
6995
|
+
return proxy;
|
|
7444
6996
|
}
|
|
7445
6997
|
/**
|
|
7446
|
-
*
|
|
6998
|
+
* Looks up the dispatcher associated with (target, path), or installs a new one.
|
|
6999
|
+
* When called for a nested target during proxy walking, the enclosing dispatcher
|
|
7000
|
+
* is reused so a single subtree fans out changes through one notification path.
|
|
7447
7001
|
*/
|
|
7448
|
-
|
|
7449
|
-
|
|
7002
|
+
static resolveDispatcher(target, path, inheritedDispatcher) {
|
|
7003
|
+
let pathMap = this.dispatchers.get(target);
|
|
7004
|
+
if (!pathMap) {
|
|
7005
|
+
pathMap = new Map();
|
|
7006
|
+
this.dispatchers.set(target, pathMap);
|
|
7007
|
+
}
|
|
7008
|
+
const existing = pathMap.get(path);
|
|
7009
|
+
if (existing) {
|
|
7010
|
+
return existing;
|
|
7011
|
+
}
|
|
7012
|
+
const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
|
|
7013
|
+
pathMap.set(path, dispatcher);
|
|
7014
|
+
return dispatcher;
|
|
7450
7015
|
}
|
|
7451
7016
|
/**
|
|
7452
|
-
*
|
|
7017
|
+
* Invokes every listener attached to the dispatcher.
|
|
7018
|
+
* Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
|
|
7453
7019
|
*/
|
|
7454
|
-
|
|
7455
|
-
|
|
7020
|
+
static dispatch(dispatcher, changedPath) {
|
|
7021
|
+
if (dispatcher.listeners.size === 0) {
|
|
7022
|
+
return;
|
|
7023
|
+
}
|
|
7024
|
+
const snapshot = Array.from(dispatcher.listeners);
|
|
7025
|
+
for (const listener of snapshot) {
|
|
7026
|
+
listener(changedPath);
|
|
7027
|
+
}
|
|
7456
7028
|
}
|
|
7457
7029
|
/**
|
|
7458
|
-
*
|
|
7030
|
+
* Subscribes a listener to changes inside the subtree of an existing reactive proxy.
|
|
7031
|
+
*
|
|
7032
|
+
* The listener is scoped by the proxy's source path: only changes at or below that
|
|
7033
|
+
* path are delivered, which lets a child component receive notifications when the
|
|
7034
|
+
* nested contents of a prop change even though the prop reference itself is unchanged.
|
|
7035
|
+
*
|
|
7036
|
+
* @param proxyOrTarget A proxy returned from create(), or the underlying target object.
|
|
7037
|
+
* @param listener Called with the full source path of every relevant change.
|
|
7038
|
+
* @returns A function that removes the subscription.
|
|
7459
7039
|
*/
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7040
|
+
static subscribe(proxyOrTarget, listener) {
|
|
7041
|
+
if (typeof proxyOrTarget !== 'object' || proxyOrTarget === null) {
|
|
7042
|
+
return () => { };
|
|
7043
|
+
}
|
|
7044
|
+
const target = (this.proxyToTarget.get(proxyOrTarget) ?? proxyOrTarget);
|
|
7045
|
+
const scopePath = this.proxyPaths.get(proxyOrTarget) ?? '';
|
|
7046
|
+
const pathMap = this.dispatchers.get(target);
|
|
7047
|
+
const dispatcher = pathMap?.get(scopePath);
|
|
7048
|
+
if (!dispatcher) {
|
|
7049
|
+
return () => { };
|
|
7050
|
+
}
|
|
7051
|
+
const wrapper = scopePath
|
|
7052
|
+
? (changedPath) => {
|
|
7053
|
+
if (changedPath === scopePath ||
|
|
7054
|
+
(typeof changedPath === 'string' && (changedPath.startsWith(scopePath + '.') ||
|
|
7055
|
+
changedPath.startsWith(scopePath + '[')))) {
|
|
7056
|
+
listener(changedPath);
|
|
7057
|
+
}
|
|
7469
7058
|
}
|
|
7059
|
+
: listener;
|
|
7060
|
+
dispatcher.listeners.add(wrapper);
|
|
7061
|
+
return () => {
|
|
7062
|
+
dispatcher.listeners.delete(wrapper);
|
|
7470
7063
|
};
|
|
7471
|
-
return updater;
|
|
7472
|
-
}
|
|
7473
|
-
/**
|
|
7474
|
-
* @inheritdoc
|
|
7475
|
-
*/
|
|
7476
|
-
get templatize() {
|
|
7477
|
-
return false;
|
|
7478
|
-
}
|
|
7479
|
-
/**
|
|
7480
|
-
* @inheritdoc
|
|
7481
|
-
*/
|
|
7482
|
-
get dependentIdentifiers() {
|
|
7483
|
-
return this.#evaluator?.dependentIdentifiers ?? [];
|
|
7484
7064
|
}
|
|
7485
7065
|
/**
|
|
7486
|
-
*
|
|
7487
|
-
*
|
|
7488
|
-
*
|
|
7066
|
+
* Checks if the given object is a reactive proxy.
|
|
7067
|
+
*
|
|
7068
|
+
* @param obj The object to check.
|
|
7069
|
+
* @returns True if the object is a reactive proxy, false otherwise.
|
|
7489
7070
|
*/
|
|
7490
|
-
|
|
7491
|
-
return (
|
|
7071
|
+
static isReactive(obj) {
|
|
7072
|
+
return this.proxyCache.has(obj);
|
|
7492
7073
|
}
|
|
7493
7074
|
/**
|
|
7494
|
-
*
|
|
7495
|
-
*
|
|
7075
|
+
* Unwraps a reactive proxy to get the original object.
|
|
7076
|
+
* If the object is not a proxy, returns it as-is.
|
|
7077
|
+
*
|
|
7078
|
+
* @param obj The object to unwrap.
|
|
7079
|
+
* @returns The original object.
|
|
7496
7080
|
*/
|
|
7497
|
-
|
|
7498
|
-
|
|
7081
|
+
static unwrap(obj) {
|
|
7082
|
+
// This is a simplified implementation
|
|
7083
|
+
// In a full implementation, we'd need to store a reverse mapping
|
|
7084
|
+
return obj;
|
|
7499
7085
|
}
|
|
7500
7086
|
/**
|
|
7501
|
-
*
|
|
7087
|
+
* Marks an object as "raw" (non-reactive).
|
|
7088
|
+
* Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
|
|
7089
|
+
* This is useful for objects that should not be reactive, such as:
|
|
7090
|
+
* - Objects with private fields (class instances with # fields)
|
|
7091
|
+
* - Third-party library instances
|
|
7092
|
+
* - Objects used only for method calls
|
|
7093
|
+
*
|
|
7094
|
+
* @param obj The object to mark as raw.
|
|
7095
|
+
* @returns The same object (for chaining).
|
|
7502
7096
|
*/
|
|
7503
|
-
|
|
7504
|
-
|
|
7097
|
+
static markRaw(obj) {
|
|
7098
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
7099
|
+
this.rawObjects.add(obj);
|
|
7100
|
+
}
|
|
7101
|
+
return obj;
|
|
7505
7102
|
}
|
|
7506
7103
|
/**
|
|
7507
|
-
*
|
|
7104
|
+
* Checks if an object is marked as raw (non-reactive).
|
|
7105
|
+
*
|
|
7106
|
+
* @param obj The object to check.
|
|
7107
|
+
* @returns True if the object is marked as raw, false otherwise.
|
|
7508
7108
|
*/
|
|
7509
|
-
|
|
7510
|
-
return this
|
|
7109
|
+
static isRaw(obj) {
|
|
7110
|
+
return typeof obj === 'object' && obj !== null && this.rawObjects.has(obj);
|
|
7511
7111
|
}
|
|
7512
7112
|
/**
|
|
7513
|
-
*
|
|
7514
|
-
*
|
|
7113
|
+
* Gets the source path for a proxy object.
|
|
7114
|
+
* This is used to map computed property values back to their source paths.
|
|
7115
|
+
* For example, if a computed property returns `model.elements[0]`,
|
|
7116
|
+
* this method returns "model.elements[0]" for that object.
|
|
7117
|
+
*
|
|
7118
|
+
* @param obj The proxy object to get the path for.
|
|
7119
|
+
* @returns The source path, or undefined if not found.
|
|
7515
7120
|
*/
|
|
7516
|
-
|
|
7517
|
-
|
|
7121
|
+
static getPath(obj) {
|
|
7122
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
7123
|
+
return undefined;
|
|
7124
|
+
}
|
|
7125
|
+
return this.proxyPaths.get(obj);
|
|
7518
7126
|
}
|
|
7519
7127
|
/**
|
|
7520
|
-
*
|
|
7128
|
+
* Registers a path alias.
|
|
7129
|
+
* This is used when a variable is assigned to reference an existing reactive object.
|
|
7130
|
+
* For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
|
|
7131
|
+
* so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
|
|
7132
|
+
*
|
|
7133
|
+
* @param aliasPath The alias path (e.g., "editingNestedStep")
|
|
7134
|
+
* @param sourcePath The source path (e.g., "routes[0].steps[0]")
|
|
7521
7135
|
*/
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
// Render it once at mount instead.
|
|
7526
|
-
if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
|
|
7527
|
-
return () => {
|
|
7528
|
-
this.#render();
|
|
7529
|
-
};
|
|
7136
|
+
static registerPathAlias(aliasPath, sourcePath) {
|
|
7137
|
+
if (aliasPath && sourcePath && aliasPath !== sourcePath) {
|
|
7138
|
+
this.pathAliases.set(aliasPath, sourcePath);
|
|
7530
7139
|
}
|
|
7531
|
-
return undefined;
|
|
7532
7140
|
}
|
|
7533
7141
|
/**
|
|
7534
|
-
*
|
|
7142
|
+
* Unregisters a path alias.
|
|
7143
|
+
*
|
|
7144
|
+
* @param aliasPath The alias path to remove
|
|
7535
7145
|
*/
|
|
7536
|
-
|
|
7537
|
-
|
|
7146
|
+
static unregisterPathAlias(aliasPath) {
|
|
7147
|
+
// Remove all aliases that start with the given path
|
|
7148
|
+
for (const key of this.pathAliases.keys()) {
|
|
7149
|
+
if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
|
|
7150
|
+
this.pathAliases.delete(key);
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7538
7153
|
}
|
|
7539
7154
|
/**
|
|
7540
|
-
*
|
|
7155
|
+
* Resolves an alias path to its source path.
|
|
7156
|
+
* Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
|
|
7157
|
+
*
|
|
7158
|
+
* @param aliasPath The alias path to resolve
|
|
7159
|
+
* @returns The source path, or undefined if no alias exists
|
|
7541
7160
|
*/
|
|
7542
|
-
|
|
7161
|
+
static resolvePathAlias(aliasPath) {
|
|
7162
|
+
// Direct match
|
|
7163
|
+
if (this.pathAliases.has(aliasPath)) {
|
|
7164
|
+
return this.pathAliases.get(aliasPath);
|
|
7165
|
+
}
|
|
7166
|
+
// Check if this is a nested path of an aliased variable
|
|
7167
|
+
// e.g., "editingNestedStep.steps" when "editingNestedStep" is aliased
|
|
7168
|
+
for (const [alias, source] of this.pathAliases.entries()) {
|
|
7169
|
+
if (aliasPath.startsWith(alias + '.') || aliasPath.startsWith(alias + '[')) {
|
|
7170
|
+
const suffix = aliasPath.substring(alias.length);
|
|
7171
|
+
return source + suffix;
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7543
7174
|
return undefined;
|
|
7544
7175
|
}
|
|
7545
7176
|
/**
|
|
7546
|
-
*
|
|
7177
|
+
* Checks if a change path matches an identifier, considering path aliases.
|
|
7178
|
+
* For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
|
|
7179
|
+
* then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
|
|
7180
|
+
*
|
|
7181
|
+
* @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
|
|
7182
|
+
* @param identifier The identifier to check (e.g., "editingNestedStep.steps")
|
|
7183
|
+
* @returns True if the change path matches the identifier
|
|
7547
7184
|
*/
|
|
7548
|
-
|
|
7549
|
-
|
|
7185
|
+
static doesChangeMatchIdentifier(changePath, identifier) {
|
|
7186
|
+
// Direct match
|
|
7187
|
+
if (this.pathMatches(changePath, identifier)) {
|
|
7188
|
+
return true;
|
|
7189
|
+
}
|
|
7190
|
+
// Check if the change path is the resolved identifier or lies below it
|
|
7191
|
+
// (handles cases like array item changes)
|
|
7192
|
+
const resolvedIdentifier = this.resolvePathAlias(identifier);
|
|
7193
|
+
if (resolvedIdentifier && this.pathMatches(changePath, resolvedIdentifier, true)) {
|
|
7194
|
+
return true;
|
|
7195
|
+
}
|
|
7196
|
+
return false;
|
|
7550
7197
|
}
|
|
7551
7198
|
/**
|
|
7552
|
-
*
|
|
7199
|
+
* Checks whether a change path names the location an identifier refers to.
|
|
7200
|
+
* A "[*]" index in the identifier (e.g. "items[*].name" for `items[i].name`)
|
|
7201
|
+
* stands for an index known only when the expression runs and matches any
|
|
7202
|
+
* one index or key.
|
|
7203
|
+
*
|
|
7204
|
+
* @param changePath The path that was changed (e.g., "items[2].name")
|
|
7205
|
+
* @param identifier The identifier to check (e.g., "items[*].name")
|
|
7206
|
+
* @param descendants Whether a change below the identifier also matches
|
|
7207
|
+
* @returns True if the change path matches the identifier
|
|
7553
7208
|
*/
|
|
7554
|
-
|
|
7555
|
-
|
|
7209
|
+
static pathMatches(changePath, identifier, descendants = false) {
|
|
7210
|
+
if (changePath === identifier) {
|
|
7211
|
+
return true;
|
|
7212
|
+
}
|
|
7213
|
+
if (!identifier.includes('[*]')) {
|
|
7214
|
+
return descendants && (changePath.startsWith(identifier + '.') || changePath.startsWith(identifier + '['));
|
|
7215
|
+
}
|
|
7216
|
+
const cacheKey = descendants ? identifier + ' ' : identifier;
|
|
7217
|
+
let pattern = this.pathPatterns.get(cacheKey);
|
|
7218
|
+
if (!pattern) {
|
|
7219
|
+
const source = identifier
|
|
7220
|
+
.split('[*]')
|
|
7221
|
+
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
7222
|
+
.join('(?:\\[[^\\]]*\\]|\\.[^.[]+)');
|
|
7223
|
+
pattern = new RegExp(`^${source}${descendants ? '(?:[.[].*)?' : ''}$`);
|
|
7224
|
+
this.pathPatterns.set(cacheKey, pattern);
|
|
7225
|
+
}
|
|
7226
|
+
return pattern.test(changePath);
|
|
7556
7227
|
}
|
|
7228
|
+
}
|
|
7229
|
+
|
|
7230
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7231
|
+
/**
|
|
7232
|
+
* Utility class for analyzing JavaScript expressions to extract variable and function dependencies.
|
|
7233
|
+
*/
|
|
7234
|
+
class ExpressionUtils {
|
|
7557
7235
|
/**
|
|
7558
|
-
*
|
|
7236
|
+
* Extracts variable and function names used in the expression.
|
|
7237
|
+
* @param expression The expression string to analyze.
|
|
7238
|
+
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
7239
|
+
* @param options Optional parsing options.
|
|
7240
|
+
* - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
|
|
7241
|
+
* declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
|
|
7242
|
+
* for backward compatibility with interpolation and binding directives).
|
|
7243
|
+
* @returns An array of identifier names.
|
|
7559
7244
|
*/
|
|
7560
|
-
|
|
7561
|
-
|
|
7245
|
+
static extractIdentifiers(expression, functionDependencies, options) {
|
|
7246
|
+
const identifiers = new Set();
|
|
7247
|
+
// In expression mode we wrap in parens so acorn parses the source as a single expression.
|
|
7248
|
+
// In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
|
|
7249
|
+
const source = options?.asScript ? expression : `(${expression})`;
|
|
7250
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
7251
|
+
// Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
|
|
7252
|
+
full(ast, (node) => {
|
|
7253
|
+
if (node.type === 'Identifier') {
|
|
7254
|
+
identifiers.add(node.name);
|
|
7255
|
+
// Check if the identifier is a function name
|
|
7256
|
+
if (functionDependencies[node.name]) {
|
|
7257
|
+
// If it is, add its dependencies to the list of identifiers
|
|
7258
|
+
for (const dependency of functionDependencies[node.name]) {
|
|
7259
|
+
identifiers.add(dependency);
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
else if (node.type === 'MemberExpression') {
|
|
7264
|
+
// Reconstruct chain parts from a MemberExpression (e.g. a.b.c). Each part
|
|
7265
|
+
// carries its own separator so that the chain is spelled the way the reactive
|
|
7266
|
+
// proxy reports changes: array indices in brackets, properties after a dot
|
|
7267
|
+
// (e.g. "items[0].name").
|
|
7268
|
+
const parts = [];
|
|
7269
|
+
let cur = node;
|
|
7270
|
+
while (cur) {
|
|
7271
|
+
// property part (right side)
|
|
7272
|
+
if (cur.property) {
|
|
7273
|
+
if (!cur.computed && cur.property.type === 'Identifier') {
|
|
7274
|
+
parts.unshift(`.${cur.property.name}`);
|
|
7275
|
+
}
|
|
7276
|
+
else if (cur.computed && cur.property.type === 'Literal') {
|
|
7277
|
+
// items[0] and items["0"] both name an index; labels['a-b'] names a key.
|
|
7278
|
+
const key = String(cur.property.value);
|
|
7279
|
+
parts.unshift(ReactiveProxy.isIndexKey(key) ? `[${key}]` : `.${key}`);
|
|
7280
|
+
}
|
|
7281
|
+
else if (cur.computed) {
|
|
7282
|
+
// e.g. items[i] or items[i + 1]: the index is only known when the
|
|
7283
|
+
// expression runs, so "[*]" stands for any index or key. Identifiers
|
|
7284
|
+
// inside the index expression are visited separately by walk.full.
|
|
7285
|
+
parts.unshift('[*]');
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
// object part (left side)
|
|
7289
|
+
if (cur.object) {
|
|
7290
|
+
if (cur.object.type === 'Identifier') {
|
|
7291
|
+
parts.unshift(cur.object.name);
|
|
7292
|
+
break;
|
|
7293
|
+
}
|
|
7294
|
+
else if (cur.object.type === 'ThisExpression') {
|
|
7295
|
+
// For `this.x` or `this.a.b` we don't want to include 'this' itself.
|
|
7296
|
+
// Stop unwrapping here; we'll drop a leading 'this' later.
|
|
7297
|
+
break;
|
|
7298
|
+
}
|
|
7299
|
+
else if (cur.object.type === 'MemberExpression') {
|
|
7300
|
+
// continue unwrapping
|
|
7301
|
+
cur = cur.object;
|
|
7302
|
+
}
|
|
7303
|
+
else {
|
|
7304
|
+
// other object types (CallExpression, etc.) — stop here
|
|
7305
|
+
break;
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
else {
|
|
7309
|
+
break;
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
// A chain that does not start at a name (e.g. `this.a.b` or `f().b`) begins
|
|
7313
|
+
// with its first property; one that begins with an index names no binding.
|
|
7314
|
+
if (parts.length > 0 && parts[0].startsWith('.')) {
|
|
7315
|
+
parts[0] = parts[0].substring(1);
|
|
7316
|
+
}
|
|
7317
|
+
if (parts.length > 0 && !parts[0].startsWith('[')) {
|
|
7318
|
+
// Add progressive chains: 'a', 'a.b', 'a.b[0]', 'a.b[0].c'
|
|
7319
|
+
for (let i = 0; i < parts.length; i++) {
|
|
7320
|
+
const chain = parts.slice(0, i + 1).join('');
|
|
7321
|
+
identifiers.add(chain);
|
|
7322
|
+
// Also apply functionDependencies lookup for the chain key
|
|
7323
|
+
if (functionDependencies[chain]) {
|
|
7324
|
+
for (const dependency of functionDependencies[chain]) {
|
|
7325
|
+
identifiers.add(dependency);
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
});
|
|
7332
|
+
return Array.from(identifiers);
|
|
7562
7333
|
}
|
|
7563
7334
|
/**
|
|
7564
|
-
*
|
|
7335
|
+
* Returns the binding name an identifier chain starts with
|
|
7336
|
+
* (e.g. "items" for "items[0].name" or "user.name").
|
|
7337
|
+
* @param identifier An identifier or member chain returned by extractIdentifiers.
|
|
7338
|
+
* @returns The left-most name of the chain.
|
|
7565
7339
|
*/
|
|
7566
|
-
|
|
7567
|
-
|
|
7340
|
+
static baseIdentifier(identifier) {
|
|
7341
|
+
const end = identifier.search(/[.[]/);
|
|
7342
|
+
return end === -1 ? identifier : identifier.substring(0, end);
|
|
7568
7343
|
}
|
|
7569
7344
|
/**
|
|
7570
|
-
*
|
|
7345
|
+
* Analyzes the dependencies of functions in the provided dictionary.
|
|
7346
|
+
* @param functions A dictionary mapping function names to their implementations.
|
|
7347
|
+
* @returns A dictionary mapping function names to arrays of their dependencies.
|
|
7571
7348
|
*/
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7349
|
+
static analyzeFunctionDependencies(functions) {
|
|
7350
|
+
const functionDependencies = {};
|
|
7351
|
+
// First pass: collect direct dependencies for each function
|
|
7352
|
+
for (const [funcName, func] of Object.entries(functions)) {
|
|
7353
|
+
try {
|
|
7354
|
+
let source = func.toString();
|
|
7355
|
+
// Handle different function formats
|
|
7356
|
+
// Arrow function: () => expr or (args) => { ... }
|
|
7357
|
+
// Method shorthand: methodName() { ... }
|
|
7358
|
+
// Function expression: function() { ... }
|
|
7359
|
+
// Detect function type more accurately
|
|
7360
|
+
const isArrowFunction = /^\s*(\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(source);
|
|
7361
|
+
const isFunctionExpression = source.startsWith('function');
|
|
7362
|
+
const isAsyncFunction = source.startsWith('async');
|
|
7363
|
+
// If it's a method shorthand (e.g., "methodName() { ... }" or "async methodName() { ... }"), convert to function expression
|
|
7364
|
+
if (!isFunctionExpression && !isArrowFunction) {
|
|
7365
|
+
// It's likely a method shorthand, convert to function expression
|
|
7366
|
+
if (isAsyncFunction) {
|
|
7367
|
+
// Remove 'async' prefix and add 'async function' prefix
|
|
7368
|
+
source = `async function ${source.substring(5).trim()}`;
|
|
7369
|
+
}
|
|
7370
|
+
else {
|
|
7371
|
+
source = `function ${source}`;
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
// Wrap in parentheses for parsing
|
|
7375
|
+
source = `(${source})`;
|
|
7376
|
+
const ast = parse(source, { ecmaVersion: "latest" });
|
|
7377
|
+
const dependencies = new Set();
|
|
7378
|
+
const declaredVariables = new Set();
|
|
7379
|
+
const thisAliases = new Set();
|
|
7380
|
+
// First, collect all declared variables (const, let, var, function params, etc.)
|
|
7381
|
+
ancestor(ast, {
|
|
7382
|
+
VariableDeclarator(node) {
|
|
7383
|
+
if (node.id.type === 'Identifier') {
|
|
7384
|
+
declaredVariables.add(node.id.name);
|
|
7385
|
+
// Detect aliases assigned from `this`, e.g. `let vm = this;`
|
|
7386
|
+
if (node.init && node.init.type === 'ThisExpression') {
|
|
7387
|
+
thisAliases.add(node.id.name);
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7390
|
+
},
|
|
7391
|
+
FunctionDeclaration(node) {
|
|
7392
|
+
if (node.id && node.id.type === 'Identifier') {
|
|
7393
|
+
declaredVariables.add(node.id.name);
|
|
7394
|
+
}
|
|
7395
|
+
// Add function parameters
|
|
7396
|
+
node.params.forEach((param) => {
|
|
7397
|
+
if (param.type === 'Identifier') {
|
|
7398
|
+
declaredVariables.add(param.name);
|
|
7399
|
+
}
|
|
7400
|
+
});
|
|
7401
|
+
},
|
|
7402
|
+
FunctionExpression(node, ancestors) {
|
|
7403
|
+
// Only process the outermost function (skip nested functions)
|
|
7404
|
+
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
7405
|
+
n.type === 'FunctionDeclaration' ||
|
|
7406
|
+
n.type === 'ArrowFunctionExpression');
|
|
7407
|
+
if (functionAncestors.length === 1) {
|
|
7408
|
+
// Add function parameters of the main function
|
|
7409
|
+
node.params.forEach((param) => {
|
|
7410
|
+
if (param.type === 'Identifier') {
|
|
7411
|
+
declaredVariables.add(param.name);
|
|
7412
|
+
}
|
|
7413
|
+
});
|
|
7414
|
+
}
|
|
7415
|
+
},
|
|
7416
|
+
ArrowFunctionExpression(node, ancestors) {
|
|
7417
|
+
// Only process the outermost function (skip nested functions)
|
|
7418
|
+
const functionAncestors = ancestors.filter((n) => n.type === 'FunctionExpression' ||
|
|
7419
|
+
n.type === 'FunctionDeclaration' ||
|
|
7420
|
+
n.type === 'ArrowFunctionExpression');
|
|
7421
|
+
if (functionAncestors.length === 1) {
|
|
7422
|
+
// Add function parameters of the main function
|
|
7423
|
+
node.params.forEach((param) => {
|
|
7424
|
+
if (param.type === 'Identifier') {
|
|
7425
|
+
declaredVariables.add(param.name);
|
|
7426
|
+
}
|
|
7427
|
+
});
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
});
|
|
7431
|
+
// Then, collect identifiers that are not declared within the function
|
|
7432
|
+
simple(ast, {
|
|
7433
|
+
Identifier(node) {
|
|
7434
|
+
// Skip the function name itself, declared variables, and common globals
|
|
7435
|
+
if (node.name !== funcName && !declaredVariables.has(node.name)) {
|
|
7436
|
+
dependencies.add(node.name);
|
|
7437
|
+
}
|
|
7438
|
+
},
|
|
7439
|
+
MemberExpression(node) {
|
|
7440
|
+
// Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
|
|
7441
|
+
if (node.property.type === 'Identifier') {
|
|
7442
|
+
if (node.object.type === 'ThisExpression') {
|
|
7443
|
+
dependencies.add(node.property.name);
|
|
7444
|
+
}
|
|
7445
|
+
else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
|
|
7446
|
+
dependencies.add(node.property.name);
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
});
|
|
7451
|
+
functionDependencies[funcName] = Array.from(dependencies);
|
|
7452
|
+
}
|
|
7453
|
+
catch (error) {
|
|
7454
|
+
console.warn(`Failed to analyze function '${funcName}':`, error);
|
|
7455
|
+
// If parsing fails, assume no dependencies
|
|
7456
|
+
functionDependencies[funcName] = [];
|
|
7457
|
+
}
|
|
7591
7458
|
}
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7459
|
+
// Second pass: resolve each function's transitive reactive dependencies.
|
|
7460
|
+
//
|
|
7461
|
+
// A function's direct dependency list mixes two kinds of identifiers: references to other
|
|
7462
|
+
// functions (call edges) and references to reactive data properties (the values we actually
|
|
7463
|
+
// care about). We want, for every function, the full set of reactive properties reachable by
|
|
7464
|
+
// following call edges to any depth.
|
|
7465
|
+
//
|
|
7466
|
+
// Functions are free to reference each other cyclically — a poller that reschedules itself
|
|
7467
|
+
// (`scheduleOperationPoll` -> `pollOperation` -> `scheduleOperationPoll`), a pair of mutually
|
|
7468
|
+
// recursive helpers, or any state machine. This is valid JavaScript, so the resolver must not
|
|
7469
|
+
// treat it as an error. Every function in a cycle (more precisely, a strongly connected
|
|
7470
|
+
// component) is reachable from the others, so they all share the same transitive dependency
|
|
7471
|
+
// set: the union of every member's direct reactive dependencies.
|
|
7472
|
+
//
|
|
7473
|
+
// A depth-first walk that bails out on back-edges cannot compute that correctly — it drops the
|
|
7474
|
+
// dependencies contributed around the cycle and caches incomplete, traversal-order-dependent
|
|
7475
|
+
// results, which would silently break reactivity for any computed that reads reactive data
|
|
7476
|
+
// through a cyclic method. We instead split each function's direct dependencies into reactive
|
|
7477
|
+
// properties and call edges, then propagate reactive properties along call edges until the sets
|
|
7478
|
+
// stop growing (a fixpoint). This converges regardless of cycles and yields the exact transitive
|
|
7479
|
+
// closure for every function, including those participating in a cycle.
|
|
7480
|
+
const directReactive = {};
|
|
7481
|
+
const callEdges = {};
|
|
7482
|
+
for (const funcName of Object.keys(functions)) {
|
|
7483
|
+
const reactive = new Set();
|
|
7484
|
+
const edges = [];
|
|
7485
|
+
for (const dep of functionDependencies[funcName] || []) {
|
|
7486
|
+
if (dep === funcName)
|
|
7487
|
+
continue; // Skip self-references.
|
|
7488
|
+
if (functions[dep]) {
|
|
7489
|
+
edges.push(dep); // Call edge to another function.
|
|
7490
|
+
}
|
|
7491
|
+
else {
|
|
7492
|
+
reactive.add(dep); // Reactive data property (terminal dependency).
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
directReactive[funcName] = reactive;
|
|
7496
|
+
callEdges[funcName] = edges;
|
|
7599
7497
|
}
|
|
7600
|
-
|
|
7601
|
-
|
|
7498
|
+
// Seed each function with its own direct reactive dependencies, then propagate along call edges
|
|
7499
|
+
// until no set changes. Each pass can only add properties, and the universe of properties is
|
|
7500
|
+
// finite, so the loop is guaranteed to terminate.
|
|
7501
|
+
const resolved = {};
|
|
7502
|
+
for (const funcName of Object.keys(functions)) {
|
|
7503
|
+
resolved[funcName] = new Set(directReactive[funcName]);
|
|
7602
7504
|
}
|
|
7603
|
-
|
|
7604
|
-
|
|
7505
|
+
let changed = true;
|
|
7506
|
+
while (changed) {
|
|
7507
|
+
changed = false;
|
|
7508
|
+
for (const funcName of Object.keys(functions)) {
|
|
7509
|
+
const target = resolved[funcName];
|
|
7510
|
+
for (const edge of callEdges[funcName]) {
|
|
7511
|
+
for (const dep of resolved[edge]) {
|
|
7512
|
+
if (!target.has(dep)) {
|
|
7513
|
+
target.add(dep);
|
|
7514
|
+
changed = true;
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7605
7519
|
}
|
|
7606
|
-
|
|
7607
|
-
|
|
7520
|
+
const resolvedDependencies = {};
|
|
7521
|
+
for (const funcName of Object.keys(functions)) {
|
|
7522
|
+
resolvedDependencies[funcName] = Array.from(resolved[funcName]);
|
|
7608
7523
|
}
|
|
7524
|
+
return resolvedDependencies;
|
|
7609
7525
|
}
|
|
7610
7526
|
/**
|
|
7611
|
-
*
|
|
7527
|
+
* Rewrites an expression to replace identifiers with 'this.identifier'.
|
|
7528
|
+
* This allows direct property access and assignment without using 'with' statement.
|
|
7529
|
+
* Uses AST parsing to accurately identify which identifiers to replace.
|
|
7530
|
+
* @param expression The original expression string.
|
|
7531
|
+
* @param identifiers The list of identifiers that are available in bindings.
|
|
7532
|
+
* @returns The rewritten expression.
|
|
7612
7533
|
*/
|
|
7613
|
-
|
|
7614
|
-
//
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7534
|
+
static rewriteExpression(expression, identifiers, options) {
|
|
7535
|
+
// Reserved words and built-in objects that should not be prefixed with 'this.'
|
|
7536
|
+
const reserved = new Set([
|
|
7537
|
+
'event', '$event', '$ctx', '$newValue',
|
|
7538
|
+
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
7539
|
+
'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
|
|
7540
|
+
'JSON', 'console', 'window', 'document', 'navigator',
|
|
7541
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
7542
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent'
|
|
7543
|
+
]);
|
|
7544
|
+
// Extract ALL identifiers from the expression (including assignment left-hand side)
|
|
7545
|
+
// This is necessary because the passed 'identifiers' parameter only includes
|
|
7546
|
+
// identifiers that are used (right-hand side), not assigned to (left-hand side)
|
|
7547
|
+
let allIdentifiersInExpression;
|
|
7548
|
+
try {
|
|
7549
|
+
allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
|
|
7618
7550
|
}
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
.filter(Boolean)
|
|
7623
|
-
.flatMap(cls => {
|
|
7624
|
-
if (typeof cls === 'string') {
|
|
7625
|
-
return cls.split(/\s+/).filter(Boolean);
|
|
7626
|
-
}
|
|
7627
|
-
else if (typeof cls === 'object' && cls !== null) {
|
|
7628
|
-
// Handle object format within array: { className: condition }
|
|
7629
|
-
// Keys may contain space-separated class names
|
|
7630
|
-
return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
7631
|
-
}
|
|
7632
|
-
return [];
|
|
7633
|
-
});
|
|
7551
|
+
catch (error) {
|
|
7552
|
+
console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
|
|
7553
|
+
return expression;
|
|
7634
7554
|
}
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7555
|
+
// Create a Set of identifiers available in bindings (from data, computed, methods)
|
|
7556
|
+
// We need to know which identifiers are valid binding properties
|
|
7557
|
+
const bindingIdentifiers = new Set(identifiers.filter(id => !reserved.has(id)));
|
|
7558
|
+
// For assignment expressions, we also need to include the left-hand side identifier
|
|
7559
|
+
// even if it's not in the tracking identifiers (because it's being assigned, not read)
|
|
7560
|
+
for (const id of allIdentifiersInExpression) {
|
|
7561
|
+
if (!reserved.has(id)) {
|
|
7562
|
+
bindingIdentifiers.add(id);
|
|
7563
|
+
}
|
|
7638
7564
|
}
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
// Add newly managed classes
|
|
7642
|
-
newClasses.forEach(cls => element.classList.add(cls));
|
|
7643
|
-
// Update managed classes list
|
|
7644
|
-
this.#managedClasses = new Set(newClasses);
|
|
7645
|
-
}
|
|
7646
|
-
/**
|
|
7647
|
-
* Updates the style attribute with support for object format.
|
|
7648
|
-
*/
|
|
7649
|
-
#updateStyle(element, value) {
|
|
7650
|
-
let newStyles = [];
|
|
7651
|
-
if (typeof value === 'string') {
|
|
7652
|
-
// Directly set the style string
|
|
7653
|
-
element.style.cssText = value;
|
|
7654
|
-
// Extract managed properties
|
|
7655
|
-
newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
|
|
7565
|
+
if (bindingIdentifiers.size === 0) {
|
|
7566
|
+
return expression;
|
|
7656
7567
|
}
|
|
7657
|
-
|
|
7658
|
-
//
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7568
|
+
try {
|
|
7569
|
+
// Build a map of positions to replace: { start: number, end: number, name: string, asThisAlias?: boolean }[]
|
|
7570
|
+
const replacements = [];
|
|
7571
|
+
// In script mode we must not wrap in parens (that would make multi-statement input invalid).
|
|
7572
|
+
// Offsets from the parser therefore refer directly to the original expression, so no shift.
|
|
7573
|
+
const asScript = options?.asScript === true;
|
|
7574
|
+
const source = asScript ? expression : `(${expression})`;
|
|
7575
|
+
const offsetShift = asScript ? 0 : 1;
|
|
7576
|
+
const parsedAst = parse(source, { ecmaVersion: 'latest' });
|
|
7577
|
+
// Track identifiers that are locally declared within the handler body (let/const/var, function
|
|
7578
|
+
// params) so we don't rewrite them to `this.xxx`. Additionally detect `this` aliases such as
|
|
7579
|
+
// `let vm = this;` so that `vm.x` can be rewritten to `this.x` even though `vm` is locally declared.
|
|
7580
|
+
const locallyDeclared = new Set();
|
|
7581
|
+
const thisAliases = new Set();
|
|
7582
|
+
if (asScript) {
|
|
7583
|
+
full(parsedAst, (node) => {
|
|
7584
|
+
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
|
|
7585
|
+
locallyDeclared.add(node.id.name);
|
|
7586
|
+
if (node.init && node.init.type === 'ThisExpression') {
|
|
7587
|
+
thisAliases.add(node.id.name);
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
|
|
7591
|
+
locallyDeclared.add(node.id.name);
|
|
7592
|
+
}
|
|
7593
|
+
});
|
|
7594
|
+
}
|
|
7595
|
+
// Collect all identifier nodes that should be replaced
|
|
7596
|
+
// Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
|
|
7597
|
+
fullAncestor(parsedAst, (node, _state, ancestors) => {
|
|
7598
|
+
if (node.type !== 'Identifier') {
|
|
7599
|
+
return;
|
|
7600
|
+
}
|
|
7601
|
+
// Skip if not in our identifier set
|
|
7602
|
+
if (!bindingIdentifiers.has(node.name)) {
|
|
7603
|
+
return;
|
|
7604
|
+
}
|
|
7605
|
+
// Locally declared identifiers must not be rewritten to `this.xxx`. The one
|
|
7606
|
+
// exception is a `this`-alias (e.g. `let vm = this;`): when such an alias is
|
|
7607
|
+
// used as the OBJECT of a MemberExpression (`vm.x`), we replace just the
|
|
7608
|
+
// object with `this` so that `vm.x` becomes `this.x`. In every other position
|
|
7609
|
+
// (the declaration LHS, a bare reference such as `console.log(vm)`, an
|
|
7610
|
+
// argument, etc.) the alias must be left untouched — rewriting it to
|
|
7611
|
+
// `this.vm` would either produce a syntax error (`let this.vm = this;`) or
|
|
7612
|
+
// change the meaning at runtime.
|
|
7613
|
+
// walk.fullAncestor includes the visited node itself as the last entry of
|
|
7614
|
+
// `ancestors`, so the actual parent node is at length - 2.
|
|
7615
|
+
const parentNode = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
|
|
7616
|
+
const isAliasAsMemberObject = parentNode?.type === 'MemberExpression' && parentNode.object === node;
|
|
7617
|
+
if (locallyDeclared.has(node.name)) {
|
|
7618
|
+
if (!thisAliases.has(node.name) || !isAliasAsMemberObject) {
|
|
7619
|
+
return;
|
|
7670
7620
|
}
|
|
7621
|
+
replacements.push({
|
|
7622
|
+
start: node.start - offsetShift,
|
|
7623
|
+
end: node.end - offsetShift,
|
|
7624
|
+
name: node.name,
|
|
7625
|
+
asThisAlias: true
|
|
7626
|
+
});
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
// Skip identifiers that are the property (not the object) of a non-computed
|
|
7630
|
+
// member access (e.g., the `prop` in `obj.prop`).
|
|
7631
|
+
if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
|
|
7632
|
+
return;
|
|
7671
7633
|
}
|
|
7634
|
+
// Add to replacements list (adjust for the wrapping parentheses in expression mode)
|
|
7635
|
+
replacements.push({
|
|
7636
|
+
start: node.start - offsetShift,
|
|
7637
|
+
end: node.end - offsetShift,
|
|
7638
|
+
name: node.name
|
|
7639
|
+
});
|
|
7640
|
+
});
|
|
7641
|
+
// Sort replacements by start position (descending) to replace from end to start
|
|
7642
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
7643
|
+
// Apply replacements
|
|
7644
|
+
let result = expression;
|
|
7645
|
+
for (const replacement of replacements) {
|
|
7646
|
+
const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
|
|
7647
|
+
result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
|
|
7672
7648
|
}
|
|
7649
|
+
return result;
|
|
7650
|
+
}
|
|
7651
|
+
catch (error) {
|
|
7652
|
+
// If AST parsing fails, fall back to the original expression
|
|
7653
|
+
console.warn('Failed to rewrite expression:', expression, error);
|
|
7654
|
+
return expression;
|
|
7673
7655
|
}
|
|
7674
|
-
// Update managed properties list
|
|
7675
|
-
this.#managedStyles = new Set(newStyles);
|
|
7676
7656
|
}
|
|
7657
|
+
}
|
|
7658
|
+
|
|
7659
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7660
|
+
/**
|
|
7661
|
+
* A centralized expression evaluator that normalizes expression evaluation across all directives.
|
|
7662
|
+
*
|
|
7663
|
+
* This class provides:
|
|
7664
|
+
* - Consistent identifier extraction and binding resolution
|
|
7665
|
+
* - Unified global object whitelisting (Math, Date, JSON, etc.)
|
|
7666
|
+
* - Expression caching for performance
|
|
7667
|
+
* - Centralized error handling
|
|
7668
|
+
* - Support for custom expression rewriting (for VOnDirective)
|
|
7669
|
+
*
|
|
7670
|
+
* Example usage:
|
|
7671
|
+
* ```typescript
|
|
7672
|
+
* const evaluator = ExpressionEvaluator.create(
|
|
7673
|
+
* "count + 1",
|
|
7674
|
+
* bindings,
|
|
7675
|
+
* functionDependencies
|
|
7676
|
+
* );
|
|
7677
|
+
* const result = evaluator.evaluate();
|
|
7678
|
+
* ```
|
|
7679
|
+
*/
|
|
7680
|
+
class ExpressionEvaluator {
|
|
7677
7681
|
/**
|
|
7678
|
-
*
|
|
7682
|
+
* Cache for compiled expression functions.
|
|
7683
|
+
* Key: expression string
|
|
7684
|
+
* Value: compiled function
|
|
7679
7685
|
*/
|
|
7680
|
-
|
|
7681
|
-
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7682
|
-
}
|
|
7686
|
+
static cache = new Map();
|
|
7683
7687
|
/**
|
|
7684
|
-
*
|
|
7685
|
-
*
|
|
7686
|
-
*
|
|
7687
|
-
* Three conditions trigger property delivery:
|
|
7688
|
-
* 1. The value is an object or array — serialising these to a string attribute
|
|
7689
|
-
* would lose type information.
|
|
7690
|
-
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
7691
|
-
* defineComponent), even when the value is a primitive.
|
|
7692
|
-
* 3. The element declares a matching prop in its static _props (case-insensitive
|
|
7693
|
-
* and kebab-case → camelCase tolerant), so primitive values reach the
|
|
7694
|
-
* reactive binding via the generated setter instead of being lost as a
|
|
7695
|
-
* plain HTML attribute that no attributeChangedCallback observes.
|
|
7688
|
+
* Global objects that are whitelisted for use in expressions.
|
|
7689
|
+
* These objects are available to all expressions for convenience and compatibility.
|
|
7696
7690
|
*/
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7691
|
+
static GLOBAL_WHITELIST = {
|
|
7692
|
+
Math,
|
|
7693
|
+
Date,
|
|
7694
|
+
JSON,
|
|
7695
|
+
Array,
|
|
7696
|
+
Object,
|
|
7697
|
+
String,
|
|
7698
|
+
Number,
|
|
7699
|
+
Boolean,
|
|
7700
|
+
parseInt,
|
|
7701
|
+
parseFloat,
|
|
7702
|
+
isNaN,
|
|
7703
|
+
isFinite,
|
|
7704
|
+
encodeURI,
|
|
7705
|
+
decodeURI,
|
|
7706
|
+
encodeURIComponent,
|
|
7707
|
+
decodeURIComponent
|
|
7708
|
+
};
|
|
7707
7709
|
/**
|
|
7708
|
-
*
|
|
7709
|
-
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
7710
|
-
* but custom element props are typically camelCase. This method checks the
|
|
7711
|
-
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
7712
|
-
* match against both the raw attribute name and its kebab-case → camelCase
|
|
7713
|
-
* conversion (so `user-name` resolves to `userName`), falling back to the
|
|
7714
|
-
* original name when no match is found.
|
|
7710
|
+
* The original expression string.
|
|
7715
7711
|
*/
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7712
|
+
expression;
|
|
7713
|
+
/**
|
|
7714
|
+
* The identifiers extracted from the expression.
|
|
7715
|
+
*/
|
|
7716
|
+
identifiers;
|
|
7717
|
+
// The original extracted identifiers (may include member chains like 'a.b')
|
|
7718
|
+
originalIdentifiers;
|
|
7719
|
+
/**
|
|
7720
|
+
* The bindings to use for evaluating the expression.
|
|
7721
|
+
*/
|
|
7722
|
+
bindings;
|
|
7723
|
+
/**
|
|
7724
|
+
* The compiled function for evaluating the expression.
|
|
7725
|
+
*/
|
|
7726
|
+
evaluatorFunc;
|
|
7727
|
+
/**
|
|
7728
|
+
* Additional context variables (e.g., 'event', '$ctx').
|
|
7729
|
+
*/
|
|
7730
|
+
additionalContext;
|
|
7731
|
+
/**
|
|
7732
|
+
* Whether any identifier has an index known only at run time ("items[*].name").
|
|
7733
|
+
* Only then are the paths read during evaluation worth recording.
|
|
7734
|
+
*/
|
|
7735
|
+
hasDynamicIndex;
|
|
7736
|
+
/**
|
|
7737
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
7738
|
+
* or undefined before the first evaluation and when nothing is recorded.
|
|
7739
|
+
*/
|
|
7740
|
+
trackedPathSet;
|
|
7741
|
+
/**
|
|
7742
|
+
* Private constructor. Use ExpressionEvaluator.create() to create instances.
|
|
7743
|
+
*/
|
|
7744
|
+
constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
|
|
7745
|
+
this.expression = expression;
|
|
7746
|
+
this.bindings = bindings;
|
|
7747
|
+
this.identifiers = identifiers;
|
|
7748
|
+
this.originalIdentifiers = originalIdentifiers;
|
|
7749
|
+
this.evaluatorFunc = evaluatorFunc;
|
|
7750
|
+
this.additionalContext = additionalContext;
|
|
7751
|
+
this.hasDynamicIndex = originalIdentifiers.some(id => id.includes('[*]'));
|
|
7722
7752
|
}
|
|
7723
7753
|
/**
|
|
7724
|
-
*
|
|
7725
|
-
*
|
|
7726
|
-
*
|
|
7727
|
-
*
|
|
7754
|
+
* Creates an ExpressionEvaluator instance.
|
|
7755
|
+
*
|
|
7756
|
+
* @param expression The expression string to evaluate.
|
|
7757
|
+
* @param bindings The bindings to use for evaluating the expression.
|
|
7758
|
+
* @param functionDependencies A dictionary mapping function names to their dependencies.
|
|
7759
|
+
* @param options Optional configuration for the evaluator.
|
|
7760
|
+
* @returns An ExpressionEvaluator instance.
|
|
7728
7761
|
*/
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7762
|
+
static create(expression, bindings, functionDependencies, options) {
|
|
7763
|
+
// Extract identifiers from the expression (may include member chains like 'a.b')
|
|
7764
|
+
const extractedIdentifiers = ExpressionUtils.extractIdentifiers(expression, functionDependencies);
|
|
7765
|
+
// For compilation we must only pass simple identifier names (no dots).
|
|
7766
|
+
// Use the base (left-most) segment of member chains and preserve order.
|
|
7767
|
+
const baseIdentifiers = [];
|
|
7768
|
+
for (const id of extractedIdentifiers) {
|
|
7769
|
+
const base = ExpressionUtils.baseIdentifier(id);
|
|
7770
|
+
if (!baseIdentifiers.includes(base)) {
|
|
7771
|
+
baseIdentifiers.push(base);
|
|
7772
|
+
}
|
|
7733
7773
|
}
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7774
|
+
// Apply custom rewrite if provided (used by VOnDirective)
|
|
7775
|
+
let processedExpression = expression;
|
|
7776
|
+
if (options?.rewriteExpression) {
|
|
7777
|
+
// Pass the original extracted identifiers (may include member chains)
|
|
7778
|
+
processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
|
|
7779
|
+
}
|
|
7780
|
+
// Build cache key including options that affect compilation
|
|
7781
|
+
const cacheKey = JSON.stringify({
|
|
7782
|
+
expr: processedExpression,
|
|
7783
|
+
additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
|
|
7739
7784
|
});
|
|
7785
|
+
// Check cache
|
|
7786
|
+
let evaluatorFunc = this.cache.get(cacheKey);
|
|
7787
|
+
if (!evaluatorFunc) {
|
|
7788
|
+
// Compile the expression using base identifiers (no dots)
|
|
7789
|
+
evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
|
|
7790
|
+
// Cache the compiled function
|
|
7791
|
+
this.cache.set(cacheKey, evaluatorFunc);
|
|
7792
|
+
}
|
|
7793
|
+
return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
|
|
7740
7794
|
}
|
|
7741
7795
|
/**
|
|
7742
|
-
*
|
|
7796
|
+
* Compiles an expression into a function.
|
|
7797
|
+
*
|
|
7798
|
+
* @param expression The expression string to compile.
|
|
7799
|
+
* @param identifiers The identifiers extracted from the expression.
|
|
7800
|
+
* @param options Optional configuration.
|
|
7801
|
+
* @returns A compiled function.
|
|
7743
7802
|
*/
|
|
7744
|
-
|
|
7745
|
-
|
|
7803
|
+
static compileExpression(expression, identifiers, options) {
|
|
7804
|
+
// Build parameter list: globals first, then identifiers (base names), then additional context
|
|
7805
|
+
// Filter out identifiers that are already in the global whitelist to avoid duplicate parameters
|
|
7806
|
+
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7807
|
+
const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
|
|
7808
|
+
const params = [
|
|
7809
|
+
...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7810
|
+
...filteredIdentifiers
|
|
7811
|
+
];
|
|
7812
|
+
// Add additional context parameters if provided
|
|
7813
|
+
if (options?.additionalContext) {
|
|
7814
|
+
params.push(...Object.keys(options.additionalContext));
|
|
7815
|
+
}
|
|
7816
|
+
const args = params.join(", ");
|
|
7817
|
+
const funcBody = `return (${expression});`;
|
|
7818
|
+
try {
|
|
7819
|
+
// Create the function with error handling
|
|
7820
|
+
return new Function(args, funcBody);
|
|
7821
|
+
}
|
|
7822
|
+
catch (error) {
|
|
7823
|
+
throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
|
|
7824
|
+
}
|
|
7746
7825
|
}
|
|
7747
7826
|
/**
|
|
7748
|
-
*
|
|
7827
|
+
* Evaluates the expression with the current bindings.
|
|
7828
|
+
*
|
|
7829
|
+
* @returns The result of the expression evaluation.
|
|
7749
7830
|
*/
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7831
|
+
evaluate() {
|
|
7832
|
+
try {
|
|
7833
|
+
// Build arguments: globals first, then identifiers, then additional context
|
|
7834
|
+
// Filter out identifiers that are already in the global whitelist to match compileExpression
|
|
7835
|
+
const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
|
|
7836
|
+
const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
|
|
7837
|
+
const values = [
|
|
7838
|
+
// Global whitelist values
|
|
7839
|
+
...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
|
|
7840
|
+
// Identifier values from bindings (excluding globals)
|
|
7841
|
+
...filteredIdentifiers.map(id => this.bindings.get(id))
|
|
7842
|
+
];
|
|
7843
|
+
// Add additional context values if provided
|
|
7844
|
+
if (this.additionalContext) {
|
|
7845
|
+
values.push(...Object.values(this.additionalContext));
|
|
7846
|
+
}
|
|
7847
|
+
// Call the compiled function with the gathered values. When an index is
|
|
7848
|
+
// known only at run time, record which concrete paths this evaluation
|
|
7849
|
+
// read so that change matching can be narrowed to them (see trackedPaths).
|
|
7850
|
+
if (!this.hasDynamicIndex) {
|
|
7851
|
+
return this.evaluatorFunc(...values);
|
|
7852
|
+
}
|
|
7853
|
+
const paths = new Set();
|
|
7854
|
+
this.trackedPathSet = undefined;
|
|
7855
|
+
const result = ReactiveProxy.track(paths, () => this.evaluatorFunc(...values));
|
|
7856
|
+
this.trackedPathSet = paths;
|
|
7857
|
+
return result;
|
|
7858
|
+
}
|
|
7859
|
+
catch (error) {
|
|
7860
|
+
console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
|
|
7861
|
+
throw error;
|
|
7862
|
+
}
|
|
7753
7863
|
}
|
|
7754
7864
|
/**
|
|
7755
|
-
*
|
|
7865
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
7866
|
+
* recorded only for an expression with a run-time index ("items[i].name").
|
|
7867
|
+
* Each evaluation replaces the previous set, so a path read for a former
|
|
7868
|
+
* index value is dropped as soon as the expression runs with the new one.
|
|
7869
|
+
* Undefined before the first evaluation and for expressions without such an index.
|
|
7756
7870
|
*/
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
'disabled', 'readonly', 'required', 'checked', 'selected',
|
|
7760
|
-
'multiple', 'autofocus', 'autoplay', 'controls', 'loop',
|
|
7761
|
-
'muted', 'open', 'hidden'
|
|
7762
|
-
];
|
|
7763
|
-
return booleanAttributes.includes(name);
|
|
7871
|
+
get trackedPaths() {
|
|
7872
|
+
return this.trackedPathSet;
|
|
7764
7873
|
}
|
|
7765
7874
|
/**
|
|
7766
|
-
*
|
|
7767
|
-
*
|
|
7768
|
-
*
|
|
7875
|
+
* Evaluates the expression and returns the result as a boolean.
|
|
7876
|
+
*
|
|
7877
|
+
* @returns The boolean result of the expression evaluation.
|
|
7769
7878
|
*/
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
element[name] = '';
|
|
7773
|
-
if (name === 'value') {
|
|
7774
|
-
element._value = null;
|
|
7775
|
-
}
|
|
7776
|
-
}
|
|
7777
|
-
else {
|
|
7778
|
-
element[name] = value;
|
|
7779
|
-
if (name === 'value') {
|
|
7780
|
-
// Store the original typed value so that v-model on radio buttons
|
|
7781
|
-
// can preserve the type (e.g., boolean false instead of string "false").
|
|
7782
|
-
element._value = value;
|
|
7783
|
-
}
|
|
7784
|
-
}
|
|
7879
|
+
evaluateAsBoolean() {
|
|
7880
|
+
return Boolean(this.evaluate());
|
|
7785
7881
|
}
|
|
7786
7882
|
/**
|
|
7787
|
-
*
|
|
7883
|
+
* Gets the list of identifiers extracted from the expression.
|
|
7788
7884
|
*/
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
}
|
|
7793
|
-
else {
|
|
7794
|
-
element.removeAttribute(name);
|
|
7795
|
-
}
|
|
7885
|
+
get dependentIdentifiers() {
|
|
7886
|
+
// Return the original extracted identifiers (may include member chains like 'a.b')
|
|
7887
|
+
return this.originalIdentifiers;
|
|
7796
7888
|
}
|
|
7797
7889
|
/**
|
|
7798
|
-
*
|
|
7799
|
-
*
|
|
7800
|
-
* an <iframe>, even to the same URL, loads its page again.
|
|
7890
|
+
* Clears the expression cache.
|
|
7891
|
+
* This should be called sparingly, as it will force recompilation of all expressions.
|
|
7801
7892
|
*/
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
if (element.hasAttribute(name)) {
|
|
7805
|
-
element.removeAttribute(name);
|
|
7806
|
-
}
|
|
7807
|
-
}
|
|
7808
|
-
else {
|
|
7809
|
-
const text = String(value);
|
|
7810
|
-
if (element.getAttribute(name) !== text) {
|
|
7811
|
-
element.setAttribute(name, text);
|
|
7812
|
-
}
|
|
7813
|
-
}
|
|
7893
|
+
static clearCache() {
|
|
7894
|
+
this.cache.clear();
|
|
7814
7895
|
}
|
|
7815
7896
|
}
|
|
7816
7897
|
|
|
7817
7898
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
7818
7899
|
/**
|
|
7819
|
-
* Directive for
|
|
7820
|
-
*
|
|
7821
|
-
*
|
|
7822
|
-
*
|
|
7823
|
-
*
|
|
7824
|
-
*
|
|
7900
|
+
* Directive for binding dynamic attributes to DOM elements.
|
|
7901
|
+
* The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
|
|
7902
|
+
* 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.
|
|
7903
|
+
* Example usage:
|
|
7904
|
+
* <img v-bind:src="imageSrc" />
|
|
7905
|
+
* The value of `imageSrc` should be defined in the component's data object.
|
|
7906
|
+
* When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
|
|
7907
|
+
* This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
|
|
7908
|
+
* It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
|
|
7909
|
+
* The `v-bind` directive can also be used with shorthand syntax using a colon (:).
|
|
7910
|
+
* For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
|
|
7911
|
+
* This shorthand syntax is often used for brevity and improved readability in templates.
|
|
7912
|
+
* 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.
|
|
7825
7913
|
*/
|
|
7826
|
-
class
|
|
7914
|
+
class VBindDirective {
|
|
7827
7915
|
/**
|
|
7828
7916
|
* The virtual node to which this directive is applied.
|
|
7829
7917
|
*/
|
|
7830
7918
|
#vNode;
|
|
7831
7919
|
/**
|
|
7832
|
-
* The
|
|
7920
|
+
* The expression evaluator for this directive.
|
|
7833
7921
|
*/
|
|
7834
|
-
#
|
|
7922
|
+
#evaluator;
|
|
7835
7923
|
/**
|
|
7836
|
-
* The
|
|
7924
|
+
* The name of the attribute to bind (e.g., "src", "class", "style").
|
|
7925
|
+
*/
|
|
7926
|
+
#attributeName;
|
|
7927
|
+
/**
|
|
7928
|
+
* The original expression string from the directive.
|
|
7929
|
+
*/
|
|
7930
|
+
#expression;
|
|
7931
|
+
/**
|
|
7932
|
+
* The set of class names managed by this directive (used when binding to the "class" attribute).
|
|
7933
|
+
* This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
|
|
7934
|
+
*/
|
|
7935
|
+
#managedClasses = new Set();
|
|
7936
|
+
/**
|
|
7937
|
+
* The set of style properties managed by this directive (used when binding to the "style" attribute).
|
|
7938
|
+
* This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
|
|
7939
|
+
*/
|
|
7940
|
+
#managedStyles = new Set();
|
|
7941
|
+
/**
|
|
7942
|
+
* @param context The context for parsing the directive.
|
|
7837
7943
|
*/
|
|
7838
|
-
#componentApp;
|
|
7839
7944
|
constructor(context) {
|
|
7840
7945
|
this.#vNode = context.vNode;
|
|
7841
|
-
|
|
7946
|
+
// Extract the attribute name from the directive
|
|
7947
|
+
// e.g., "v-bind:src" -> "src", ":href" -> "href"
|
|
7948
|
+
const attrName = context.attribute.name;
|
|
7949
|
+
if (attrName.startsWith('v-bind:')) {
|
|
7950
|
+
this.#attributeName = attrName.substring(7);
|
|
7951
|
+
}
|
|
7952
|
+
else if (attrName.startsWith(':')) {
|
|
7953
|
+
this.#attributeName = attrName.substring(1);
|
|
7954
|
+
}
|
|
7955
|
+
// Parse the expression and create the evaluator
|
|
7956
|
+
this.#expression = context.attribute.value;
|
|
7957
|
+
if (this.#expression && context.vNode.bindings) {
|
|
7958
|
+
this.#evaluator = ExpressionEvaluator.create(this.#expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
|
|
7959
|
+
}
|
|
7842
7960
|
// Remove the directive attribute from the element
|
|
7843
7961
|
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
7844
7962
|
}
|
|
@@ -7846,7 +7964,7 @@ class VComponentDirective {
|
|
|
7846
7964
|
* @inheritdoc
|
|
7847
7965
|
*/
|
|
7848
7966
|
get name() {
|
|
7849
|
-
return StandardDirectiveName.
|
|
7967
|
+
return StandardDirectiveName.V_BIND;
|
|
7850
7968
|
}
|
|
7851
7969
|
/**
|
|
7852
7970
|
* @inheritdoc
|
|
@@ -7858,7 +7976,7 @@ class VComponentDirective {
|
|
|
7858
7976
|
* @inheritdoc
|
|
7859
7977
|
*/
|
|
7860
7978
|
get needsAnchor() {
|
|
7861
|
-
return
|
|
7979
|
+
return false;
|
|
7862
7980
|
}
|
|
7863
7981
|
/**
|
|
7864
7982
|
* @inheritdoc
|
|
@@ -7870,10 +7988,15 @@ class VComponentDirective {
|
|
|
7870
7988
|
* @inheritdoc
|
|
7871
7989
|
*/
|
|
7872
7990
|
get domUpdater() {
|
|
7873
|
-
|
|
7991
|
+
const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
|
|
7992
|
+
const evaluator = this.#evaluator;
|
|
7993
|
+
// Create an updater that handles the attribute binding
|
|
7874
7994
|
const updater = {
|
|
7875
7995
|
get dependentIdentifiers() {
|
|
7876
|
-
return
|
|
7996
|
+
return identifiers;
|
|
7997
|
+
},
|
|
7998
|
+
get trackedPaths() {
|
|
7999
|
+
return evaluator?.trackedPaths;
|
|
7877
8000
|
},
|
|
7878
8001
|
applyToDOM: () => {
|
|
7879
8002
|
this.#render();
|
|
@@ -7885,18 +8008,60 @@ class VComponentDirective {
|
|
|
7885
8008
|
* @inheritdoc
|
|
7886
8009
|
*/
|
|
7887
8010
|
get templatize() {
|
|
7888
|
-
return
|
|
8011
|
+
return false;
|
|
7889
8012
|
}
|
|
7890
8013
|
/**
|
|
7891
8014
|
* @inheritdoc
|
|
7892
8015
|
*/
|
|
7893
8016
|
get dependentIdentifiers() {
|
|
7894
|
-
return [];
|
|
8017
|
+
return this.#evaluator?.dependentIdentifiers ?? [];
|
|
8018
|
+
}
|
|
8019
|
+
/**
|
|
8020
|
+
* Indicates if this directive is binding the "key" attribute.
|
|
8021
|
+
* The "key" attribute is special and is used for optimizing rendering of lists.
|
|
8022
|
+
* If this directive is binding the "key" attribute, it will be handled by the VForDirective.
|
|
8023
|
+
*/
|
|
8024
|
+
get isKey() {
|
|
8025
|
+
return (this.#attributeName === 'key');
|
|
8026
|
+
}
|
|
8027
|
+
/**
|
|
8028
|
+
* Indicates if this directive is binding the "options" attribute or any of its sub-properties (e.g., "options.intersection").
|
|
8029
|
+
* The "options" attribute is special and is used for passing options to certain directives like VIntersectionDirective.
|
|
8030
|
+
*/
|
|
8031
|
+
get isOptions() {
|
|
8032
|
+
return (this.#attributeName === 'options' || this.#attributeName?.startsWith('options.') === true);
|
|
8033
|
+
}
|
|
8034
|
+
/**
|
|
8035
|
+
* Gets the name of the attribute being bound (e.g., "src", "class", "options", "options.intersection").
|
|
8036
|
+
*/
|
|
8037
|
+
get attributeName() {
|
|
8038
|
+
return this.#attributeName;
|
|
8039
|
+
}
|
|
8040
|
+
/**
|
|
8041
|
+
* Gets the original expression string from the directive.
|
|
8042
|
+
*/
|
|
8043
|
+
get expression() {
|
|
8044
|
+
return this.#expression;
|
|
8045
|
+
}
|
|
8046
|
+
/**
|
|
8047
|
+
* Evaluates the bound expression and returns the current value.
|
|
8048
|
+
* Returns undefined when no evaluator is available (e.g., empty expression).
|
|
8049
|
+
*/
|
|
8050
|
+
evaluate() {
|
|
8051
|
+
return this.#evaluator?.evaluate();
|
|
7895
8052
|
}
|
|
7896
8053
|
/**
|
|
7897
8054
|
* @inheritdoc
|
|
7898
8055
|
*/
|
|
7899
8056
|
get onMount() {
|
|
8057
|
+
// A constant expression (e.g. :label="'Apple'") has no reactive
|
|
8058
|
+
// identifiers, so the identifier-driven update cycle never applies it.
|
|
8059
|
+
// Render it once at mount instead.
|
|
8060
|
+
if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
|
|
8061
|
+
return () => {
|
|
8062
|
+
this.#render();
|
|
8063
|
+
};
|
|
8064
|
+
}
|
|
7900
8065
|
return undefined;
|
|
7901
8066
|
}
|
|
7902
8067
|
/**
|
|
@@ -7933,529 +8098,449 @@ class VComponentDirective {
|
|
|
7933
8098
|
* @inheritdoc
|
|
7934
8099
|
*/
|
|
7935
8100
|
destroy() {
|
|
7936
|
-
|
|
7937
|
-
|
|
8101
|
+
// Do nothing. No special cleanup needed.
|
|
8102
|
+
}
|
|
8103
|
+
/**
|
|
8104
|
+
* Renders the bound attribute by evaluating the expression and updating the DOM element.
|
|
8105
|
+
*/
|
|
8106
|
+
#render() {
|
|
8107
|
+
// Do nothing for special attributes
|
|
8108
|
+
if (this.isKey || this.isOptions) {
|
|
7938
8109
|
return;
|
|
7939
8110
|
}
|
|
7940
|
-
|
|
7941
|
-
const
|
|
7942
|
-
//
|
|
7943
|
-
this.#
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
8111
|
+
const element = this.#vNode.node;
|
|
8112
|
+
const attributeName = this.#attributeName;
|
|
8113
|
+
// If there's no evaluator or attribute name, do nothing
|
|
8114
|
+
if (!this.#evaluator || !attributeName) {
|
|
8115
|
+
return;
|
|
8116
|
+
}
|
|
8117
|
+
// Evaluate the expression to get the value
|
|
8118
|
+
const value = this.#evaluator.evaluate();
|
|
8119
|
+
// Handle different attribute types
|
|
8120
|
+
if (attributeName === 'class') {
|
|
8121
|
+
this.#updateClass(element, value);
|
|
8122
|
+
}
|
|
8123
|
+
else if (attributeName === 'style') {
|
|
8124
|
+
this.#updateStyle(element, value);
|
|
8125
|
+
}
|
|
8126
|
+
else if (this.#isCustomElementProperty(element, attributeName, value)) {
|
|
8127
|
+
// Custom elements: set objects/arrays (and declared props) as properties
|
|
8128
|
+
// so complex values are not serialized to strings via setAttribute.
|
|
8129
|
+
// HTML attributes are lowercased by the browser, so resolve to the
|
|
8130
|
+
// actual camelCase property name declared on the custom element.
|
|
8131
|
+
const propName = this.#resolveCustomElementPropertyName(element, attributeName);
|
|
8132
|
+
element[propName] = value;
|
|
8133
|
+
}
|
|
8134
|
+
else if (this.#isDOMProperty(attributeName)) {
|
|
8135
|
+
this.#updateProperty(element, attributeName, value);
|
|
8136
|
+
}
|
|
8137
|
+
else if (this.#isBooleanAttribute(attributeName)) {
|
|
8138
|
+
this.#updateBooleanAttribute(element, attributeName, value);
|
|
8139
|
+
}
|
|
8140
|
+
else {
|
|
8141
|
+
this.#updateAttribute(element, attributeName, value);
|
|
7947
8142
|
}
|
|
7948
|
-
this.#componentApp = undefined;
|
|
7949
8143
|
}
|
|
7950
8144
|
/**
|
|
7951
|
-
*
|
|
8145
|
+
* Updates the class attribute with support for string, array, and object formats.
|
|
7952
8146
|
*/
|
|
7953
|
-
#
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
8147
|
+
#updateClass(element, value) {
|
|
8148
|
+
// Determine the new set of classes to apply
|
|
8149
|
+
let newClasses = [];
|
|
8150
|
+
if (typeof value === 'string') {
|
|
8151
|
+
newClasses = value.split(/\s+/).filter(Boolean);
|
|
7957
8152
|
}
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
8153
|
+
else if (Array.isArray(value)) {
|
|
8154
|
+
// Flatten array elements that may contain strings or objects
|
|
8155
|
+
newClasses = value
|
|
8156
|
+
.filter(Boolean)
|
|
8157
|
+
.flatMap(cls => {
|
|
8158
|
+
if (typeof cls === 'string') {
|
|
8159
|
+
return cls.split(/\s+/).filter(Boolean);
|
|
8160
|
+
}
|
|
8161
|
+
else if (typeof cls === 'object' && cls !== null) {
|
|
8162
|
+
// Handle object format within array: { className: condition }
|
|
8163
|
+
// Keys may contain space-separated class names
|
|
8164
|
+
return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
8165
|
+
}
|
|
8166
|
+
return [];
|
|
8167
|
+
});
|
|
7971
8168
|
}
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
8169
|
+
else if (typeof value === 'object' && value !== null) {
|
|
8170
|
+
// Keys may contain space-separated class names
|
|
8171
|
+
newClasses = Object.keys(value).filter(key => value[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
|
|
7976
8172
|
}
|
|
7977
|
-
//
|
|
7978
|
-
|
|
7979
|
-
//
|
|
7980
|
-
|
|
7981
|
-
|
|
8173
|
+
// Remove previously managed classes
|
|
8174
|
+
this.#managedClasses.forEach(cls => element.classList.remove(cls));
|
|
8175
|
+
// Add newly managed classes
|
|
8176
|
+
newClasses.forEach(cls => element.classList.add(cls));
|
|
8177
|
+
// Update managed classes list
|
|
8178
|
+
this.#managedClasses = new Set(newClasses);
|
|
7982
8179
|
}
|
|
7983
8180
|
/**
|
|
7984
|
-
*
|
|
7985
|
-
* @returns The cloned root HTMLElement of the component.
|
|
7986
|
-
* @throws Error if the component or its template is not found.
|
|
8181
|
+
* Updates the style attribute with support for object format.
|
|
7987
8182
|
*/
|
|
7988
|
-
#
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
const finalTemplateID = component.templateID || component.id;
|
|
7996
|
-
const templateElement = document.querySelector(`#${finalTemplateID}`);
|
|
7997
|
-
if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
|
|
7998
|
-
throw new Error(`Template element '#${finalTemplateID}' not found`);
|
|
8183
|
+
#updateStyle(element, value) {
|
|
8184
|
+
let newStyles = [];
|
|
8185
|
+
if (typeof value === 'string') {
|
|
8186
|
+
// Directly set the style string
|
|
8187
|
+
element.style.cssText = value;
|
|
8188
|
+
// Extract managed properties
|
|
8189
|
+
newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
|
|
7999
8190
|
}
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8191
|
+
else if (typeof value === 'object' && value !== null) {
|
|
8192
|
+
// Remove all previously managed properties
|
|
8193
|
+
this.#managedStyles.forEach(prop => {
|
|
8194
|
+
element.style.removeProperty(this.#camelToKebab(prop));
|
|
8195
|
+
});
|
|
8196
|
+
// Add newly managed properties
|
|
8197
|
+
for (const key in value) {
|
|
8198
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
8199
|
+
const cssKey = this.#camelToKebab(key);
|
|
8200
|
+
const cssValue = value[key];
|
|
8201
|
+
if (cssValue != null) {
|
|
8202
|
+
element.style.setProperty(cssKey, String(cssValue));
|
|
8203
|
+
newStyles.push(key);
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8007
8206
|
}
|
|
8008
8207
|
}
|
|
8009
|
-
|
|
8208
|
+
// Update managed properties list
|
|
8209
|
+
this.#managedStyles = new Set(newStyles);
|
|
8010
8210
|
}
|
|
8011
|
-
}
|
|
8012
|
-
|
|
8013
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8014
|
-
/**
|
|
8015
|
-
* Utility class for creating reactive proxies that automatically track changes.
|
|
8016
|
-
*/
|
|
8017
|
-
class ReactiveProxy {
|
|
8018
|
-
/**
|
|
8019
|
-
* A WeakMap to store the proxy for each target object and path combination.
|
|
8020
|
-
* This prevents creating multiple proxies for the same object accessed from different paths.
|
|
8021
|
-
*/
|
|
8022
|
-
static proxyCache = new WeakMap();
|
|
8023
|
-
/**
|
|
8024
|
-
* A WeakMap to track which objects are proxies, mapping proxy -> original target.
|
|
8025
|
-
* This prevents double-wrapping of already proxied objects.
|
|
8026
|
-
*/
|
|
8027
|
-
static proxyToTarget = new WeakMap();
|
|
8028
|
-
/**
|
|
8029
|
-
* A WeakSet to track objects marked as "raw" (non-reactive).
|
|
8030
|
-
* These objects will not be wrapped with Proxy.
|
|
8031
|
-
*/
|
|
8032
|
-
static rawObjects = new WeakSet();
|
|
8033
8211
|
/**
|
|
8034
|
-
*
|
|
8035
|
-
* This allows retrieving the source path of an object for computed property mapping.
|
|
8212
|
+
* Converts camelCase to kebab-case for CSS properties.
|
|
8036
8213
|
*/
|
|
8037
|
-
|
|
8214
|
+
#camelToKebab(str) {
|
|
8215
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
8216
|
+
}
|
|
8038
8217
|
/**
|
|
8039
|
-
*
|
|
8040
|
-
*
|
|
8041
|
-
*
|
|
8042
|
-
*
|
|
8218
|
+
* Returns true when the target is a custom element and the binding should be
|
|
8219
|
+
* delivered as a property rather than an HTML attribute.
|
|
8220
|
+
*
|
|
8221
|
+
* Three conditions trigger property delivery:
|
|
8222
|
+
* 1. The value is an object or array — serialising these to a string attribute
|
|
8223
|
+
* would lose type information.
|
|
8224
|
+
* 2. The element exposes a matching property accessor (e.g. a prop declared via
|
|
8225
|
+
* defineComponent), even when the value is a primitive.
|
|
8226
|
+
* 3. The element declares a matching prop in its static _props (case-insensitive
|
|
8227
|
+
* and kebab-case → camelCase tolerant), so primitive values reach the
|
|
8228
|
+
* reactive binding via the generated setter instead of being lost as a
|
|
8229
|
+
* plain HTML attribute that no attributeChangedCallback observes.
|
|
8043
8230
|
*/
|
|
8044
|
-
|
|
8231
|
+
#isCustomElementProperty(element, name, value) {
|
|
8232
|
+
if (!element.tagName.includes('-')) {
|
|
8233
|
+
return false;
|
|
8234
|
+
}
|
|
8235
|
+
const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
8236
|
+
if (isObjectOrArray || name in element) {
|
|
8237
|
+
return true;
|
|
8238
|
+
}
|
|
8239
|
+
return this.#findDeclaredProp(element, name) !== undefined;
|
|
8240
|
+
}
|
|
8045
8241
|
/**
|
|
8046
|
-
*
|
|
8047
|
-
*
|
|
8048
|
-
*
|
|
8049
|
-
*
|
|
8242
|
+
* Resolves the actual property name on a custom element for a given
|
|
8243
|
+
* (lowercased) HTML attribute name. HTML attributes are always lowercase,
|
|
8244
|
+
* but custom element props are typically camelCase. This method checks the
|
|
8245
|
+
* element's declared _props (set by defineComponent) for a case-insensitive
|
|
8246
|
+
* match against both the raw attribute name and its kebab-case → camelCase
|
|
8247
|
+
* conversion (so `user-name` resolves to `userName`), falling back to the
|
|
8248
|
+
* original name when no match is found.
|
|
8050
8249
|
*/
|
|
8051
|
-
|
|
8250
|
+
#resolveCustomElementPropertyName(element, name) {
|
|
8251
|
+
// Fast path: exact match already exists
|
|
8252
|
+
if (name in element) {
|
|
8253
|
+
return name;
|
|
8254
|
+
}
|
|
8255
|
+
return this.#findDeclaredProp(element, name) ?? name;
|
|
8256
|
+
}
|
|
8052
8257
|
/**
|
|
8053
|
-
*
|
|
8054
|
-
*
|
|
8258
|
+
* Looks up a declared prop on a custom element constructor whose name matches
|
|
8259
|
+
* the given attribute name, treating the comparison as case-insensitive and
|
|
8260
|
+
* tolerant of kebab-case → camelCase conversion. Returns the canonical prop
|
|
8261
|
+
* name (as declared) when a match is found, or undefined otherwise.
|
|
8055
8262
|
*/
|
|
8056
|
-
|
|
8263
|
+
#findDeclaredProp(element, name) {
|
|
8264
|
+
const props = element.constructor._props;
|
|
8265
|
+
if (!Array.isArray(props)) {
|
|
8266
|
+
return undefined;
|
|
8267
|
+
}
|
|
8268
|
+
const lowerName = name.toLowerCase();
|
|
8269
|
+
const camelLower = this.#kebabToCamel(lowerName).toLowerCase();
|
|
8270
|
+
return props.find(p => {
|
|
8271
|
+
const lowerProp = p.toLowerCase();
|
|
8272
|
+
return lowerProp === lowerName || lowerProp === camelLower;
|
|
8273
|
+
});
|
|
8274
|
+
}
|
|
8057
8275
|
/**
|
|
8058
|
-
*
|
|
8059
|
-
* proxy. Everything else is returned untouched (raw).
|
|
8060
|
-
*
|
|
8061
|
-
* Rationale: a Proxy can only safely intercept plain data containers. Built-in
|
|
8062
|
-
* and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
|
|
8063
|
-
* `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
|
|
8064
|
-
* slots, and invoking their native methods with a Proxy as the `this`/receiver
|
|
8065
|
-
* throws "Illegal invocation". They must therefore never be wrapped. This is an
|
|
8066
|
-
* allow-list rather than a deny-list so that *every* such exotic type is
|
|
8067
|
-
* excluded by default, not just the handful we happened to enumerate.
|
|
8068
|
-
*
|
|
8069
|
-
* Plain user-defined class instances report `[object Object]` (unless they set
|
|
8070
|
-
* `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
|
|
8071
|
-
* `Map`/`Set` stay reactive because the proxy specially wraps their mutation
|
|
8072
|
-
* methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
|
|
8073
|
-
* since they cannot be wrapped that way and would break the same as `File`.
|
|
8074
|
-
*
|
|
8075
|
-
* Callers that want a plain object kept non-reactive can still opt out
|
|
8076
|
-
* explicitly via {@link markRaw}.
|
|
8276
|
+
* Converts kebab-case to camelCase (e.g. `user-name` → `userName`).
|
|
8077
8277
|
*/
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
'[object Arguments]',
|
|
8082
|
-
'[object Map]',
|
|
8083
|
-
'[object Set]',
|
|
8084
|
-
]);
|
|
8278
|
+
#kebabToCamel(str) {
|
|
8279
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
8280
|
+
}
|
|
8085
8281
|
/**
|
|
8086
|
-
*
|
|
8087
|
-
* See {@link REACTIVE_TYPE_TAGS} for the reasoning.
|
|
8282
|
+
* Checks if the attribute should be set as a DOM property.
|
|
8088
8283
|
*/
|
|
8089
|
-
|
|
8090
|
-
|
|
8284
|
+
#isDOMProperty(name) {
|
|
8285
|
+
const domProperties = ['value', 'checked', 'selected', 'innerHTML', 'textContent'];
|
|
8286
|
+
return domProperties.includes(name);
|
|
8091
8287
|
}
|
|
8092
8288
|
/**
|
|
8093
|
-
*
|
|
8094
|
-
* The proxy will call the onChange callback whenever a property is modified.
|
|
8095
|
-
*
|
|
8096
|
-
* @param target The object to make reactive.
|
|
8097
|
-
* @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
|
|
8098
|
-
* @param path The current path in the object tree (used internally for nested objects).
|
|
8099
|
-
* @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
|
|
8100
|
-
* @returns A reactive proxy of the target object.
|
|
8289
|
+
* Checks if the attribute is a boolean attribute.
|
|
8101
8290
|
*/
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
}
|
|
8121
|
-
// Resolve (and register) the dispatcher for this (target, path).
|
|
8122
|
-
// Nested create() calls inherit the dispatcher from the enclosing call so
|
|
8123
|
-
// that a single dispatcher fans out changes at any depth of the subtree.
|
|
8124
|
-
const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
|
|
8125
|
-
if (onChange) {
|
|
8126
|
-
dispatcher.listeners.add(onChange);
|
|
8127
|
-
}
|
|
8128
|
-
// Check if we already have a proxy for this target with this path
|
|
8129
|
-
let pathMap = this.proxyCache.get(target);
|
|
8130
|
-
if (pathMap) {
|
|
8131
|
-
const existingProxy = pathMap.get(path);
|
|
8132
|
-
if (existingProxy) {
|
|
8133
|
-
return existingProxy;
|
|
8291
|
+
#isBooleanAttribute(name) {
|
|
8292
|
+
const booleanAttributes = [
|
|
8293
|
+
'disabled', 'readonly', 'required', 'checked', 'selected',
|
|
8294
|
+
'multiple', 'autofocus', 'autoplay', 'controls', 'loop',
|
|
8295
|
+
'muted', 'open', 'hidden'
|
|
8296
|
+
];
|
|
8297
|
+
return booleanAttributes.includes(name);
|
|
8298
|
+
}
|
|
8299
|
+
/**
|
|
8300
|
+
* Updates a DOM property.
|
|
8301
|
+
* For value-like properties (value, textContent, innerHTML), null/undefined is converted to empty string
|
|
8302
|
+
* to match Vue.js behavior and prevent "undefined" from being displayed.
|
|
8303
|
+
*/
|
|
8304
|
+
#updateProperty(element, name, value) {
|
|
8305
|
+
if (value == null && (name === 'value' || name === 'textContent' || name === 'innerHTML')) {
|
|
8306
|
+
element[name] = '';
|
|
8307
|
+
if (name === 'value') {
|
|
8308
|
+
element._value = null;
|
|
8134
8309
|
}
|
|
8135
8310
|
}
|
|
8136
8311
|
else {
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
get(obj, key, receiver) {
|
|
8143
|
-
const value = Reflect.get(obj, key);
|
|
8144
|
-
// If the value is an object or array, make it reactive too
|
|
8145
|
-
if (typeof value === 'object' && value !== null) {
|
|
8146
|
-
// Don't wrap objects marked as raw (non-reactive)
|
|
8147
|
-
if (ReactiveProxy.rawObjects.has(value)) {
|
|
8148
|
-
return value;
|
|
8149
|
-
}
|
|
8150
|
-
// Only deeply proxy plain data containers; hand back built-in
|
|
8151
|
-
// and host objects (Date, File, Blob, Promise, WeakMap, DOM
|
|
8152
|
-
// nodes, …) untouched so their native methods keep working.
|
|
8153
|
-
if (!ReactiveProxy.isReactivable(value)) {
|
|
8154
|
-
return value;
|
|
8155
|
-
}
|
|
8156
|
-
// Build the nested path
|
|
8157
|
-
const keyStr = String(key);
|
|
8158
|
-
const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
8159
|
-
return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
|
|
8160
|
-
}
|
|
8161
|
-
// If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
|
|
8162
|
-
if (typeof value === 'function') {
|
|
8163
|
-
// For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
|
|
8164
|
-
if (Array.isArray(obj)) {
|
|
8165
|
-
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
8166
|
-
if (!arrayMutationMethods.includes(key)) {
|
|
8167
|
-
return value;
|
|
8168
|
-
}
|
|
8169
|
-
return function (...args) {
|
|
8170
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8171
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8172
|
-
return result;
|
|
8173
|
-
};
|
|
8174
|
-
}
|
|
8175
|
-
// For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
|
|
8176
|
-
if (obj.constructor.name === 'Map') {
|
|
8177
|
-
const mapMutationMethods = ['set', 'delete', 'clear'];
|
|
8178
|
-
return function (...args) {
|
|
8179
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8180
|
-
if (mapMutationMethods.includes(key)) {
|
|
8181
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8182
|
-
}
|
|
8183
|
-
return result;
|
|
8184
|
-
};
|
|
8185
|
-
}
|
|
8186
|
-
// For Set, we only want to wrap mutation methods, not read methods like 'has'
|
|
8187
|
-
if (obj.constructor.name === 'Set') {
|
|
8188
|
-
const setMutationMethods = ['add', 'delete', 'clear'];
|
|
8189
|
-
return function (...args) {
|
|
8190
|
-
const result = value.apply(this === receiver ? obj : this, args);
|
|
8191
|
-
if (setMutationMethods.includes(key)) {
|
|
8192
|
-
ReactiveProxy.dispatch(dispatcher, path || undefined);
|
|
8193
|
-
}
|
|
8194
|
-
return result;
|
|
8195
|
-
};
|
|
8196
|
-
}
|
|
8197
|
-
}
|
|
8198
|
-
return value;
|
|
8199
|
-
},
|
|
8200
|
-
set(obj, key, value) {
|
|
8201
|
-
const oldValue = Reflect.get(obj, key);
|
|
8202
|
-
const result = Reflect.set(obj, key, value);
|
|
8203
|
-
// Only trigger onChange if the value actually changed
|
|
8204
|
-
if (oldValue !== value) {
|
|
8205
|
-
const keyStr = String(key);
|
|
8206
|
-
const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
8207
|
-
ReactiveProxy.dispatch(dispatcher, fullPath);
|
|
8208
|
-
}
|
|
8209
|
-
return result;
|
|
8210
|
-
},
|
|
8211
|
-
deleteProperty(obj, key) {
|
|
8212
|
-
const result = Reflect.deleteProperty(obj, key);
|
|
8213
|
-
const keyStr = String(key);
|
|
8214
|
-
const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
|
8215
|
-
ReactiveProxy.dispatch(dispatcher, fullPath);
|
|
8216
|
-
return result;
|
|
8312
|
+
element[name] = value;
|
|
8313
|
+
if (name === 'value') {
|
|
8314
|
+
// Store the original typed value so that v-model on radio buttons
|
|
8315
|
+
// can preserve the type (e.g., boolean false instead of string "false").
|
|
8316
|
+
element._value = value;
|
|
8217
8317
|
}
|
|
8218
|
-
});
|
|
8219
|
-
// Cache the proxy for this path
|
|
8220
|
-
pathMap.set(path, proxy);
|
|
8221
|
-
// Track that this proxy wraps the target to prevent double-wrapping
|
|
8222
|
-
this.proxyToTarget.set(proxy, target);
|
|
8223
|
-
// Store the path for this proxy (for computed property mapping)
|
|
8224
|
-
if (path) {
|
|
8225
|
-
this.proxyPaths.set(proxy, path);
|
|
8226
8318
|
}
|
|
8227
|
-
return proxy;
|
|
8228
8319
|
}
|
|
8229
8320
|
/**
|
|
8230
|
-
*
|
|
8231
|
-
* When called for a nested target during proxy walking, the enclosing dispatcher
|
|
8232
|
-
* is reused so a single subtree fans out changes through one notification path.
|
|
8321
|
+
* Updates a boolean attribute.
|
|
8233
8322
|
*/
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
pathMap = new Map();
|
|
8238
|
-
this.dispatchers.set(target, pathMap);
|
|
8323
|
+
#updateBooleanAttribute(element, name, value) {
|
|
8324
|
+
if (value) {
|
|
8325
|
+
element.setAttribute(name, '');
|
|
8239
8326
|
}
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
return existing;
|
|
8327
|
+
else {
|
|
8328
|
+
element.removeAttribute(name);
|
|
8243
8329
|
}
|
|
8244
|
-
const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
|
|
8245
|
-
pathMap.set(path, dispatcher);
|
|
8246
|
-
return dispatcher;
|
|
8247
8330
|
}
|
|
8248
8331
|
/**
|
|
8249
|
-
*
|
|
8250
|
-
*
|
|
8332
|
+
* Updates a regular attribute.
|
|
8333
|
+
* An attribute that already holds the value is left alone: setting `src` on
|
|
8334
|
+
* an <iframe>, even to the same URL, loads its page again.
|
|
8251
8335
|
*/
|
|
8252
|
-
|
|
8253
|
-
if (
|
|
8254
|
-
|
|
8336
|
+
#updateAttribute(element, name, value) {
|
|
8337
|
+
if (value == null || value === false) {
|
|
8338
|
+
if (element.hasAttribute(name)) {
|
|
8339
|
+
element.removeAttribute(name);
|
|
8340
|
+
}
|
|
8255
8341
|
}
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8342
|
+
else {
|
|
8343
|
+
const text = String(value);
|
|
8344
|
+
if (element.getAttribute(name) !== text) {
|
|
8345
|
+
element.setAttribute(name, text);
|
|
8346
|
+
}
|
|
8259
8347
|
}
|
|
8260
8348
|
}
|
|
8349
|
+
}
|
|
8350
|
+
|
|
8351
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8352
|
+
/**
|
|
8353
|
+
* Directive for rendering components.
|
|
8354
|
+
* Usage: <div v-component="componentId" :options="props"></div>
|
|
8355
|
+
*
|
|
8356
|
+
* The :options binding is used to pass properties to the component.
|
|
8357
|
+
* Example:
|
|
8358
|
+
* <div v-component="my-component" :options="{message: 'Hello'}"></div>
|
|
8359
|
+
*/
|
|
8360
|
+
class VComponentDirective {
|
|
8261
8361
|
/**
|
|
8262
|
-
*
|
|
8263
|
-
*
|
|
8264
|
-
* The listener is scoped by the proxy's source path: only changes at or below that
|
|
8265
|
-
* path are delivered, which lets a child component receive notifications when the
|
|
8266
|
-
* nested contents of a prop change even though the prop reference itself is unchanged.
|
|
8267
|
-
*
|
|
8268
|
-
* @param proxyOrTarget A proxy returned from create(), or the underlying target object.
|
|
8269
|
-
* @param listener Called with the full source path of every relevant change.
|
|
8270
|
-
* @returns A function that removes the subscription.
|
|
8362
|
+
* The virtual node to which this directive is applied.
|
|
8271
8363
|
*/
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8364
|
+
#vNode;
|
|
8365
|
+
/**
|
|
8366
|
+
* The component ID to render.
|
|
8367
|
+
*/
|
|
8368
|
+
#componentId;
|
|
8369
|
+
/**
|
|
8370
|
+
* The application instance for the component.
|
|
8371
|
+
*/
|
|
8372
|
+
#componentApp;
|
|
8373
|
+
constructor(context) {
|
|
8374
|
+
this.#vNode = context.vNode;
|
|
8375
|
+
this.#componentId = context.attribute.value.trim();
|
|
8376
|
+
// Remove the directive attribute from the element
|
|
8377
|
+
this.#vNode.node.removeAttribute(context.attribute.name);
|
|
8378
|
+
}
|
|
8379
|
+
/**
|
|
8380
|
+
* @inheritdoc
|
|
8381
|
+
*/
|
|
8382
|
+
get name() {
|
|
8383
|
+
return StandardDirectiveName.V_COMPONENT;
|
|
8384
|
+
}
|
|
8385
|
+
/**
|
|
8386
|
+
* @inheritdoc
|
|
8387
|
+
*/
|
|
8388
|
+
get vNode() {
|
|
8389
|
+
return this.#vNode;
|
|
8390
|
+
}
|
|
8391
|
+
/**
|
|
8392
|
+
* @inheritdoc
|
|
8393
|
+
*/
|
|
8394
|
+
get needsAnchor() {
|
|
8395
|
+
return true;
|
|
8396
|
+
}
|
|
8397
|
+
/**
|
|
8398
|
+
* @inheritdoc
|
|
8399
|
+
*/
|
|
8400
|
+
get bindingsPreparer() {
|
|
8401
|
+
return undefined;
|
|
8402
|
+
}
|
|
8403
|
+
/**
|
|
8404
|
+
* @inheritdoc
|
|
8405
|
+
*/
|
|
8406
|
+
get domUpdater() {
|
|
8407
|
+
// Create and return the DOM updater
|
|
8408
|
+
const updater = {
|
|
8409
|
+
get dependentIdentifiers() {
|
|
8410
|
+
return [];
|
|
8411
|
+
},
|
|
8412
|
+
applyToDOM: () => {
|
|
8413
|
+
this.#render();
|
|
8290
8414
|
}
|
|
8291
|
-
: listener;
|
|
8292
|
-
dispatcher.listeners.add(wrapper);
|
|
8293
|
-
return () => {
|
|
8294
|
-
dispatcher.listeners.delete(wrapper);
|
|
8295
8415
|
};
|
|
8416
|
+
return updater;
|
|
8296
8417
|
}
|
|
8297
8418
|
/**
|
|
8298
|
-
*
|
|
8299
|
-
*
|
|
8300
|
-
* @param obj The object to check.
|
|
8301
|
-
* @returns True if the object is a reactive proxy, false otherwise.
|
|
8419
|
+
* @inheritdoc
|
|
8302
8420
|
*/
|
|
8303
|
-
|
|
8304
|
-
return
|
|
8421
|
+
get templatize() {
|
|
8422
|
+
return true;
|
|
8305
8423
|
}
|
|
8306
8424
|
/**
|
|
8307
|
-
*
|
|
8308
|
-
* If the object is not a proxy, returns it as-is.
|
|
8309
|
-
*
|
|
8310
|
-
* @param obj The object to unwrap.
|
|
8311
|
-
* @returns The original object.
|
|
8425
|
+
* @inheritdoc
|
|
8312
8426
|
*/
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8427
|
+
get dependentIdentifiers() {
|
|
8428
|
+
return [];
|
|
8429
|
+
}
|
|
8430
|
+
/**
|
|
8431
|
+
* @inheritdoc
|
|
8432
|
+
*/
|
|
8433
|
+
get onMount() {
|
|
8434
|
+
return undefined;
|
|
8317
8435
|
}
|
|
8318
8436
|
/**
|
|
8319
|
-
*
|
|
8320
|
-
* Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
|
|
8321
|
-
* This is useful for objects that should not be reactive, such as:
|
|
8322
|
-
* - Objects with private fields (class instances with # fields)
|
|
8323
|
-
* - Third-party library instances
|
|
8324
|
-
* - Objects used only for method calls
|
|
8325
|
-
*
|
|
8326
|
-
* @param obj The object to mark as raw.
|
|
8327
|
-
* @returns The same object (for chaining).
|
|
8437
|
+
* @inheritdoc
|
|
8328
8438
|
*/
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
this.rawObjects.add(obj);
|
|
8332
|
-
}
|
|
8333
|
-
return obj;
|
|
8439
|
+
get onMounted() {
|
|
8440
|
+
return undefined;
|
|
8334
8441
|
}
|
|
8335
8442
|
/**
|
|
8336
|
-
*
|
|
8337
|
-
*
|
|
8338
|
-
* @param obj The object to check.
|
|
8339
|
-
* @returns True if the object is marked as raw, false otherwise.
|
|
8443
|
+
* @inheritdoc
|
|
8340
8444
|
*/
|
|
8341
|
-
|
|
8342
|
-
return
|
|
8445
|
+
get onUpdate() {
|
|
8446
|
+
return undefined;
|
|
8343
8447
|
}
|
|
8344
8448
|
/**
|
|
8345
|
-
*
|
|
8346
|
-
* This is used to map computed property values back to their source paths.
|
|
8347
|
-
* For example, if a computed property returns `model.elements[0]`,
|
|
8348
|
-
* this method returns "model.elements[0]" for that object.
|
|
8349
|
-
*
|
|
8350
|
-
* @param obj The proxy object to get the path for.
|
|
8351
|
-
* @returns The source path, or undefined if not found.
|
|
8449
|
+
* @inheritdoc
|
|
8352
8450
|
*/
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
return undefined;
|
|
8356
|
-
}
|
|
8357
|
-
return this.proxyPaths.get(obj);
|
|
8451
|
+
get onUpdated() {
|
|
8452
|
+
return undefined;
|
|
8358
8453
|
}
|
|
8359
8454
|
/**
|
|
8360
|
-
*
|
|
8361
|
-
* This is used when a variable is assigned to reference an existing reactive object.
|
|
8362
|
-
* For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
|
|
8363
|
-
* so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
|
|
8364
|
-
*
|
|
8365
|
-
* @param aliasPath The alias path (e.g., "editingNestedStep")
|
|
8366
|
-
* @param sourcePath The source path (e.g., "routes[0].steps[0]")
|
|
8455
|
+
* @inheritdoc
|
|
8367
8456
|
*/
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
this.pathAliases.set(aliasPath, sourcePath);
|
|
8371
|
-
}
|
|
8457
|
+
get onUnmount() {
|
|
8458
|
+
return undefined;
|
|
8372
8459
|
}
|
|
8373
8460
|
/**
|
|
8374
|
-
*
|
|
8375
|
-
*
|
|
8376
|
-
* @param aliasPath The alias path to remove
|
|
8461
|
+
* @inheritdoc
|
|
8377
8462
|
*/
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
for (const key of this.pathAliases.keys()) {
|
|
8381
|
-
if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
|
|
8382
|
-
this.pathAliases.delete(key);
|
|
8383
|
-
}
|
|
8384
|
-
}
|
|
8463
|
+
get onUnmounted() {
|
|
8464
|
+
return undefined;
|
|
8385
8465
|
}
|
|
8386
8466
|
/**
|
|
8387
|
-
*
|
|
8388
|
-
* Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
|
|
8389
|
-
*
|
|
8390
|
-
* @param aliasPath The alias path to resolve
|
|
8391
|
-
* @returns The source path, or undefined if no alias exists
|
|
8467
|
+
* @inheritdoc
|
|
8392
8468
|
*/
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
return
|
|
8469
|
+
destroy() {
|
|
8470
|
+
if (!this.#componentApp) {
|
|
8471
|
+
// Not rendered, no action needed
|
|
8472
|
+
return;
|
|
8397
8473
|
}
|
|
8398
|
-
//
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8474
|
+
// Get the component's root VNode
|
|
8475
|
+
const componentVNode = this.#componentApp.rootVNode;
|
|
8476
|
+
// Destroy component application first (calls @unmount hooks while DOM is still accessible)
|
|
8477
|
+
this.#componentApp.unmount();
|
|
8478
|
+
// Remove the component's root node from the DOM
|
|
8479
|
+
if (componentVNode?.node.parentNode) {
|
|
8480
|
+
componentVNode.node.parentNode.removeChild(componentVNode.node);
|
|
8405
8481
|
}
|
|
8406
|
-
|
|
8482
|
+
this.#componentApp = undefined;
|
|
8407
8483
|
}
|
|
8408
8484
|
/**
|
|
8409
|
-
*
|
|
8410
|
-
* For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
|
|
8411
|
-
* then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
|
|
8412
|
-
*
|
|
8413
|
-
* @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
|
|
8414
|
-
* @param identifier The identifier to check (e.g., "editingNestedStep.steps")
|
|
8415
|
-
* @returns True if the change path matches the identifier
|
|
8485
|
+
* Renders the component.
|
|
8416
8486
|
*/
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
return
|
|
8487
|
+
#render() {
|
|
8488
|
+
if (this.#componentApp) {
|
|
8489
|
+
// Already rendered, no action needed
|
|
8490
|
+
return;
|
|
8421
8491
|
}
|
|
8422
|
-
//
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8492
|
+
// Clone the component's template and replace the original node
|
|
8493
|
+
const clone = this.#cloneNode();
|
|
8494
|
+
this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
|
|
8495
|
+
// Get properties from :options or :options.component directive
|
|
8496
|
+
let properties = {};
|
|
8497
|
+
const optionsDirective = this.#vNode.directiveManager?.optionsDirective('component');
|
|
8498
|
+
if (optionsDirective && optionsDirective.expression && this.#vNode.bindings) {
|
|
8499
|
+
// Evaluate the options expression using ExpressionEvaluator
|
|
8500
|
+
const evaluator = ExpressionEvaluator.create(optionsDirective.expression, this.#vNode.bindings, this.#vNode.vApplication.functionDependencies);
|
|
8501
|
+
const result = evaluator.evaluate();
|
|
8502
|
+
if (typeof result === 'object' && result !== null) {
|
|
8503
|
+
properties = result;
|
|
8504
|
+
}
|
|
8427
8505
|
}
|
|
8428
|
-
|
|
8506
|
+
// Get component definition from the application's component registry
|
|
8507
|
+
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
8508
|
+
if (!component) {
|
|
8509
|
+
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
8510
|
+
}
|
|
8511
|
+
// Create component instance
|
|
8512
|
+
const instance = component.createInstance(properties);
|
|
8513
|
+
// Create and mount child application using the parent application's registries
|
|
8514
|
+
this.#componentApp = this.#vNode.vApplication.createChildApp(instance);
|
|
8515
|
+
this.#componentApp.mount(clone);
|
|
8429
8516
|
}
|
|
8430
8517
|
/**
|
|
8431
|
-
*
|
|
8432
|
-
*
|
|
8433
|
-
*
|
|
8434
|
-
* one index or key.
|
|
8435
|
-
*
|
|
8436
|
-
* @param changePath The path that was changed (e.g., "items[2].name")
|
|
8437
|
-
* @param identifier The identifier to check (e.g., "items[*].name")
|
|
8438
|
-
* @param descendants Whether a change below the identifier also matches
|
|
8439
|
-
* @returns True if the change path matches the identifier
|
|
8518
|
+
* Clones the component's template and returns the root element.
|
|
8519
|
+
* @returns The cloned root HTMLElement of the component.
|
|
8520
|
+
* @throws Error if the component or its template is not found.
|
|
8440
8521
|
*/
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8522
|
+
#cloneNode() {
|
|
8523
|
+
// Get component definition from the application's component registry
|
|
8524
|
+
const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
|
|
8525
|
+
if (!component) {
|
|
8526
|
+
throw new Error(`Component '${this.#componentId}' not found in registry`);
|
|
8444
8527
|
}
|
|
8445
|
-
|
|
8446
|
-
|
|
8528
|
+
// Get template element
|
|
8529
|
+
const finalTemplateID = component.templateID || component.id;
|
|
8530
|
+
const templateElement = document.querySelector(`#${finalTemplateID}`);
|
|
8531
|
+
if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
|
|
8532
|
+
throw new Error(`Template element '#${finalTemplateID}' not found`);
|
|
8447
8533
|
}
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
this.pathPatterns.set(cacheKey, pattern);
|
|
8534
|
+
// Clone template content
|
|
8535
|
+
const fragment = templateElement.content.cloneNode(true);
|
|
8536
|
+
const childNodes = Array.from(fragment.childNodes);
|
|
8537
|
+
// Find the first element node
|
|
8538
|
+
for (const node of childNodes) {
|
|
8539
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
8540
|
+
return node;
|
|
8541
|
+
}
|
|
8457
8542
|
}
|
|
8458
|
-
|
|
8543
|
+
throw new Error(`No element found in template '#${finalTemplateID}'`);
|
|
8459
8544
|
}
|
|
8460
8545
|
}
|
|
8461
8546
|
|
|
@@ -8486,6 +8571,13 @@ class VBindings {
|
|
|
8486
8571
|
* The set of changed identifiers.
|
|
8487
8572
|
*/
|
|
8488
8573
|
#changes = new Set();
|
|
8574
|
+
/**
|
|
8575
|
+
* On the root bindings: every descendant scope (a v-for row, a component) that
|
|
8576
|
+
* recorded a change since the last clearChanges(). The root clears their change
|
|
8577
|
+
* sets together with its own at the end of the update cycle, so a loop variable
|
|
8578
|
+
* set when a row was created does not count as changed in every later cycle.
|
|
8579
|
+
*/
|
|
8580
|
+
#dirtyScopes;
|
|
8489
8581
|
/**
|
|
8490
8582
|
* Cache for array lengths to detect length changes when the same object reference is used.
|
|
8491
8583
|
*/
|
|
@@ -8628,7 +8720,7 @@ class VBindings {
|
|
|
8628
8720
|
for (const part of changedPath?.split('.') || []) {
|
|
8629
8721
|
path = path ? `${path}.${part}` : part;
|
|
8630
8722
|
this.#logger?.debug(`Binding changed: ${path}`);
|
|
8631
|
-
this.#
|
|
8723
|
+
this.#addChange(path);
|
|
8632
8724
|
}
|
|
8633
8725
|
if (!this.#suppressOnChange) {
|
|
8634
8726
|
this.#onChange?.(changedPath);
|
|
@@ -8668,7 +8760,7 @@ class VBindings {
|
|
|
8668
8760
|
for (const part of changedPath.split('.')) {
|
|
8669
8761
|
path = path ? `${path}.${part}` : part;
|
|
8670
8762
|
this.#logger?.debug(`Binding changed (external): ${path}`);
|
|
8671
|
-
this.#
|
|
8763
|
+
this.#addChange(path);
|
|
8672
8764
|
}
|
|
8673
8765
|
if (!this.#suppressOnChange) {
|
|
8674
8766
|
this.#onChange?.(changedPath);
|
|
@@ -8695,7 +8787,7 @@ class VBindings {
|
|
|
8695
8787
|
const newValuePreview = newValueString.length > 100 ? `${newValueString.substring(0, 100)}...` : newValueString;
|
|
8696
8788
|
this.#logger.debug(`Binding set on ${target === obj ? 'local' : 'parent'}: ${key}: ${oldValuePreview} -> ${newValuePreview}`);
|
|
8697
8789
|
}
|
|
8698
|
-
this.#
|
|
8790
|
+
this.#addChange(key);
|
|
8699
8791
|
if (!this.#suppressOnChange) {
|
|
8700
8792
|
this.#onChange?.(key);
|
|
8701
8793
|
}
|
|
@@ -8710,7 +8802,7 @@ class VBindings {
|
|
|
8710
8802
|
this.#externalSubscriptions.delete(key);
|
|
8711
8803
|
}
|
|
8712
8804
|
this.#logger?.debug(`Binding deleted: ${key}`);
|
|
8713
|
-
this.#
|
|
8805
|
+
this.#addChange(key);
|
|
8714
8806
|
this.#onChange?.(key);
|
|
8715
8807
|
return result;
|
|
8716
8808
|
}
|
|
@@ -8751,6 +8843,27 @@ class VBindings {
|
|
|
8751
8843
|
*/
|
|
8752
8844
|
clearChanges() {
|
|
8753
8845
|
this.#changes.clear();
|
|
8846
|
+
if (this.#dirtyScopes) {
|
|
8847
|
+
for (const scope of this.#dirtyScopes) {
|
|
8848
|
+
scope.#changes.clear();
|
|
8849
|
+
}
|
|
8850
|
+
this.#dirtyScopes.clear();
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
/**
|
|
8854
|
+
* Records a changed identifier on this scope and, for a non-root scope, registers the
|
|
8855
|
+
* scope with the root so that the change is cleared at the end of the update cycle.
|
|
8856
|
+
*/
|
|
8857
|
+
#addChange(key) {
|
|
8858
|
+
this.#changes.add(key);
|
|
8859
|
+
if (!this.#parent) {
|
|
8860
|
+
return;
|
|
8861
|
+
}
|
|
8862
|
+
let root = this;
|
|
8863
|
+
while (root.#parent) {
|
|
8864
|
+
root = root.#parent;
|
|
8865
|
+
}
|
|
8866
|
+
(root.#dirtyScopes ??= new Set()).add(this);
|
|
8754
8867
|
}
|
|
8755
8868
|
/**
|
|
8756
8869
|
* Sets a binding value.
|
|
@@ -8926,7 +9039,7 @@ class VBindings {
|
|
|
8926
9039
|
* @param key The identifier to mark as changed.
|
|
8927
9040
|
*/
|
|
8928
9041
|
markChanged(key) {
|
|
8929
|
-
this.#
|
|
9042
|
+
this.#addChange(key);
|
|
8930
9043
|
}
|
|
8931
9044
|
/**
|
|
8932
9045
|
* Resolves a local path alias for the given identifier by walking up the bindings chain.
|
|
@@ -8973,6 +9086,54 @@ class VBindings {
|
|
|
8973
9086
|
// Fall back to global alias resolution (computed properties, etc.)
|
|
8974
9087
|
return ReactiveProxy.doesChangeMatchIdentifier(changePath, identifier);
|
|
8975
9088
|
}
|
|
9089
|
+
/**
|
|
9090
|
+
* Checks whether any of the changes affects an expression with the given identifiers.
|
|
9091
|
+
*
|
|
9092
|
+
* An identifier with a run-time index ("frames[*].url" for frames[i].url) normally
|
|
9093
|
+
* matches a change at any index. When trackedPaths holds the paths the expression read
|
|
9094
|
+
* during its last evaluation, the identifier is narrowed to the concrete paths among
|
|
9095
|
+
* them ("frames[0].url"), so a write to another element is ignored. When no recorded
|
|
9096
|
+
* path fits the identifier (not evaluated yet, a raw object, a short-circuited read),
|
|
9097
|
+
* the identifier keeps matching every index.
|
|
9098
|
+
*
|
|
9099
|
+
* @param changes The change paths of the current update cycle.
|
|
9100
|
+
* @param identifiers The identifiers the expression depends on.
|
|
9101
|
+
* @param trackedPaths The paths read during the expression's last evaluation, if recorded.
|
|
9102
|
+
* @returns True if some change matches some identifier.
|
|
9103
|
+
*/
|
|
9104
|
+
isAffected(changes, identifiers, trackedPaths) {
|
|
9105
|
+
if (changes.length === 0) {
|
|
9106
|
+
return false;
|
|
9107
|
+
}
|
|
9108
|
+
for (const identifier of identifiers) {
|
|
9109
|
+
const candidates = this.#concretePaths(identifier, trackedPaths) ?? [identifier];
|
|
9110
|
+
for (const candidate of candidates) {
|
|
9111
|
+
if (changes.some(change => this.doesChangeMatchIdentifier(change, candidate))) {
|
|
9112
|
+
return true;
|
|
9113
|
+
}
|
|
9114
|
+
}
|
|
9115
|
+
}
|
|
9116
|
+
return false;
|
|
9117
|
+
}
|
|
9118
|
+
/**
|
|
9119
|
+
* Returns the recorded paths that an identifier with a run-time index stood for during
|
|
9120
|
+
* the last evaluation, or undefined when the identifier has no such index or nothing
|
|
9121
|
+
* recorded fits it. The identifier is tried both as spelled and resolved through path
|
|
9122
|
+
* aliases, since recorded paths are source paths.
|
|
9123
|
+
*/
|
|
9124
|
+
#concretePaths(identifier, trackedPaths) {
|
|
9125
|
+
if (!trackedPaths || trackedPaths.size === 0 || !identifier.includes('[*]')) {
|
|
9126
|
+
return undefined;
|
|
9127
|
+
}
|
|
9128
|
+
const resolved = this.resolveAlias(identifier);
|
|
9129
|
+
const paths = [];
|
|
9130
|
+
for (const path of trackedPaths) {
|
|
9131
|
+
if (ReactiveProxy.pathMatches(path, identifier) || (resolved !== undefined && ReactiveProxy.pathMatches(path, resolved))) {
|
|
9132
|
+
paths.push(path);
|
|
9133
|
+
}
|
|
9134
|
+
}
|
|
9135
|
+
return paths.length > 0 ? paths : undefined;
|
|
9136
|
+
}
|
|
8976
9137
|
}
|
|
8977
9138
|
|
|
8978
9139
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
@@ -9167,10 +9328,14 @@ class VSlotOutletDirective {
|
|
|
9167
9328
|
*/
|
|
9168
9329
|
get domUpdater() {
|
|
9169
9330
|
const identifiers = this.dependentIdentifiers;
|
|
9331
|
+
const directive = this;
|
|
9170
9332
|
const updater = {
|
|
9171
9333
|
get dependentIdentifiers() {
|
|
9172
9334
|
return identifiers;
|
|
9173
9335
|
},
|
|
9336
|
+
get trackedPaths() {
|
|
9337
|
+
return directive.#collectTrackedPaths();
|
|
9338
|
+
},
|
|
9174
9339
|
applyToDOM: () => {
|
|
9175
9340
|
this.#refresh();
|
|
9176
9341
|
}
|
|
@@ -9193,6 +9358,21 @@ class VSlotOutletDirective {
|
|
|
9193
9358
|
}
|
|
9194
9359
|
return Array.from(ids);
|
|
9195
9360
|
}
|
|
9361
|
+
/**
|
|
9362
|
+
* The paths the prop expressions read during their last evaluation, united across
|
|
9363
|
+
* props, or undefined when no prop expression has a run-time index.
|
|
9364
|
+
*/
|
|
9365
|
+
#collectTrackedPaths() {
|
|
9366
|
+
let paths;
|
|
9367
|
+
for (const prop of this.#props) {
|
|
9368
|
+
const tracked = prop.evaluator?.trackedPaths;
|
|
9369
|
+
if (tracked) {
|
|
9370
|
+
paths ??= new Set();
|
|
9371
|
+
tracked.forEach(path => paths.add(path));
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
return paths;
|
|
9375
|
+
}
|
|
9196
9376
|
/**
|
|
9197
9377
|
* @inheritdoc
|
|
9198
9378
|
*
|
|
@@ -9998,6 +10178,11 @@ class VTextEvaluator {
|
|
|
9998
10178
|
* The function used to evaluate the text with current bindings.
|
|
9999
10179
|
*/
|
|
10000
10180
|
#evaluate;
|
|
10181
|
+
/**
|
|
10182
|
+
* The source paths read through reactive proxies during the last evaluation,
|
|
10183
|
+
* recorded only when some expression has a run-time index ("items[i].name").
|
|
10184
|
+
*/
|
|
10185
|
+
#trackedPaths;
|
|
10001
10186
|
/**
|
|
10002
10187
|
* Constructs a VTextEvaluator instance.
|
|
10003
10188
|
* @param text The text containing embedded expressions in the form of {{...}}.
|
|
@@ -10038,8 +10223,10 @@ class VTextEvaluator {
|
|
|
10038
10223
|
func: new Function(args, funcBody)
|
|
10039
10224
|
};
|
|
10040
10225
|
});
|
|
10226
|
+
// Only an expression with an index known at run time needs its reads recorded
|
|
10227
|
+
const hasDynamicIndex = this.#identifiers.some(id => id.includes('[*]'));
|
|
10041
10228
|
// Create a function to reconstruct the text with evaluated expressions
|
|
10042
|
-
|
|
10229
|
+
const evaluateText = (bindings) => {
|
|
10043
10230
|
let result = text;
|
|
10044
10231
|
evaluators.forEach((evaluator, i) => {
|
|
10045
10232
|
// Build values for baseArgs (no dots) in the same order as function parameters
|
|
@@ -10057,6 +10244,15 @@ class VTextEvaluator {
|
|
|
10057
10244
|
});
|
|
10058
10245
|
return result;
|
|
10059
10246
|
};
|
|
10247
|
+
this.#evaluate = hasDynamicIndex
|
|
10248
|
+
? (bindings) => {
|
|
10249
|
+
const paths = new Set();
|
|
10250
|
+
this.#trackedPaths = undefined;
|
|
10251
|
+
const result = ReactiveProxy.track(paths, () => evaluateText(bindings));
|
|
10252
|
+
this.#trackedPaths = paths;
|
|
10253
|
+
return result;
|
|
10254
|
+
}
|
|
10255
|
+
: evaluateText;
|
|
10060
10256
|
}
|
|
10061
10257
|
/**
|
|
10062
10258
|
* Checks if the given text contains any expressions in the form of {{...}}.
|
|
@@ -10073,6 +10269,14 @@ class VTextEvaluator {
|
|
|
10073
10269
|
get identifiers() {
|
|
10074
10270
|
return this.#identifiers;
|
|
10075
10271
|
}
|
|
10272
|
+
/**
|
|
10273
|
+
* The source paths read through reactive proxies during the last evaluation.
|
|
10274
|
+
* Recorded only when an expression has a run-time index ("items[i].name");
|
|
10275
|
+
* undefined before the first evaluation and for texts without such an index.
|
|
10276
|
+
*/
|
|
10277
|
+
get trackedPaths() {
|
|
10278
|
+
return this.#trackedPaths;
|
|
10279
|
+
}
|
|
10076
10280
|
/**
|
|
10077
10281
|
* Evaluates the text with the provided bindings and returns the resulting string.
|
|
10078
10282
|
* @param bindings The bindings to use for evaluating the expressions.
|
|
@@ -10562,7 +10766,7 @@ class VNode {
|
|
|
10562
10766
|
if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
|
|
10563
10767
|
// If this is a text node with a text evaluator, update its content if needed
|
|
10564
10768
|
// Check if any of the identifiers are in the changed identifiers (considering path aliases)
|
|
10565
|
-
const changed = this
|
|
10769
|
+
const changed = this.bindings.isAffected(changes, this.#textEvaluator.identifiers, this.#textEvaluator.trackedPaths);
|
|
10566
10770
|
// If the text node has changed, update its content
|
|
10567
10771
|
if (changed) {
|
|
10568
10772
|
const text = this.#node;
|
|
@@ -10585,7 +10789,7 @@ class VNode {
|
|
|
10585
10789
|
}
|
|
10586
10790
|
// Prepare bindings for each preparer if relevant identifiers have changed
|
|
10587
10791
|
for (const preparer of this.#directiveManager.bindingsPreparers) {
|
|
10588
|
-
const changed =
|
|
10792
|
+
const changed = this.bindings.isAffected(changes, preparer.dependentIdentifiers, preparer.trackedPaths);
|
|
10589
10793
|
if (changed) {
|
|
10590
10794
|
preparer.prepareBindings();
|
|
10591
10795
|
}
|
|
@@ -10594,7 +10798,7 @@ class VNode {
|
|
|
10594
10798
|
// Apply DOM updaters from directives, if any
|
|
10595
10799
|
if (this.#directiveManager?.domUpdaters) {
|
|
10596
10800
|
for (const updater of this.#directiveManager.domUpdaters) {
|
|
10597
|
-
const changed =
|
|
10801
|
+
const changed = this.bindings.isAffected(changes, updater.dependentIdentifiers, updater.trackedPaths);
|
|
10598
10802
|
if (changed) {
|
|
10599
10803
|
updater.applyToDOM();
|
|
10600
10804
|
}
|
|
@@ -10847,6 +11051,21 @@ class VConditionalDirectiveContext {
|
|
|
10847
11051
|
get allDependentIdentifiers() {
|
|
10848
11052
|
return this.#allDependentIdentifiers;
|
|
10849
11053
|
}
|
|
11054
|
+
/**
|
|
11055
|
+
* The paths the conditions of the chain read during their last evaluation, united
|
|
11056
|
+
* across directives, or undefined when no condition has a run-time index.
|
|
11057
|
+
*/
|
|
11058
|
+
get trackedPaths() {
|
|
11059
|
+
let paths;
|
|
11060
|
+
for (const directive of this.#directives) {
|
|
11061
|
+
const tracked = directive.trackedPaths;
|
|
11062
|
+
if (tracked) {
|
|
11063
|
+
paths ??= new Set();
|
|
11064
|
+
tracked.forEach(path => paths.add(path));
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
return paths;
|
|
11068
|
+
}
|
|
10850
11069
|
/**
|
|
10851
11070
|
* Adds a directive (v-else-if or v-else) to the conditional context.
|
|
10852
11071
|
* @param directive The directive to add.
|
|
@@ -11043,11 +11262,15 @@ class VConditionalDirective {
|
|
|
11043
11262
|
*/
|
|
11044
11263
|
get domUpdater() {
|
|
11045
11264
|
const identifiers = this.#conditionalContext.allDependentIdentifiers;
|
|
11265
|
+
const conditionalContext = this.#conditionalContext;
|
|
11046
11266
|
// Create an updater that handles the conditional rendering
|
|
11047
11267
|
const updater = {
|
|
11048
11268
|
get dependentIdentifiers() {
|
|
11049
11269
|
return identifiers;
|
|
11050
11270
|
},
|
|
11271
|
+
get trackedPaths() {
|
|
11272
|
+
return conditionalContext.trackedPaths;
|
|
11273
|
+
},
|
|
11051
11274
|
applyToDOM: () => {
|
|
11052
11275
|
this.#render();
|
|
11053
11276
|
}
|
|
@@ -11066,6 +11289,13 @@ class VConditionalDirective {
|
|
|
11066
11289
|
get dependentIdentifiers() {
|
|
11067
11290
|
return this.#evaluator?.dependentIdentifiers ?? [];
|
|
11068
11291
|
}
|
|
11292
|
+
/**
|
|
11293
|
+
* The paths the condition read during its last evaluation, when it has an index
|
|
11294
|
+
* known only at run time (see ExpressionEvaluator.trackedPaths).
|
|
11295
|
+
*/
|
|
11296
|
+
get trackedPaths() {
|
|
11297
|
+
return this.#evaluator?.trackedPaths;
|
|
11298
|
+
}
|
|
11069
11299
|
/**
|
|
11070
11300
|
* The context for managing related conditional directives (v-if, v-else-if, v-else).
|
|
11071
11301
|
*/
|
|
@@ -11432,11 +11662,15 @@ class VForDirective {
|
|
|
11432
11662
|
*/
|
|
11433
11663
|
get domUpdater() {
|
|
11434
11664
|
const identifiers = this.#sourceEvaluator?.dependentIdentifiers ?? [];
|
|
11665
|
+
const sourceEvaluator = this.#sourceEvaluator;
|
|
11435
11666
|
// Create and return the DOM updater
|
|
11436
11667
|
const updater = {
|
|
11437
11668
|
get dependentIdentifiers() {
|
|
11438
11669
|
return identifiers;
|
|
11439
11670
|
},
|
|
11671
|
+
get trackedPaths() {
|
|
11672
|
+
return sourceEvaluator?.trackedPaths;
|
|
11673
|
+
},
|
|
11440
11674
|
applyToDOM: () => {
|
|
11441
11675
|
this.#render();
|
|
11442
11676
|
}
|
|
@@ -12156,6 +12390,9 @@ class VHtmlDirective {
|
|
|
12156
12390
|
get dependentIdentifiers() {
|
|
12157
12391
|
return identifiers;
|
|
12158
12392
|
},
|
|
12393
|
+
get trackedPaths() {
|
|
12394
|
+
return evaluator.trackedPaths;
|
|
12395
|
+
},
|
|
12159
12396
|
applyToDOM: () => {
|
|
12160
12397
|
const element = vNode.node;
|
|
12161
12398
|
const htmlContent = evaluator.evaluate() ?? '';
|
|
@@ -12582,6 +12819,9 @@ class VModelDirective {
|
|
|
12582
12819
|
get dependentIdentifiers() {
|
|
12583
12820
|
return self.#collectDependentIdentifiers();
|
|
12584
12821
|
},
|
|
12822
|
+
get trackedPaths() {
|
|
12823
|
+
return self.#evaluator?.trackedPaths;
|
|
12824
|
+
},
|
|
12585
12825
|
applyToDOM: () => {
|
|
12586
12826
|
self.#render();
|
|
12587
12827
|
}
|
|
@@ -13980,6 +14220,9 @@ class VShowDirective {
|
|
|
13980
14220
|
get dependentIdentifiers() {
|
|
13981
14221
|
return identifiers;
|
|
13982
14222
|
},
|
|
14223
|
+
get trackedPaths() {
|
|
14224
|
+
return evaluator.trackedPaths;
|
|
14225
|
+
},
|
|
13983
14226
|
applyToDOM() {
|
|
13984
14227
|
const shouldRender = evaluator.evaluateAsBoolean();
|
|
13985
14228
|
if (shouldRender) {
|
|
@@ -14147,6 +14390,9 @@ class VTextDirective {
|
|
|
14147
14390
|
get dependentIdentifiers() {
|
|
14148
14391
|
return identifiers;
|
|
14149
14392
|
},
|
|
14393
|
+
get trackedPaths() {
|
|
14394
|
+
return evaluator.trackedPaths;
|
|
14395
|
+
},
|
|
14150
14396
|
applyToDOM() {
|
|
14151
14397
|
const element = vNode.node;
|
|
14152
14398
|
element.textContent = toDisplayString(evaluator.evaluate());
|
|
@@ -14309,6 +14555,9 @@ class VFocusDirective {
|
|
|
14309
14555
|
get dependentIdentifiers() {
|
|
14310
14556
|
return identifiers;
|
|
14311
14557
|
},
|
|
14558
|
+
get trackedPaths() {
|
|
14559
|
+
return evaluator.trackedPaths;
|
|
14560
|
+
},
|
|
14312
14561
|
applyToDOM: () => {
|
|
14313
14562
|
const value = evaluator.evaluateAsBoolean();
|
|
14314
14563
|
const previous = this.#previousValue;
|