@mintjamsinc/ichigojs 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ichigo.esm.js +1062 -687
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.esm.min.js.map +1 -1
- package/dist/ichigo.umd.js +1062 -687
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/ichigo.umd.min.js.map +1 -1
- package/dist/types/ichigo/VApplication.d.ts +1 -19
- package/dist/types/ichigo/VBindings.d.ts +53 -3
- package/dist/types/ichigo/VBindingsInit.d.ts +15 -0
- package/dist/types/ichigo/VBindingsPreparer.d.ts +4 -6
- package/dist/types/ichigo/VDOMUpdater.d.ts +2 -2
- package/dist/types/ichigo/VNode.d.ts +34 -8
- package/dist/types/ichigo/VNodeInit.d.ts +6 -5
- package/dist/types/ichigo/directives/VBindDirective.d.ts +8 -0
- package/dist/types/ichigo/directives/VConditionalDirective.d.ts +8 -0
- package/dist/types/ichigo/directives/VConditionalDirectiveContext.d.ts +5 -0
- package/dist/types/ichigo/directives/VDirective.d.ts +17 -0
- package/dist/types/ichigo/directives/VForDirective.d.ts +8 -0
- package/dist/types/ichigo/directives/VModelDirective.d.ts +8 -0
- package/dist/types/ichigo/directives/VOnDirective.d.ts +8 -0
- package/dist/types/ichigo/directives/VShowDirective.d.ts +8 -0
- package/dist/types/ichigo/util/ReactiveProxy.d.ts +6 -5
- package/package.json +2 -2
package/dist/ichigo.esm.js
CHANGED
@@ -6759,7 +6759,7 @@ class VBindDirective {
|
|
6759
6759
|
/**
|
6760
6760
|
* A list of variable and function names used in the directive's expression.
|
6761
6761
|
*/
|
6762
|
-
#
|
6762
|
+
#dependentIdentifiers;
|
6763
6763
|
/**
|
6764
6764
|
* A function that evaluates the directive's expression.
|
6765
6765
|
* It returns the evaluated value of the expression.
|
@@ -6773,6 +6773,16 @@ class VBindDirective {
|
|
6773
6773
|
* The original expression string from the directive.
|
6774
6774
|
*/
|
6775
6775
|
#expression;
|
6776
|
+
/**
|
6777
|
+
* The set of class names managed by this directive (used when binding to the "class" attribute).
|
6778
|
+
* This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
|
6779
|
+
*/
|
6780
|
+
#managedClasses = new Set();
|
6781
|
+
/**
|
6782
|
+
* The set of style properties managed by this directive (used when binding to the "style" attribute).
|
6783
|
+
* This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
|
6784
|
+
*/
|
6785
|
+
#managedStyles = new Set();
|
6776
6786
|
/**
|
6777
6787
|
* @param context The context for parsing the directive.
|
6778
6788
|
*/
|
@@ -6790,7 +6800,7 @@ class VBindDirective {
|
|
6790
6800
|
// Parse the expression to extract identifiers and create the evaluator
|
6791
6801
|
this.#expression = context.attribute.value;
|
6792
6802
|
if (this.#expression) {
|
6793
|
-
this.#
|
6803
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(this.#expression, context.vNode.vApplication.functionDependencies);
|
6794
6804
|
this.#evaluate = this.#createEvaluator(this.#expression);
|
6795
6805
|
}
|
6796
6806
|
// Remove the directive attribute from the element
|
@@ -6824,11 +6834,11 @@ class VBindDirective {
|
|
6824
6834
|
* @inheritdoc
|
6825
6835
|
*/
|
6826
6836
|
get domUpdater() {
|
6827
|
-
const identifiers = this.#
|
6837
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
6828
6838
|
const render = () => this.#render();
|
6829
6839
|
// Create an updater that handles the attribute binding
|
6830
6840
|
const updater = {
|
6831
|
-
get
|
6841
|
+
get dependentIdentifiers() {
|
6832
6842
|
return identifiers;
|
6833
6843
|
},
|
6834
6844
|
applyToDOM() {
|
@@ -6837,6 +6847,18 @@ class VBindDirective {
|
|
6837
6847
|
};
|
6838
6848
|
return updater;
|
6839
6849
|
}
|
6850
|
+
/**
|
6851
|
+
* @inheritdoc
|
6852
|
+
*/
|
6853
|
+
get templatize() {
|
6854
|
+
return false;
|
6855
|
+
}
|
6856
|
+
/**
|
6857
|
+
* @inheritdoc
|
6858
|
+
*/
|
6859
|
+
get dependentIdentifiers() {
|
6860
|
+
return this.#dependentIdentifiers ?? [];
|
6861
|
+
}
|
6840
6862
|
/**
|
6841
6863
|
* Indicates if this directive is binding the "key" attribute.
|
6842
6864
|
* The "key" attribute is special and is used for optimizing rendering of lists.
|
@@ -6894,39 +6916,54 @@ class VBindDirective {
|
|
6894
6916
|
* Updates the class attribute with support for string, array, and object formats.
|
6895
6917
|
*/
|
6896
6918
|
#updateClass(element, value) {
|
6897
|
-
//
|
6898
|
-
|
6919
|
+
// Determine the new set of classes to apply
|
6920
|
+
let newClasses = [];
|
6899
6921
|
if (typeof value === 'string') {
|
6900
|
-
|
6922
|
+
newClasses = value.split(/\s+/).filter(Boolean);
|
6901
6923
|
}
|
6902
6924
|
else if (Array.isArray(value)) {
|
6903
|
-
|
6925
|
+
newClasses = value.filter(Boolean);
|
6904
6926
|
}
|
6905
6927
|
else if (typeof value === 'object' && value !== null) {
|
6906
|
-
|
6907
|
-
element.className = classes.join(' ');
|
6928
|
+
newClasses = Object.keys(value).filter(key => value[key]);
|
6908
6929
|
}
|
6930
|
+
// Remove previously managed classes
|
6931
|
+
this.#managedClasses.forEach(cls => element.classList.remove(cls));
|
6932
|
+
// Add newly managed classes
|
6933
|
+
newClasses.forEach(cls => element.classList.add(cls));
|
6934
|
+
// Update managed classes list
|
6935
|
+
this.#managedClasses = new Set(newClasses);
|
6909
6936
|
}
|
6910
6937
|
/**
|
6911
6938
|
* Updates the style attribute with support for object format.
|
6912
6939
|
*/
|
6913
6940
|
#updateStyle(element, value) {
|
6941
|
+
let newStyles = [];
|
6914
6942
|
if (typeof value === 'string') {
|
6943
|
+
// Directly set the style string
|
6915
6944
|
element.style.cssText = value;
|
6945
|
+
// Extract managed properties
|
6946
|
+
newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
|
6916
6947
|
}
|
6917
6948
|
else if (typeof value === 'object' && value !== null) {
|
6918
|
-
//
|
6919
|
-
|
6949
|
+
// Remove all previously managed properties
|
6950
|
+
this.#managedStyles.forEach(prop => {
|
6951
|
+
element.style.removeProperty(this.#camelToKebab(prop));
|
6952
|
+
});
|
6953
|
+
// Add newly managed properties
|
6920
6954
|
for (const key in value) {
|
6921
6955
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
6922
6956
|
const cssKey = this.#camelToKebab(key);
|
6923
6957
|
const cssValue = value[key];
|
6924
6958
|
if (cssValue != null) {
|
6925
6959
|
element.style.setProperty(cssKey, String(cssValue));
|
6960
|
+
newStyles.push(key);
|
6926
6961
|
}
|
6927
6962
|
}
|
6928
6963
|
}
|
6929
6964
|
}
|
6965
|
+
// Update managed properties list
|
6966
|
+
this.#managedStyles = new Set(newStyles);
|
6930
6967
|
}
|
6931
6968
|
/**
|
6932
6969
|
* Converts camelCase to kebab-case for CSS properties.
|
@@ -6986,7 +7023,7 @@ class VBindDirective {
|
|
6986
7023
|
* @returns A function that evaluates the directive's condition.
|
6987
7024
|
*/
|
6988
7025
|
#createEvaluator(expression) {
|
6989
|
-
const identifiers = this.#
|
7026
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
6990
7027
|
const args = identifiers.join(", ");
|
6991
7028
|
const funcBody = `return (${expression});`;
|
6992
7029
|
// Create a dynamic function with the identifiers as parameters
|
@@ -6994,7 +7031,7 @@ class VBindDirective {
|
|
6994
7031
|
// Return a function that calls the dynamic function with the current values from the virtual node's bindings
|
6995
7032
|
return () => {
|
6996
7033
|
// Gather the current values of the identifiers from the bindings
|
6997
|
-
const values = identifiers.map(id => this.#vNode.bindings?.
|
7034
|
+
const values = identifiers.map(id => this.#vNode.bindings?.get(id));
|
6998
7035
|
// Call the dynamic function with the gathered values and return the result as a boolean
|
6999
7036
|
return func(...values);
|
7000
7037
|
};
|
@@ -7003,304 +7040,273 @@ class VBindDirective {
|
|
7003
7040
|
|
7004
7041
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7005
7042
|
/**
|
7006
|
-
*
|
7043
|
+
* Utility class for creating reactive proxies that automatically track changes.
|
7007
7044
|
*/
|
7008
|
-
class
|
7009
|
-
#directives = [];
|
7045
|
+
class ReactiveProxy {
|
7010
7046
|
/**
|
7011
|
-
*
|
7012
|
-
*
|
7047
|
+
* A WeakMap to store the proxy for each target object and path combination.
|
7048
|
+
* This prevents creating multiple proxies for the same object accessed from different paths.
|
7013
7049
|
*/
|
7014
|
-
|
7015
|
-
this.#directives.push(directive);
|
7016
|
-
}
|
7050
|
+
static proxyCache = new WeakMap();
|
7017
7051
|
/**
|
7018
|
-
*
|
7019
|
-
*
|
7020
|
-
*
|
7021
|
-
* @
|
7052
|
+
* Creates a reactive proxy for the given object.
|
7053
|
+
* The proxy will call the onChange callback whenever a property is modified.
|
7054
|
+
*
|
7055
|
+
* @param target The object to make reactive.
|
7056
|
+
* @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
|
7057
|
+
* @param path The current path in the object tree (used internally for nested objects).
|
7058
|
+
* @returns A reactive proxy of the target object.
|
7022
7059
|
*/
|
7023
|
-
|
7024
|
-
|
7025
|
-
if (
|
7026
|
-
|
7060
|
+
static create(target, onChange, path = '') {
|
7061
|
+
// If the target is not an object or is null, return it as-is
|
7062
|
+
if (typeof target !== 'object' || target === null) {
|
7063
|
+
return target;
|
7027
7064
|
}
|
7028
|
-
// Check if
|
7029
|
-
|
7030
|
-
|
7031
|
-
|
7032
|
-
|
7065
|
+
// Check if we already have a proxy for this target with this path
|
7066
|
+
let pathMap = this.proxyCache.get(target);
|
7067
|
+
if (pathMap) {
|
7068
|
+
const existingProxy = pathMap.get(path);
|
7069
|
+
if (existingProxy) {
|
7070
|
+
return existingProxy;
|
7033
7071
|
}
|
7034
7072
|
}
|
7035
|
-
|
7073
|
+
else {
|
7074
|
+
pathMap = new Map();
|
7075
|
+
this.proxyCache.set(target, pathMap);
|
7076
|
+
}
|
7077
|
+
// Create the proxy with path captured in closure
|
7078
|
+
const proxy = new Proxy(target, {
|
7079
|
+
get(obj, key) {
|
7080
|
+
const value = Reflect.get(obj, key);
|
7081
|
+
// If the value is an object or array, make it reactive too
|
7082
|
+
if (typeof value === 'object' && value !== null) {
|
7083
|
+
// Build the nested path
|
7084
|
+
const keyStr = String(key);
|
7085
|
+
const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
7086
|
+
return ReactiveProxy.create(value, onChange, nestedPath);
|
7087
|
+
}
|
7088
|
+
// For arrays, intercept mutation methods
|
7089
|
+
if (Array.isArray(obj) && typeof value === 'function') {
|
7090
|
+
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
7091
|
+
if (arrayMutationMethods.includes(key)) {
|
7092
|
+
return function (...args) {
|
7093
|
+
const result = value.apply(this, args);
|
7094
|
+
onChange(path || undefined);
|
7095
|
+
return result;
|
7096
|
+
};
|
7097
|
+
}
|
7098
|
+
}
|
7099
|
+
return value;
|
7100
|
+
},
|
7101
|
+
set(obj, key, value) {
|
7102
|
+
const oldValue = Reflect.get(obj, key);
|
7103
|
+
const result = Reflect.set(obj, key, value);
|
7104
|
+
// Only trigger onChange if the value actually changed
|
7105
|
+
if (oldValue !== value) {
|
7106
|
+
const keyStr = String(key);
|
7107
|
+
const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
7108
|
+
onChange(fullPath);
|
7109
|
+
}
|
7110
|
+
return result;
|
7111
|
+
},
|
7112
|
+
deleteProperty(obj, key) {
|
7113
|
+
const result = Reflect.deleteProperty(obj, key);
|
7114
|
+
const keyStr = String(key);
|
7115
|
+
const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
|
7116
|
+
onChange(fullPath);
|
7117
|
+
return result;
|
7118
|
+
}
|
7119
|
+
});
|
7120
|
+
// Cache the proxy for this path
|
7121
|
+
pathMap.set(path, proxy);
|
7122
|
+
return proxy;
|
7036
7123
|
}
|
7037
|
-
}
|
7038
|
-
|
7039
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7040
|
-
class VConditionalDirective {
|
7041
7124
|
/**
|
7042
|
-
*
|
7125
|
+
* Checks if the given object is a reactive proxy.
|
7126
|
+
*
|
7127
|
+
* @param obj The object to check.
|
7128
|
+
* @returns True if the object is a reactive proxy, false otherwise.
|
7043
7129
|
*/
|
7044
|
-
|
7130
|
+
static isReactive(obj) {
|
7131
|
+
return this.proxyCache.has(obj);
|
7132
|
+
}
|
7045
7133
|
/**
|
7046
|
-
*
|
7047
|
-
*
|
7048
|
-
|
7049
|
-
|
7050
|
-
|
7051
|
-
* A function that evaluates the directive's condition.
|
7052
|
-
* It returns true if the condition is met, otherwise false.
|
7053
|
-
* This may be undefined if the directive does not have an expression (e.g., v-else).
|
7134
|
+
* Unwraps a reactive proxy to get the original object.
|
7135
|
+
* If the object is not a proxy, returns it as-is.
|
7136
|
+
*
|
7137
|
+
* @param obj The object to unwrap.
|
7138
|
+
* @returns The original object.
|
7054
7139
|
*/
|
7055
|
-
|
7140
|
+
static unwrap(obj) {
|
7141
|
+
// This is a simplified implementation
|
7142
|
+
// In a full implementation, we'd need to store a reverse mapping
|
7143
|
+
return obj;
|
7144
|
+
}
|
7145
|
+
}
|
7146
|
+
|
7147
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7148
|
+
/**
|
7149
|
+
* A dictionary representing bindings for a virtual node.
|
7150
|
+
* The key is the binding name, and the value is the binding value.
|
7151
|
+
* Supports hierarchical lookup through parent bindings.
|
7152
|
+
*/
|
7153
|
+
class VBindings {
|
7056
7154
|
/**
|
7057
|
-
* The
|
7155
|
+
* The parent bindings, if any.
|
7058
7156
|
*/
|
7059
|
-
#
|
7157
|
+
#parent;
|
7060
7158
|
/**
|
7061
|
-
*
|
7159
|
+
* The key is the binding name, and the value is the binding value.
|
7062
7160
|
*/
|
7063
|
-
|
7064
|
-
this.#vNode = context.vNode;
|
7065
|
-
// Parse the expression to extract identifiers and create the evaluator
|
7066
|
-
const expression = context.attribute.value;
|
7067
|
-
if (expression) {
|
7068
|
-
this.#identifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
7069
|
-
this.#evaluate = this.#createEvaluator(expression);
|
7070
|
-
}
|
7071
|
-
// Remove the directive attribute from the element
|
7072
|
-
this.#vNode.node.removeAttribute(context.attribute.name);
|
7073
|
-
// Initialize the conditional context for managing related directives
|
7074
|
-
this.#conditionalContext = this.#initializeConditionalContext();
|
7075
|
-
this.#conditionalContext.addDirective(this);
|
7076
|
-
}
|
7161
|
+
#local;
|
7077
7162
|
/**
|
7078
|
-
*
|
7163
|
+
* The change tracker, if any.
|
7079
7164
|
*/
|
7080
|
-
|
7081
|
-
return this.#vNode;
|
7082
|
-
}
|
7165
|
+
#onChange;
|
7083
7166
|
/**
|
7084
|
-
*
|
7167
|
+
* The set of changed identifiers.
|
7085
7168
|
*/
|
7086
|
-
|
7087
|
-
return true;
|
7088
|
-
}
|
7169
|
+
#changes = new Set();
|
7089
7170
|
/**
|
7090
|
-
*
|
7171
|
+
* Cache for array lengths to detect length changes when the same object reference is used.
|
7091
7172
|
*/
|
7092
|
-
|
7093
|
-
return undefined;
|
7094
|
-
}
|
7173
|
+
#lengthCache = new Map();
|
7095
7174
|
/**
|
7096
|
-
*
|
7175
|
+
* Creates a new instance of VBindings.
|
7176
|
+
* @param parent The parent bindings, if any.
|
7097
7177
|
*/
|
7098
|
-
|
7099
|
-
|
7100
|
-
|
7101
|
-
|
7102
|
-
|
7103
|
-
|
7104
|
-
|
7178
|
+
constructor(args = {}) {
|
7179
|
+
this.#parent = args.parent;
|
7180
|
+
this.#onChange = args.onChange;
|
7181
|
+
this.#local = new Proxy({}, {
|
7182
|
+
get: (obj, key) => {
|
7183
|
+
if (Reflect.has(obj, key)) {
|
7184
|
+
return Reflect.get(obj, key);
|
7185
|
+
}
|
7186
|
+
return this.#parent?.raw[key];
|
7105
7187
|
},
|
7106
|
-
|
7107
|
-
|
7188
|
+
set: (obj, key, value) => {
|
7189
|
+
let target = obj;
|
7190
|
+
if (!Reflect.has(target, key)) {
|
7191
|
+
for (let parent = this.#parent; parent; parent = parent.#parent) {
|
7192
|
+
if (Reflect.has(parent.#local, key)) {
|
7193
|
+
target = parent.#local;
|
7194
|
+
break;
|
7195
|
+
}
|
7196
|
+
}
|
7197
|
+
}
|
7198
|
+
let newValue = value;
|
7199
|
+
if (typeof value === 'object' && value !== null) {
|
7200
|
+
// Wrap objects/arrays with reactive proxy, tracking the root key
|
7201
|
+
newValue = ReactiveProxy.create(value, (changedPath) => {
|
7202
|
+
let path = '';
|
7203
|
+
for (const part of changedPath?.split('.') || []) {
|
7204
|
+
path = path ? `${path}.${part}` : part;
|
7205
|
+
this.#changes.add(path);
|
7206
|
+
}
|
7207
|
+
this.#onChange?.(changedPath);
|
7208
|
+
}, key);
|
7209
|
+
}
|
7210
|
+
const oldValue = Reflect.get(target, key);
|
7211
|
+
const result = Reflect.set(target, key, newValue);
|
7212
|
+
// Detect changes
|
7213
|
+
let hasChanged = oldValue !== newValue;
|
7214
|
+
// Special handling for arrays: check length changes even if same object reference
|
7215
|
+
if (!hasChanged && Array.isArray(newValue)) {
|
7216
|
+
const cachedLength = this.#lengthCache.get(key);
|
7217
|
+
const currentLength = newValue.length;
|
7218
|
+
if (cachedLength !== undefined && cachedLength !== currentLength) {
|
7219
|
+
hasChanged = true;
|
7220
|
+
}
|
7221
|
+
this.#lengthCache.set(key, currentLength);
|
7222
|
+
}
|
7223
|
+
if (hasChanged) {
|
7224
|
+
this.#changes.add(key);
|
7225
|
+
this.#onChange?.(key);
|
7226
|
+
}
|
7227
|
+
return result;
|
7228
|
+
},
|
7229
|
+
deleteProperty: (obj, key) => {
|
7230
|
+
const result = Reflect.deleteProperty(obj, key);
|
7231
|
+
this.#changes.add(key);
|
7232
|
+
this.#onChange?.(key);
|
7233
|
+
return result;
|
7108
7234
|
}
|
7109
|
-
};
|
7110
|
-
return updater;
|
7235
|
+
});
|
7111
7236
|
}
|
7112
7237
|
/**
|
7113
|
-
*
|
7238
|
+
* Gets the raw bindings.
|
7239
|
+
* If a key is not found locally, it searches parent bindings recursively.
|
7114
7240
|
*/
|
7115
|
-
get
|
7116
|
-
return this.#
|
7241
|
+
get raw() {
|
7242
|
+
return this.#local;
|
7117
7243
|
}
|
7118
7244
|
/**
|
7119
|
-
* Indicates whether
|
7120
|
-
* For v-if and v-else-if, this depends on the evaluation of their expressions.
|
7121
|
-
* For v-else, this is always true.
|
7245
|
+
* Indicates whether there are any changed identifiers.
|
7122
7246
|
*/
|
7123
|
-
get
|
7124
|
-
if (
|
7125
|
-
// No expression means always true (e.g., v-else)
|
7247
|
+
get hasChanges() {
|
7248
|
+
if (this.#parent?.hasChanges) {
|
7126
7249
|
return true;
|
7127
7250
|
}
|
7128
|
-
return this.#
|
7251
|
+
return this.#changes.size > 0;
|
7129
7252
|
}
|
7130
7253
|
/**
|
7131
|
-
*
|
7254
|
+
* Gets the list of changed identifiers.
|
7132
7255
|
*/
|
7133
|
-
|
7134
|
-
|
7256
|
+
get changes() {
|
7257
|
+
const changes = new Set(this.#parent?.changes || []);
|
7258
|
+
this.#changes.forEach(id => changes.add(id));
|
7259
|
+
return Array.from(changes);
|
7135
7260
|
}
|
7136
7261
|
/**
|
7137
|
-
*
|
7138
|
-
* Inserts or removes the node from the DOM as needed.
|
7262
|
+
* Indicates whether this is the root bindings (i.e., has no parent).
|
7139
7263
|
*/
|
7140
|
-
|
7141
|
-
|
7142
|
-
if (this.#conditionalContext.isPrecedingConditionMet(this)) {
|
7143
|
-
// Previous condition met, ensure node is removed
|
7144
|
-
this.#removedNode();
|
7145
|
-
return;
|
7146
|
-
}
|
7147
|
-
if (!this.#evaluate) {
|
7148
|
-
// No expression means always true (e.g., v-else)
|
7149
|
-
this.#insertNode();
|
7150
|
-
return;
|
7151
|
-
}
|
7152
|
-
// Evaluate the condition and insert or remove the node accordingly
|
7153
|
-
const shouldRender = this.#evaluate();
|
7154
|
-
if (shouldRender) {
|
7155
|
-
this.#insertNode();
|
7156
|
-
}
|
7157
|
-
else {
|
7158
|
-
this.#removedNode();
|
7159
|
-
}
|
7264
|
+
get isRoot() {
|
7265
|
+
return !this.#parent;
|
7160
7266
|
}
|
7161
7267
|
/**
|
7162
|
-
*
|
7163
|
-
* If there is no anchor node, the node is inserted as a child of its parent node.
|
7164
|
-
* If the node is already in the DOM, no action is taken.
|
7268
|
+
* Clears the set of changed identifiers.
|
7165
7269
|
*/
|
7166
|
-
|
7167
|
-
|
7168
|
-
// Already in DOM, no action needed
|
7169
|
-
return;
|
7170
|
-
}
|
7171
|
-
if (this.#vNode?.anchorNode) {
|
7172
|
-
// Insert after the anchor node
|
7173
|
-
this.#vNode.anchorNode.parentNode?.insertBefore(this.#vNode.node, this.#vNode.anchorNode.nextSibling);
|
7174
|
-
}
|
7175
|
-
else if (this.#vNode.parentVNode) {
|
7176
|
-
// Append to the parent node
|
7177
|
-
const parentElement = this.#vNode.parentVNode.node;
|
7178
|
-
parentElement.appendChild(this.#vNode.node);
|
7179
|
-
}
|
7180
|
-
else {
|
7181
|
-
// No anchor or parent VNode available
|
7182
|
-
throw new Error("Cannot insert node: No anchor or parent VNode available.");
|
7183
|
-
}
|
7270
|
+
clearChanges() {
|
7271
|
+
this.#changes.clear();
|
7184
7272
|
}
|
7185
7273
|
/**
|
7186
|
-
*
|
7187
|
-
*
|
7274
|
+
* Sets a binding value.
|
7275
|
+
* @param key The binding name.
|
7276
|
+
* @param value The binding value.
|
7188
7277
|
*/
|
7189
|
-
|
7190
|
-
|
7191
|
-
// Already removed from DOM, no action needed
|
7192
|
-
return;
|
7193
|
-
}
|
7194
|
-
this.#vNode.node.parentNode?.removeChild(this.#vNode.node);
|
7278
|
+
set(key, value) {
|
7279
|
+
this.#local[key] = value;
|
7195
7280
|
}
|
7196
7281
|
/**
|
7197
|
-
*
|
7198
|
-
* @param
|
7199
|
-
* @returns
|
7282
|
+
* Gets a binding value.
|
7283
|
+
* @param key The binding name.
|
7284
|
+
* @returns The binding value, or undefined if not found.
|
7200
7285
|
*/
|
7201
|
-
|
7202
|
-
|
7203
|
-
const args = identifiers.join(", ");
|
7204
|
-
const funcBody = `return (${expression});`;
|
7205
|
-
// Create a dynamic function with the identifiers as parameters
|
7206
|
-
const func = new Function(args, funcBody);
|
7207
|
-
// Return a function that calls the dynamic function with the current values from the virtual node's bindings
|
7208
|
-
return () => {
|
7209
|
-
// Gather the current values of the identifiers from the bindings
|
7210
|
-
const values = identifiers.map(id => this.#vNode.bindings?.[id]);
|
7211
|
-
// Call the dynamic function with the gathered values and return the result as a boolean
|
7212
|
-
return Boolean(func(...values));
|
7213
|
-
};
|
7286
|
+
get(key) {
|
7287
|
+
return this.#local[key];
|
7214
7288
|
}
|
7215
7289
|
/**
|
7216
|
-
*
|
7290
|
+
* Checks if a binding exists.
|
7291
|
+
* @param key The binding name.
|
7292
|
+
* @param recursive Whether to search parent bindings. Default is true.
|
7293
|
+
* @returns True if the binding exists, false otherwise.
|
7217
7294
|
*/
|
7218
|
-
|
7219
|
-
|
7220
|
-
|
7221
|
-
return new VConditionalDirectiveContext();
|
7295
|
+
has(key, recursive = true) {
|
7296
|
+
if (key in this.#local) {
|
7297
|
+
return true;
|
7222
7298
|
}
|
7223
|
-
|
7224
|
-
|
7225
|
-
if (!precedingDirective) {
|
7226
|
-
throw new Error("preceding v-if or v-else-if directive not found.");
|
7299
|
+
if (!recursive) {
|
7300
|
+
return false;
|
7227
7301
|
}
|
7228
|
-
|
7229
|
-
|
7230
|
-
|
7231
|
-
|
7232
|
-
|
7233
|
-
|
7234
|
-
|
7235
|
-
|
7236
|
-
* Directive for conditional rendering in the virtual DOM.
|
7237
|
-
* This directive renders an element if the preceding v-if or v-else-if directive evaluated to false.
|
7238
|
-
* For example:
|
7239
|
-
* <div v-else>This div is rendered if the previous v-if or v-else-if was false.</div>
|
7240
|
-
* The element and its children are included in the DOM only if the preceding v-if or v-else-if expression evaluates to false.
|
7241
|
-
* If the preceding expression is true, this element and its children are not rendered.
|
7242
|
-
* This directive must be used immediately after a v-if or v-else-if directive.
|
7243
|
-
*/
|
7244
|
-
class VElseDirective extends VConditionalDirective {
|
7245
|
-
/**
|
7246
|
-
* @param context The context for parsing the directive.
|
7247
|
-
*/
|
7248
|
-
constructor(context) {
|
7249
|
-
super(context);
|
7250
|
-
}
|
7251
|
-
/**
|
7252
|
-
* @inheritdoc
|
7253
|
-
*/
|
7254
|
-
get name() {
|
7255
|
-
return StandardDirectiveName.V_ELSE;
|
7256
|
-
}
|
7257
|
-
}
|
7258
|
-
|
7259
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7260
|
-
/**
|
7261
|
-
* Directive for conditional rendering in the virtual DOM.
|
7262
|
-
* This directive renders an element based on a boolean expression, but only if preceding v-if or v-else-if directives were false.
|
7263
|
-
* For example:
|
7264
|
-
* <div v-else-if="isAlternativeVisible">This div is conditionally rendered.</div>
|
7265
|
-
* The element and its children are included in the DOM only if the expression evaluates to true AND no preceding condition was met.
|
7266
|
-
* This directive must be used after a v-if or another v-else-if directive.
|
7267
|
-
*/
|
7268
|
-
class VElseIfDirective extends VConditionalDirective {
|
7269
|
-
/**
|
7270
|
-
* @param context The context for parsing the directive.
|
7271
|
-
*/
|
7272
|
-
constructor(context) {
|
7273
|
-
super(context);
|
7274
|
-
}
|
7275
|
-
/**
|
7276
|
-
* @inheritdoc
|
7277
|
-
*/
|
7278
|
-
get name() {
|
7279
|
-
return StandardDirectiveName.V_ELSE_IF;
|
7280
|
-
}
|
7281
|
-
}
|
7282
|
-
|
7283
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7284
|
-
class BindingsUtils {
|
7285
|
-
/**
|
7286
|
-
* Gets the identifiers that have changed between two sets of bindings.
|
7287
|
-
* @param oldBindings The old set of bindings.
|
7288
|
-
* @param newBindings The new set of bindings.
|
7289
|
-
* @returns An array of identifiers that have changed.
|
7290
|
-
*/
|
7291
|
-
static getChangedIdentifiers(oldBindings, newBindings) {
|
7292
|
-
const changed = [];
|
7293
|
-
for (const key of Object.keys(newBindings)) {
|
7294
|
-
if (!Object.hasOwn(oldBindings, key) || oldBindings[key] !== newBindings[key]) {
|
7295
|
-
changed.push(key);
|
7296
|
-
}
|
7297
|
-
}
|
7298
|
-
for (const key of Object.keys(oldBindings)) {
|
7299
|
-
if (!Object.hasOwn(newBindings, key)) {
|
7300
|
-
changed.push(key);
|
7301
|
-
}
|
7302
|
-
}
|
7303
|
-
return Array.from(new Set(changed));
|
7302
|
+
return this.#parent?.has(key) ?? false;
|
7303
|
+
}
|
7304
|
+
/**
|
7305
|
+
* Removes a local binding.
|
7306
|
+
* @param key The binding name.
|
7307
|
+
*/
|
7308
|
+
remove(key) {
|
7309
|
+
delete this.#local[key];
|
7304
7310
|
}
|
7305
7311
|
}
|
7306
7312
|
|
@@ -7390,9 +7396,32 @@ class VDirectiveManager {
|
|
7390
7396
|
}
|
7391
7397
|
#parseDirectives() {
|
7392
7398
|
const element = this.#vNode.node;
|
7399
|
+
// Collect relevant attributes
|
7400
|
+
const attributes = [];
|
7401
|
+
if (element.hasAttribute(StandardDirectiveName.V_FOR)) {
|
7402
|
+
attributes.push(element.getAttributeNode(StandardDirectiveName.V_FOR));
|
7403
|
+
for (const attr of Array.from(element.attributes)) {
|
7404
|
+
if (['v-bind:key', ':key'].includes(attr.name)) {
|
7405
|
+
attributes.push(attr);
|
7406
|
+
break;
|
7407
|
+
}
|
7408
|
+
}
|
7409
|
+
}
|
7410
|
+
else if (element.hasAttribute(StandardDirectiveName.V_IF)) {
|
7411
|
+
attributes.push(element.getAttributeNode(StandardDirectiveName.V_IF));
|
7412
|
+
}
|
7413
|
+
else if (element.hasAttribute(StandardDirectiveName.V_ELSE_IF)) {
|
7414
|
+
attributes.push(element.getAttributeNode(StandardDirectiveName.V_ELSE_IF));
|
7415
|
+
}
|
7416
|
+
else if (element.hasAttribute(StandardDirectiveName.V_ELSE)) {
|
7417
|
+
attributes.push(element.getAttributeNode(StandardDirectiveName.V_ELSE));
|
7418
|
+
}
|
7419
|
+
else {
|
7420
|
+
attributes.push(...Array.from(element.attributes));
|
7421
|
+
}
|
7393
7422
|
// Parse directives from attributes
|
7394
7423
|
const directives = [];
|
7395
|
-
for (const attribute of
|
7424
|
+
for (const attribute of attributes) {
|
7396
7425
|
// Create a context for parsing the directive
|
7397
7426
|
const context = {
|
7398
7427
|
vNode: this.#vNode,
|
@@ -7479,7 +7508,7 @@ class VTextEvaluator {
|
|
7479
7508
|
let result = text;
|
7480
7509
|
evaluators.forEach((evaluator, i) => {
|
7481
7510
|
// Gather the current values of the identifiers from the bindings
|
7482
|
-
const values = evaluator.ids.map(id => bindings
|
7511
|
+
const values = evaluator.ids.map(id => bindings.get(id));
|
7483
7512
|
// Evaluate the expression and replace {{...}} in the text
|
7484
7513
|
result = result.replace(matches[i][0], String(evaluator.func(...values)));
|
7485
7514
|
});
|
@@ -7511,6 +7540,11 @@ class VTextEvaluator {
|
|
7511
7540
|
}
|
7512
7541
|
|
7513
7542
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7543
|
+
/**
|
7544
|
+
* Represents a virtual node in the virtual DOM.
|
7545
|
+
* A virtual node corresponds to a real DOM node and contains additional information for data binding and directives.
|
7546
|
+
* This class is responsible for managing the state and behavior of the virtual node, including its bindings, directives, and child nodes.
|
7547
|
+
*/
|
7514
7548
|
class VNode {
|
7515
7549
|
/**
|
7516
7550
|
* The application instance associated with this virtual node.
|
@@ -7543,11 +7577,10 @@ class VNode {
|
|
7543
7577
|
*/
|
7544
7578
|
#bindings;
|
7545
7579
|
/**
|
7546
|
-
* The
|
7547
|
-
* This is
|
7548
|
-
* This is optional and may be undefined if there are no preparers.
|
7580
|
+
* The initial set of identifiers that this node depends on.
|
7581
|
+
* This is optional and may be undefined if there are no dependent identifiers.
|
7549
7582
|
*/
|
7550
|
-
#
|
7583
|
+
#initDependentIdentifiers;
|
7551
7584
|
/**
|
7552
7585
|
* An evaluator for text nodes that contain expressions in {{...}}.
|
7553
7586
|
* This is used to dynamically update the text content based on data bindings.
|
@@ -7560,15 +7593,16 @@ class VNode {
|
|
7560
7593
|
*/
|
7561
7594
|
#directiveManager;
|
7562
7595
|
/**
|
7563
|
-
* The list of
|
7564
|
-
* This is optional and may be undefined if there are no
|
7596
|
+
* The list of dependents for this virtual node.
|
7597
|
+
* This is optional and may be undefined if there are no dependents.
|
7565
7598
|
*/
|
7566
|
-
#
|
7599
|
+
#dependents;
|
7567
7600
|
/**
|
7568
7601
|
* The list of identifiers for this virtual node.
|
7569
7602
|
* This includes variable and function names used in expressions.
|
7603
|
+
* This is optional and may be undefined if there are no identifiers.
|
7570
7604
|
*/
|
7571
|
-
#
|
7605
|
+
#dependentIdentifiers;
|
7572
7606
|
/**
|
7573
7607
|
* The list of preparable identifiers for this virtual node.
|
7574
7608
|
* This includes variable and function names used in directive bindings preparers.
|
@@ -7591,7 +7625,8 @@ class VNode {
|
|
7591
7625
|
this.#nodeName = args.node.nodeName;
|
7592
7626
|
this.#parentVNode = args.parentVNode;
|
7593
7627
|
this.#bindings = args.bindings;
|
7594
|
-
this.#
|
7628
|
+
this.#initDependentIdentifiers = args.dependentIdentifiers;
|
7629
|
+
this.#parentVNode?.addChild(this);
|
7595
7630
|
// If the node is a text node, check for expressions and create a text evaluator
|
7596
7631
|
if (this.#nodeType === Node.TEXT_NODE) {
|
7597
7632
|
const text = this.#node;
|
@@ -7603,23 +7638,24 @@ class VNode {
|
|
7603
7638
|
// If the node is an element, initialize directives and child nodes
|
7604
7639
|
if (this.#nodeType === Node.ELEMENT_NODE && this.#node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
|
7605
7640
|
this.#node;
|
7606
|
-
// Initialize directive manager
|
7607
|
-
this.#directiveManager = new VDirectiveManager(this);
|
7608
7641
|
// Initialize child virtual nodes
|
7609
7642
|
this.#childVNodes = [];
|
7610
|
-
//
|
7611
|
-
|
7612
|
-
|
7613
|
-
|
7614
|
-
|
7615
|
-
|
7616
|
-
|
7617
|
-
|
7643
|
+
// Initialize directive manager
|
7644
|
+
this.#directiveManager = new VDirectiveManager(this);
|
7645
|
+
// For non-v-for elements, recursively create VNode instances for child nodes
|
7646
|
+
if (!this.#directiveManager.directives?.some(d => d.templatize)) {
|
7647
|
+
for (const childNode of Array.from(this.#node.childNodes)) {
|
7648
|
+
new VNode({
|
7649
|
+
node: childNode,
|
7650
|
+
vApplication: this.#vApplication,
|
7651
|
+
parentVNode: this
|
7652
|
+
});
|
7653
|
+
}
|
7618
7654
|
}
|
7619
7655
|
}
|
7620
7656
|
// If there is a parent virtual node, add this node as a dependency
|
7621
7657
|
if (this.#parentVNode) {
|
7622
|
-
this.#closers = this.#parentVNode.
|
7658
|
+
this.#closers = this.#parentVNode.addDependent(this);
|
7623
7659
|
}
|
7624
7660
|
}
|
7625
7661
|
/**
|
@@ -7694,7 +7730,10 @@ class VNode {
|
|
7694
7730
|
* The data bindings associated with this virtual node, if any.
|
7695
7731
|
*/
|
7696
7732
|
get bindings() {
|
7697
|
-
|
7733
|
+
if (this.#bindings) {
|
7734
|
+
return this.#bindings;
|
7735
|
+
}
|
7736
|
+
return this.#parentVNode?.bindings;
|
7698
7737
|
}
|
7699
7738
|
/**
|
7700
7739
|
* The directive manager associated with this virtual node.
|
@@ -7731,28 +7770,30 @@ class VNode {
|
|
7731
7770
|
* The list of identifiers for this virtual node.
|
7732
7771
|
* This includes variable and function names used in expressions.
|
7733
7772
|
*/
|
7734
|
-
get
|
7735
|
-
// If already computed, return the cached identifiers
|
7736
|
-
if (this.#
|
7737
|
-
return this.#
|
7773
|
+
get dependentIdentifiers() {
|
7774
|
+
// If already computed, return the cached dependent identifiers
|
7775
|
+
if (this.#dependentIdentifiers) {
|
7776
|
+
return this.#dependentIdentifiers;
|
7738
7777
|
}
|
7739
7778
|
// Collect identifiers from text evaluator and directives
|
7740
|
-
const
|
7779
|
+
const ids = [];
|
7780
|
+
// Include initial dependent identifiers, if any
|
7781
|
+
ids.push(...this.#initDependentIdentifiers ?? []);
|
7741
7782
|
// If this is a text node with a text evaluator, include its identifiers
|
7742
7783
|
if (this.#textEvaluator) {
|
7743
|
-
|
7784
|
+
ids.push(...this.#textEvaluator.identifiers);
|
7744
7785
|
}
|
7745
7786
|
// Include identifiers from directive bindings preparers
|
7746
7787
|
this.#directiveManager?.bindingsPreparers?.forEach(preparer => {
|
7747
|
-
|
7788
|
+
ids.push(...preparer.dependentIdentifiers);
|
7748
7789
|
});
|
7749
7790
|
// Include identifiers from directive DOM updaters
|
7750
7791
|
this.#directiveManager?.domUpdaters?.forEach(updater => {
|
7751
|
-
|
7792
|
+
ids.push(...updater.dependentIdentifiers);
|
7752
7793
|
});
|
7753
7794
|
// Remove duplicates by converting to a Set and back to an array
|
7754
|
-
this.#
|
7755
|
-
return this.#
|
7795
|
+
this.#dependentIdentifiers = [...new Set(ids)];
|
7796
|
+
return this.#dependentIdentifiers;
|
7756
7797
|
}
|
7757
7798
|
get preparableIdentifiers() {
|
7758
7799
|
// If already computed, return the cached preparable identifiers
|
@@ -7761,8 +7802,6 @@ class VNode {
|
|
7761
7802
|
}
|
7762
7803
|
// Collect preparable identifiers from directive bindings preparers
|
7763
7804
|
const preparableIdentifiers = [];
|
7764
|
-
// Include preparable identifiers from this node's bindings preparer, if any
|
7765
|
-
preparableIdentifiers.push(...(this.#bindingsPreparer?.preparableIdentifiers ?? []));
|
7766
7805
|
// Include preparable identifiers from directive bindings preparers
|
7767
7806
|
this.#directiveManager?.bindingsPreparers?.forEach(preparer => {
|
7768
7807
|
preparableIdentifiers.push(...preparer.preparableIdentifiers);
|
@@ -7771,134 +7810,178 @@ class VNode {
|
|
7771
7810
|
this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
|
7772
7811
|
return this.#preparableIdentifiers;
|
7773
7812
|
}
|
7813
|
+
/**
|
7814
|
+
* The DOM path of this virtual node.
|
7815
|
+
* This is a string representation of the path from the root to this node,
|
7816
|
+
* using the node names and their indices among siblings with the same name.
|
7817
|
+
* For example: "DIV[0]/SPAN[1]/#text[0]"
|
7818
|
+
* @return The DOM path as a string.
|
7819
|
+
*/
|
7820
|
+
get domPath() {
|
7821
|
+
const path = [];
|
7822
|
+
let node = this;
|
7823
|
+
while (node) {
|
7824
|
+
if (node.parentVNode && node.parentVNode.childVNodes) {
|
7825
|
+
const siblings = node.parentVNode.childVNodes.filter(v => v.nodeName === node?.nodeName);
|
7826
|
+
const index = siblings.indexOf(node);
|
7827
|
+
path.unshift(`${node.nodeName}[${index}]`);
|
7828
|
+
}
|
7829
|
+
else {
|
7830
|
+
path.unshift(node.nodeName);
|
7831
|
+
}
|
7832
|
+
node = node.parentVNode;
|
7833
|
+
}
|
7834
|
+
return path.join('/');
|
7835
|
+
}
|
7774
7836
|
/**
|
7775
7837
|
* Updates the virtual node and its children based on the current bindings.
|
7776
7838
|
* This method evaluates any expressions in text nodes and applies effectors from directives.
|
7777
7839
|
* It also recursively updates child virtual nodes.
|
7778
7840
|
* @param context The context for the update operation.
|
7841
|
+
* This includes the current bindings and a list of identifiers that have changed.
|
7779
7842
|
*/
|
7780
|
-
update(
|
7781
|
-
|
7782
|
-
const { bindings, changedIdentifiers, isInitial } = context;
|
7843
|
+
update() {
|
7844
|
+
const changes = this.bindings?.changes || [];
|
7783
7845
|
// If this is a text node with a text evaluator, update its content if needed
|
7784
7846
|
if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
|
7785
|
-
if
|
7786
|
-
|
7847
|
+
// Check if any of the identifiers are in the changed identifiers
|
7848
|
+
const changed = this.#textEvaluator.identifiers.some(id => changes.includes(id));
|
7849
|
+
// If the text node has changed, update its content
|
7850
|
+
if (changed) {
|
7787
7851
|
const text = this.#node;
|
7788
|
-
text.data = this.#textEvaluator.evaluate(bindings);
|
7852
|
+
text.data = this.#textEvaluator.evaluate(this.bindings);
|
7789
7853
|
}
|
7790
|
-
|
7791
|
-
|
7792
|
-
|
7793
|
-
|
7794
|
-
|
7854
|
+
return;
|
7855
|
+
}
|
7856
|
+
// Prepare new bindings using directive bindings preparers, if any
|
7857
|
+
if (this.#directiveManager?.bindingsPreparers) {
|
7858
|
+
// Ensure local bindings are initialized
|
7859
|
+
if (!this.#bindings) {
|
7860
|
+
this.#bindings = new VBindings({ parent: this.bindings });
|
7861
|
+
}
|
7862
|
+
// Prepare bindings for each preparer if relevant identifiers have changed
|
7863
|
+
for (const preparer of this.#directiveManager.bindingsPreparers) {
|
7864
|
+
const changed = preparer.dependentIdentifiers.some(id => changes.includes(id));
|
7795
7865
|
if (changed) {
|
7796
|
-
|
7797
|
-
text.data = this.#textEvaluator.evaluate(bindings);
|
7866
|
+
preparer.prepareBindings();
|
7798
7867
|
}
|
7799
7868
|
}
|
7800
|
-
return;
|
7801
7869
|
}
|
7802
|
-
if
|
7803
|
-
|
7804
|
-
|
7805
|
-
|
7870
|
+
// Apply DOM updaters from directives, if any
|
7871
|
+
if (this.#directiveManager?.domUpdaters) {
|
7872
|
+
for (const updater of this.#directiveManager.domUpdaters) {
|
7873
|
+
const changed = updater.dependentIdentifiers.some(id => changes.includes(id));
|
7874
|
+
if (changed) {
|
7806
7875
|
updater.applyToDOM();
|
7807
7876
|
}
|
7808
7877
|
}
|
7809
|
-
// Recursively update dependent virtual nodes
|
7810
|
-
this.#dependencies?.forEach(dependentNode => {
|
7811
|
-
// Update the dependent node
|
7812
|
-
dependentNode.update({
|
7813
|
-
bindings: this.#bindings,
|
7814
|
-
changedIdentifiers: [],
|
7815
|
-
isInitial: true
|
7816
|
-
});
|
7817
|
-
});
|
7818
7878
|
}
|
7819
|
-
|
7820
|
-
|
7821
|
-
|
7822
|
-
|
7823
|
-
|
7824
|
-
const newBindings = { ...bindings };
|
7825
|
-
const changes = new Set([...changedIdentifiers]);
|
7826
|
-
this.#bindingsPreparer?.prepareBindings(newBindings);
|
7827
|
-
if (this.#directiveManager?.bindingsPreparers) {
|
7828
|
-
for (const preparer of this.#directiveManager.bindingsPreparers) {
|
7829
|
-
const changed = preparer.identifiers.some(id => changedIdentifiers.includes(id));
|
7830
|
-
if (changed) {
|
7831
|
-
preparer.prepareBindings(newBindings);
|
7832
|
-
}
|
7833
|
-
}
|
7879
|
+
// Recursively update dependent virtual nodes
|
7880
|
+
this.#dependents?.forEach(dependentNode => {
|
7881
|
+
const changed = dependentNode.dependentIdentifiers.some(id => changes.includes(id));
|
7882
|
+
if (changed) {
|
7883
|
+
dependentNode.update();
|
7834
7884
|
}
|
7835
|
-
|
7836
|
-
|
7837
|
-
|
7838
|
-
|
7839
|
-
|
7840
|
-
|
7885
|
+
});
|
7886
|
+
}
|
7887
|
+
/**
|
7888
|
+
* Forces an update of the virtual node and its children, regardless of changed identifiers.
|
7889
|
+
* This method evaluates any expressions in text nodes and applies effectors from directives.
|
7890
|
+
* It also recursively updates child virtual nodes.
|
7891
|
+
* This is useful when an immediate update is needed, bypassing the usual change detection.
|
7892
|
+
*/
|
7893
|
+
forceUpdate() {
|
7894
|
+
// If this is a text node with a text evaluator, update its content if needed
|
7895
|
+
if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
|
7896
|
+
const text = this.#node;
|
7897
|
+
text.data = this.#textEvaluator.evaluate(this.bindings);
|
7898
|
+
return;
|
7899
|
+
}
|
7900
|
+
// Prepare new bindings using directive bindings preparers, if any
|
7901
|
+
if (this.#directiveManager?.bindingsPreparers) {
|
7902
|
+
// Ensure local bindings are initialized
|
7903
|
+
if (!this.#bindings) {
|
7904
|
+
this.#bindings = new VBindings({ parent: this.bindings });
|
7841
7905
|
}
|
7842
|
-
//
|
7843
|
-
|
7844
|
-
|
7845
|
-
|
7846
|
-
|
7847
|
-
|
7848
|
-
|
7849
|
-
|
7906
|
+
// Prepare bindings for each preparer if relevant identifiers have changed
|
7907
|
+
for (const preparer of this.#directiveManager.bindingsPreparers) {
|
7908
|
+
preparer.prepareBindings();
|
7909
|
+
}
|
7910
|
+
}
|
7911
|
+
// Apply DOM updaters from directives, if any
|
7912
|
+
if (this.#directiveManager?.domUpdaters) {
|
7913
|
+
for (const updater of this.#directiveManager.domUpdaters) {
|
7914
|
+
updater.applyToDOM();
|
7850
7915
|
}
|
7851
|
-
// Recursively update dependent virtual nodes
|
7852
|
-
this.#dependencies?.forEach(dependentNode => {
|
7853
|
-
// Check if any of the dependent node's identifiers are in the changed identifiers
|
7854
|
-
if (dependentNode.identifiers.filter(id => changes.has(id)).length === 0) {
|
7855
|
-
return;
|
7856
|
-
}
|
7857
|
-
// Update the dependent node
|
7858
|
-
dependentNode.update({
|
7859
|
-
bindings: this.#bindings,
|
7860
|
-
changedIdentifiers: Array.from(changes),
|
7861
|
-
});
|
7862
|
-
});
|
7863
7916
|
}
|
7917
|
+
// Recursively update child virtual nodes
|
7918
|
+
this.#childVNodes?.forEach(childVNode => {
|
7919
|
+
childVNode.forceUpdate();
|
7920
|
+
});
|
7864
7921
|
}
|
7865
7922
|
/**
|
7866
|
-
* Adds a
|
7867
|
-
*
|
7868
|
-
|
7923
|
+
* Adds a child virtual node to this virtual node.
|
7924
|
+
* @param child The child virtual node to add.
|
7925
|
+
*/
|
7926
|
+
addChild(child) {
|
7927
|
+
this.#childVNodes?.push(child);
|
7928
|
+
}
|
7929
|
+
/**
|
7930
|
+
* Adds a dependent virtual node that relies on this node's bindings.
|
7931
|
+
* @param dependent The dependent virtual node to add.
|
7932
|
+
* @param dependentIdentifiers The identifiers that the dependent node relies on.
|
7933
|
+
* If not provided, the dependent node's own identifiers will be used.
|
7869
7934
|
* @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
|
7870
7935
|
*/
|
7871
|
-
|
7872
|
-
// List of closers to unregister
|
7936
|
+
addDependent(dependent, dependentIdentifiers = undefined) {
|
7937
|
+
// List of closers to unregister the dependency
|
7873
7938
|
const closers = [];
|
7874
|
-
//
|
7875
|
-
|
7876
|
-
|
7877
|
-
|
7878
|
-
|
7939
|
+
// If dependent identifiers are not provided, use the dependent node's own identifiers
|
7940
|
+
if (!dependentIdentifiers) {
|
7941
|
+
dependentIdentifiers = [...dependent.dependentIdentifiers];
|
7942
|
+
}
|
7943
|
+
// Prepare alternative identifiers by stripping array indices (e.g., "items[0]" -> "items")
|
7944
|
+
const allDeps = new Set();
|
7945
|
+
dependentIdentifiers.forEach(id => {
|
7946
|
+
allDeps.add(id);
|
7947
|
+
const idx = id.indexOf('[');
|
7948
|
+
if (idx !== -1) {
|
7949
|
+
allDeps.add(id.substring(0, idx));
|
7879
7950
|
}
|
7951
|
+
});
|
7952
|
+
// Get this node's identifiers
|
7953
|
+
const thisIds = [...this.preparableIdentifiers];
|
7954
|
+
if (this.#bindings) {
|
7955
|
+
thisIds.push(...this.#bindings?.raw ? Object.keys(this.#bindings.raw) : []);
|
7880
7956
|
}
|
7881
7957
|
// If the dependent node has an identifier in this node's identifiers, add it as a dependency
|
7882
|
-
if (
|
7958
|
+
if ([...allDeps].some(id => thisIds.includes(id))) {
|
7883
7959
|
// If the dependencies list is not initialized, create it
|
7884
|
-
if (!this.#
|
7885
|
-
this.#
|
7960
|
+
if (!this.#dependents) {
|
7961
|
+
this.#dependents = [];
|
7886
7962
|
}
|
7887
7963
|
// Add the dependent node to the list
|
7888
|
-
this.#
|
7964
|
+
this.#dependents.push(dependent);
|
7965
|
+
// Remove the matched identifiers from the dependent node's identifiers to avoid duplicate dependencies
|
7966
|
+
thisIds.forEach(id => {
|
7967
|
+
const idx = dependentIdentifiers.indexOf(id);
|
7968
|
+
if (idx !== -1) {
|
7969
|
+
dependentIdentifiers.splice(idx, 1);
|
7970
|
+
}
|
7971
|
+
});
|
7889
7972
|
// Create a closer to unregister the dependency
|
7890
7973
|
closers.push({
|
7891
7974
|
close: () => {
|
7892
7975
|
// Remove the dependent node from the dependencies list
|
7893
|
-
const index = this.#
|
7976
|
+
const index = this.#dependents?.indexOf(dependent) ?? -1;
|
7894
7977
|
if (index !== -1) {
|
7895
|
-
this.#
|
7978
|
+
this.#dependents?.splice(index, 1);
|
7896
7979
|
}
|
7897
7980
|
}
|
7898
7981
|
});
|
7899
7982
|
}
|
7900
7983
|
// Recursively add the dependency to the parent node, if any
|
7901
|
-
this.#parentVNode?.
|
7984
|
+
this.#parentVNode?.addDependent(dependent, dependentIdentifiers)?.forEach(closer => closers.push(closer));
|
7902
7985
|
// Return a closer to unregister the dependency
|
7903
7986
|
return closers.length > 0 ? closers : undefined;
|
7904
7987
|
}
|
@@ -7934,6 +8017,333 @@ class VNode {
|
|
7934
8017
|
}
|
7935
8018
|
}
|
7936
8019
|
|
8020
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
8021
|
+
/**
|
8022
|
+
* Context for managing related conditional directives (v-if, v-else-if, v-else).
|
8023
|
+
*/
|
8024
|
+
class VConditionalDirectiveContext {
|
8025
|
+
/**
|
8026
|
+
* A list of directives (v-if, v-else-if, v-else) in the order they appear in the template.
|
8027
|
+
*/
|
8028
|
+
#directives = [];
|
8029
|
+
/**
|
8030
|
+
* A cached list of all variable and function names used in the expressions of the associated directives.
|
8031
|
+
*/
|
8032
|
+
#allDependentIdentifiers = [];
|
8033
|
+
/**
|
8034
|
+
* Gets a list of all variable and function names used in the expressions of the associated directives.
|
8035
|
+
* This is useful for determining dependencies for re-evaluation when data changes.
|
8036
|
+
*/
|
8037
|
+
get allDependentIdentifiers() {
|
8038
|
+
return this.#allDependentIdentifiers;
|
8039
|
+
}
|
8040
|
+
/**
|
8041
|
+
* Adds a directive (v-else-if or v-else) to the conditional context.
|
8042
|
+
* @param directive The directive to add.
|
8043
|
+
*/
|
8044
|
+
addDirective(directive) {
|
8045
|
+
this.#directives.push(directive);
|
8046
|
+
// Update the cached list of all dependent identifiers
|
8047
|
+
if (directive.dependentIdentifiers) {
|
8048
|
+
for (const id of directive.dependentIdentifiers) {
|
8049
|
+
if (!this.#allDependentIdentifiers.includes(id)) {
|
8050
|
+
this.#allDependentIdentifiers.push(id);
|
8051
|
+
}
|
8052
|
+
}
|
8053
|
+
}
|
8054
|
+
}
|
8055
|
+
/**
|
8056
|
+
* Checks if any preceding directive's condition is met.
|
8057
|
+
* This is used to determine if a v-else-if or v-else directive should be rendered.
|
8058
|
+
* @param directive The directive to check against.
|
8059
|
+
* @returns True if any preceding directive's condition is met, otherwise false.
|
8060
|
+
*/
|
8061
|
+
isPrecedingConditionMet(directive) {
|
8062
|
+
const index = this.#directives.indexOf(directive);
|
8063
|
+
if (index === -1) {
|
8064
|
+
throw new Error("Directive not found in context.");
|
8065
|
+
}
|
8066
|
+
// Check if all previous directives are met
|
8067
|
+
for (let i = 0; i < index; i++) {
|
8068
|
+
const d = this.#directives[i];
|
8069
|
+
if (d.conditionIsMet === true) {
|
8070
|
+
return true;
|
8071
|
+
}
|
8072
|
+
}
|
8073
|
+
return false;
|
8074
|
+
}
|
8075
|
+
}
|
8076
|
+
|
8077
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
8078
|
+
class VConditionalDirective {
|
8079
|
+
/**
|
8080
|
+
* The virtual node to which this directive is applied.
|
8081
|
+
*/
|
8082
|
+
#vNode;
|
8083
|
+
/**
|
8084
|
+
* A list of variable and function names used in the directive's expression.
|
8085
|
+
* This may be undefined if the directive does not have an expression (e.g., v-else).
|
8086
|
+
*/
|
8087
|
+
#dependentIdentifiers;
|
8088
|
+
/*
|
8089
|
+
* A function that evaluates the directive's condition.
|
8090
|
+
* It returns true if the condition is met, otherwise false.
|
8091
|
+
* This may be undefined if the directive does not have an expression (e.g., v-else).
|
8092
|
+
*/
|
8093
|
+
#evaluate;
|
8094
|
+
/**
|
8095
|
+
* The context for managing related conditional directives (v-if, v-else-if, v-else).
|
8096
|
+
*/
|
8097
|
+
#conditionalContext;
|
8098
|
+
/**
|
8099
|
+
* The currently rendered virtual node, if any.
|
8100
|
+
*/
|
8101
|
+
#renderedVNode;
|
8102
|
+
/**
|
8103
|
+
* @param context The context for parsing the directive.
|
8104
|
+
*/
|
8105
|
+
constructor(context) {
|
8106
|
+
this.#vNode = context.vNode;
|
8107
|
+
// Parse the expression to extract identifiers and create the evaluator
|
8108
|
+
const expression = context.attribute.value;
|
8109
|
+
if (expression) {
|
8110
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
8111
|
+
this.#evaluate = this.#createEvaluator(expression);
|
8112
|
+
}
|
8113
|
+
// Remove the directive attribute from the element
|
8114
|
+
this.#vNode.node.removeAttribute(context.attribute.name);
|
8115
|
+
// Initialize the conditional context for managing related directives
|
8116
|
+
this.#conditionalContext = this.#initializeConditionalContext();
|
8117
|
+
this.#conditionalContext.addDirective(this);
|
8118
|
+
}
|
8119
|
+
/**
|
8120
|
+
* @inheritdoc
|
8121
|
+
*/
|
8122
|
+
get vNode() {
|
8123
|
+
return this.#vNode;
|
8124
|
+
}
|
8125
|
+
/**
|
8126
|
+
* @inheritdoc
|
8127
|
+
*/
|
8128
|
+
get needsAnchor() {
|
8129
|
+
return true;
|
8130
|
+
}
|
8131
|
+
/**
|
8132
|
+
* @inheritdoc
|
8133
|
+
*/
|
8134
|
+
get bindingsPreparer() {
|
8135
|
+
return undefined;
|
8136
|
+
}
|
8137
|
+
/**
|
8138
|
+
* @inheritdoc
|
8139
|
+
*/
|
8140
|
+
get domUpdater() {
|
8141
|
+
const identifiers = this.#conditionalContext.allDependentIdentifiers;
|
8142
|
+
const render = () => this.#render();
|
8143
|
+
// Create an updater that handles the conditional rendering
|
8144
|
+
const updater = {
|
8145
|
+
get dependentIdentifiers() {
|
8146
|
+
return identifiers;
|
8147
|
+
},
|
8148
|
+
applyToDOM() {
|
8149
|
+
render();
|
8150
|
+
}
|
8151
|
+
};
|
8152
|
+
return updater;
|
8153
|
+
}
|
8154
|
+
/**
|
8155
|
+
* @inheritdoc
|
8156
|
+
*/
|
8157
|
+
get templatize() {
|
8158
|
+
return true;
|
8159
|
+
}
|
8160
|
+
/**
|
8161
|
+
* @inheritdoc
|
8162
|
+
*/
|
8163
|
+
get dependentIdentifiers() {
|
8164
|
+
return this.#dependentIdentifiers ?? [];
|
8165
|
+
}
|
8166
|
+
/**
|
8167
|
+
* The context for managing related conditional directives (v-if, v-else-if, v-else).
|
8168
|
+
*/
|
8169
|
+
get conditionalContext() {
|
8170
|
+
return this.#conditionalContext;
|
8171
|
+
}
|
8172
|
+
/**
|
8173
|
+
* Indicates whether the condition for this directive is currently met.
|
8174
|
+
* For v-if and v-else-if, this depends on the evaluation of their expressions.
|
8175
|
+
* For v-else, this is always true.
|
8176
|
+
*/
|
8177
|
+
get conditionIsMet() {
|
8178
|
+
if (!this.#evaluate) {
|
8179
|
+
// No expression means always true (e.g., v-else)
|
8180
|
+
return true;
|
8181
|
+
}
|
8182
|
+
return this.#evaluate();
|
8183
|
+
}
|
8184
|
+
/**
|
8185
|
+
* @inheritdoc
|
8186
|
+
*/
|
8187
|
+
destroy() {
|
8188
|
+
// Default implementation does nothing. Override in subclasses if needed.
|
8189
|
+
}
|
8190
|
+
/**
|
8191
|
+
* Renders the node based on the evaluation of the directive's condition.
|
8192
|
+
* Inserts or removes the node from the DOM as needed.
|
8193
|
+
*/
|
8194
|
+
#render() {
|
8195
|
+
// Check if any preceding directive's condition is met
|
8196
|
+
if (this.#conditionalContext.isPrecedingConditionMet(this)) {
|
8197
|
+
// Previous condition met, ensure node is removed
|
8198
|
+
this.#removedNode();
|
8199
|
+
return;
|
8200
|
+
}
|
8201
|
+
if (!this.#evaluate) {
|
8202
|
+
// No expression means always true (e.g., v-else)
|
8203
|
+
this.#insertNode();
|
8204
|
+
return;
|
8205
|
+
}
|
8206
|
+
// Evaluate the condition and insert or remove the node accordingly
|
8207
|
+
const shouldRender = this.#evaluate();
|
8208
|
+
if (shouldRender) {
|
8209
|
+
this.#insertNode();
|
8210
|
+
}
|
8211
|
+
else {
|
8212
|
+
this.#removedNode();
|
8213
|
+
}
|
8214
|
+
}
|
8215
|
+
/**
|
8216
|
+
* Inserts the node into the DOM at the position marked by the anchor node, if any.
|
8217
|
+
* If there is no anchor node, the node is inserted as a child of its parent node.
|
8218
|
+
* If the node is already in the DOM, no action is taken.
|
8219
|
+
*/
|
8220
|
+
#insertNode() {
|
8221
|
+
if (this.#renderedVNode) {
|
8222
|
+
// Already rendered, no action needed
|
8223
|
+
return;
|
8224
|
+
}
|
8225
|
+
this.#renderedVNode = this.#cloneTemplate();
|
8226
|
+
this.#vNode.anchorNode?.parentNode?.insertBefore(this.#renderedVNode.node, this.#vNode.anchorNode.nextSibling);
|
8227
|
+
this.#renderedVNode.forceUpdate();
|
8228
|
+
}
|
8229
|
+
/**
|
8230
|
+
* Removes the node from the DOM.
|
8231
|
+
* If the node is not in the DOM, no action is taken.
|
8232
|
+
*/
|
8233
|
+
#removedNode() {
|
8234
|
+
if (!this.#renderedVNode) {
|
8235
|
+
// Not rendered, no action needed
|
8236
|
+
return;
|
8237
|
+
}
|
8238
|
+
this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
|
8239
|
+
this.#renderedVNode.destroy();
|
8240
|
+
this.#renderedVNode = undefined;
|
8241
|
+
}
|
8242
|
+
/**
|
8243
|
+
* Clones the template element and creates a new VNode for the cloned element.
|
8244
|
+
*/
|
8245
|
+
#cloneTemplate() {
|
8246
|
+
const element = this.#vNode.node;
|
8247
|
+
const clone = element.cloneNode(true);
|
8248
|
+
// Create a new VNode for the cloned element
|
8249
|
+
const vNode = new VNode({
|
8250
|
+
node: clone,
|
8251
|
+
vApplication: this.#vNode.vApplication,
|
8252
|
+
parentVNode: this.#vNode.parentVNode
|
8253
|
+
});
|
8254
|
+
return vNode;
|
8255
|
+
}
|
8256
|
+
/**
|
8257
|
+
* Creates a function to evaluate the directive's condition.
|
8258
|
+
* @param expression The expression string to evaluate.
|
8259
|
+
* @returns A function that evaluates the directive's condition.
|
8260
|
+
*/
|
8261
|
+
#createEvaluator(expression) {
|
8262
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8263
|
+
const args = identifiers.join(", ");
|
8264
|
+
const funcBody = `return (${expression});`;
|
8265
|
+
// Create a dynamic function with the identifiers as parameters
|
8266
|
+
const func = new Function(args, funcBody);
|
8267
|
+
// Return a function that calls the dynamic function with the current values from the virtual node's bindings
|
8268
|
+
return () => {
|
8269
|
+
// Gather the current values of the identifiers from the bindings
|
8270
|
+
const values = identifiers.map(id => this.#vNode.bindings?.get(id));
|
8271
|
+
// Call the dynamic function with the gathered values and return the result as a boolean
|
8272
|
+
return Boolean(func(...values));
|
8273
|
+
};
|
8274
|
+
}
|
8275
|
+
/**
|
8276
|
+
* Initializes the conditional context for managing related directives.
|
8277
|
+
*/
|
8278
|
+
#initializeConditionalContext() {
|
8279
|
+
// Create a new context if this is a v-if directive
|
8280
|
+
if (this.name === StandardDirectiveName.V_IF) {
|
8281
|
+
return new VConditionalDirectiveContext();
|
8282
|
+
}
|
8283
|
+
// Link to the existing conditional context from the preceding v-if or v-else-if directive
|
8284
|
+
let prevVNode = this.vNode.previousSibling;
|
8285
|
+
while (prevVNode && prevVNode.nodeType !== Node.ELEMENT_NODE) {
|
8286
|
+
prevVNode = prevVNode.previousSibling;
|
8287
|
+
}
|
8288
|
+
const precedingDirective = prevVNode?.directiveManager?.directives?.find(d => d.name === StandardDirectiveName.V_IF || d.name === StandardDirectiveName.V_ELSE_IF);
|
8289
|
+
if (!precedingDirective) {
|
8290
|
+
throw new Error("preceding v-if or v-else-if directive not found.");
|
8291
|
+
}
|
8292
|
+
// Cast to VConditionalDirective to access conditionalContext
|
8293
|
+
const conditionalContext = precedingDirective.conditionalContext;
|
8294
|
+
return conditionalContext;
|
8295
|
+
}
|
8296
|
+
}
|
8297
|
+
|
8298
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
8299
|
+
/**
|
8300
|
+
* Directive for conditional rendering in the virtual DOM.
|
8301
|
+
* This directive renders an element if the preceding v-if or v-else-if directive evaluated to false.
|
8302
|
+
* For example:
|
8303
|
+
* <div v-else>This div is rendered if the previous v-if or v-else-if was false.</div>
|
8304
|
+
* The element and its children are included in the DOM only if the preceding v-if or v-else-if expression evaluates to false.
|
8305
|
+
* If the preceding expression is true, this element and its children are not rendered.
|
8306
|
+
* This directive must be used immediately after a v-if or v-else-if directive.
|
8307
|
+
*/
|
8308
|
+
class VElseDirective extends VConditionalDirective {
|
8309
|
+
/**
|
8310
|
+
* @param context The context for parsing the directive.
|
8311
|
+
*/
|
8312
|
+
constructor(context) {
|
8313
|
+
super(context);
|
8314
|
+
}
|
8315
|
+
/**
|
8316
|
+
* @inheritdoc
|
8317
|
+
*/
|
8318
|
+
get name() {
|
8319
|
+
return StandardDirectiveName.V_ELSE;
|
8320
|
+
}
|
8321
|
+
}
|
8322
|
+
|
8323
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
8324
|
+
/**
|
8325
|
+
* Directive for conditional rendering in the virtual DOM.
|
8326
|
+
* This directive renders an element based on a boolean expression, but only if preceding v-if or v-else-if directives were false.
|
8327
|
+
* For example:
|
8328
|
+
* <div v-else-if="isAlternativeVisible">This div is conditionally rendered.</div>
|
8329
|
+
* The element and its children are included in the DOM only if the expression evaluates to true AND no preceding condition was met.
|
8330
|
+
* This directive must be used after a v-if or another v-else-if directive.
|
8331
|
+
*/
|
8332
|
+
class VElseIfDirective extends VConditionalDirective {
|
8333
|
+
/**
|
8334
|
+
* @param context The context for parsing the directive.
|
8335
|
+
*/
|
8336
|
+
constructor(context) {
|
8337
|
+
super(context);
|
8338
|
+
}
|
8339
|
+
/**
|
8340
|
+
* @inheritdoc
|
8341
|
+
*/
|
8342
|
+
get name() {
|
8343
|
+
return StandardDirectiveName.V_ELSE_IF;
|
8344
|
+
}
|
8345
|
+
}
|
8346
|
+
|
7937
8347
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
7938
8348
|
/**
|
7939
8349
|
* Directive for rendering a list of items using a loop.
|
@@ -7958,7 +8368,7 @@ class VForDirective {
|
|
7958
8368
|
/**
|
7959
8369
|
* A list of variable and function names used in the directive's expression.
|
7960
8370
|
*/
|
7961
|
-
#
|
8371
|
+
#dependentIdentifiers;
|
7962
8372
|
/**
|
7963
8373
|
* A function that evaluates the directive's expression to get the source data.
|
7964
8374
|
* It returns the collection to iterate over.
|
@@ -7996,7 +8406,7 @@ class VForDirective {
|
|
7996
8406
|
this.#indexName = parsed.indexName;
|
7997
8407
|
this.#sourceName = parsed.sourceName;
|
7998
8408
|
// Extract identifiers from the source expression
|
7999
|
-
this.#
|
8409
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(parsed.sourceName, context.vNode.vApplication.functionDependencies);
|
8000
8410
|
this.#evaluateSource = this.#createSourceEvaluator(parsed.sourceName);
|
8001
8411
|
}
|
8002
8412
|
// Remove the directive attribute from the element
|
@@ -8030,11 +8440,11 @@ class VForDirective {
|
|
8030
8440
|
* @inheritdoc
|
8031
8441
|
*/
|
8032
8442
|
get domUpdater() {
|
8033
|
-
const identifiers = this.#
|
8443
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8034
8444
|
const render = () => this.#render();
|
8035
8445
|
// Create and return the DOM updater
|
8036
8446
|
const updater = {
|
8037
|
-
get
|
8447
|
+
get dependentIdentifiers() {
|
8038
8448
|
return identifiers;
|
8039
8449
|
},
|
8040
8450
|
applyToDOM() {
|
@@ -8043,6 +8453,18 @@ class VForDirective {
|
|
8043
8453
|
};
|
8044
8454
|
return updater;
|
8045
8455
|
}
|
8456
|
+
/**
|
8457
|
+
* @inheritdoc
|
8458
|
+
*/
|
8459
|
+
get templatize() {
|
8460
|
+
return true;
|
8461
|
+
}
|
8462
|
+
/**
|
8463
|
+
* @inheritdoc
|
8464
|
+
*/
|
8465
|
+
get dependentIdentifiers() {
|
8466
|
+
return this.#dependentIdentifiers ?? [];
|
8467
|
+
}
|
8046
8468
|
/**
|
8047
8469
|
* @inheritdoc
|
8048
8470
|
*/
|
@@ -8079,12 +8501,9 @@ class VForDirective {
|
|
8079
8501
|
if (this.#evaluateKey && this.#itemName) {
|
8080
8502
|
iterations = iterations.map(iter => {
|
8081
8503
|
// Create bindings for this iteration
|
8082
|
-
const itemBindings = new
|
8083
|
-
|
8084
|
-
|
8085
|
-
itemBindings.set(key, this.#vNode.bindings[key]);
|
8086
|
-
}
|
8087
|
-
}
|
8504
|
+
const itemBindings = new VBindings({
|
8505
|
+
parent: this.#vNode.bindings
|
8506
|
+
});
|
8088
8507
|
itemBindings.set(this.#itemName, iter.item);
|
8089
8508
|
if (this.#indexName) {
|
8090
8509
|
itemBindings.set(this.#indexName, iter.index);
|
@@ -8136,11 +8555,7 @@ class VForDirective {
|
|
8136
8555
|
else {
|
8137
8556
|
parent.appendChild(vNode.node);
|
8138
8557
|
}
|
8139
|
-
vNode.
|
8140
|
-
bindings: this.#vNode.bindings || {},
|
8141
|
-
changedIdentifiers: [],
|
8142
|
-
isInitial: true
|
8143
|
-
});
|
8558
|
+
vNode.forceUpdate();
|
8144
8559
|
}
|
8145
8560
|
else {
|
8146
8561
|
// Reuse existing item
|
@@ -8191,12 +8606,12 @@ class VForDirective {
|
|
8191
8606
|
* Creates a function to evaluate the source data expression.
|
8192
8607
|
*/
|
8193
8608
|
#createSourceEvaluator(expression) {
|
8194
|
-
const identifiers = this.#
|
8609
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8195
8610
|
const args = identifiers.join(", ");
|
8196
8611
|
const funcBody = `return (${expression});`;
|
8197
8612
|
const func = new Function(args, funcBody);
|
8198
8613
|
return () => {
|
8199
|
-
const values = identifiers.map(id => this.#vNode.bindings?.
|
8614
|
+
const values = identifiers.map(id => this.#vNode.bindings?.get(id));
|
8200
8615
|
return func(...values);
|
8201
8616
|
};
|
8202
8617
|
}
|
@@ -8221,80 +8636,39 @@ class VForDirective {
|
|
8221
8636
|
const element = this.#vNode.node;
|
8222
8637
|
const clone = element.cloneNode(true);
|
8223
8638
|
// Prepare identifiers for the item
|
8224
|
-
|
8225
|
-
|
8639
|
+
this.#itemName;
|
8640
|
+
this.#indexName;
|
8226
8641
|
// Create bindings for this iteration
|
8227
|
-
const bindings = {
|
8642
|
+
const bindings = new VBindings({
|
8643
|
+
parent: this.#vNode.bindings
|
8644
|
+
});
|
8228
8645
|
if (this.#itemName) {
|
8229
|
-
bindings
|
8646
|
+
bindings.set(this.#itemName, context.item);
|
8230
8647
|
}
|
8231
8648
|
if (this.#indexName) {
|
8232
|
-
bindings
|
8649
|
+
bindings.set(this.#indexName, context.index);
|
8233
8650
|
}
|
8234
|
-
const itemBindingsPreparer = {
|
8235
|
-
get identifiers() {
|
8236
|
-
return []; // No specific identifiers for item
|
8237
|
-
},
|
8238
|
-
get preparableIdentifiers() {
|
8239
|
-
// Return item and index names if defined
|
8240
|
-
const ids = [];
|
8241
|
-
if (itemName)
|
8242
|
-
ids.push(itemName);
|
8243
|
-
if (indexName)
|
8244
|
-
ids.push(indexName);
|
8245
|
-
return ids;
|
8246
|
-
},
|
8247
|
-
prepareBindings(bindings) {
|
8248
|
-
// Prepare bindings for the current item
|
8249
|
-
if (itemName) {
|
8250
|
-
bindings[itemName] = context.item;
|
8251
|
-
}
|
8252
|
-
if (indexName) {
|
8253
|
-
bindings[indexName] = context.index;
|
8254
|
-
}
|
8255
|
-
}
|
8256
|
-
};
|
8257
8651
|
// Create a new VNode for the cloned element
|
8258
8652
|
const vNode = new VNode({
|
8259
8653
|
node: clone,
|
8260
8654
|
vApplication: this.#vNode.vApplication,
|
8261
8655
|
parentVNode: this.#vNode.parentVNode,
|
8262
8656
|
bindings,
|
8263
|
-
|
8657
|
+
dependentIdentifiers: [`${this.#sourceName}[${context.index}]`]
|
8264
8658
|
});
|
8265
|
-
// Set data attributes for debugging
|
8266
|
-
clone.setAttribute('data-v-for-key', String(context.key));
|
8267
|
-
clone.setAttribute('data-v-for-index', String(context.index));
|
8268
8659
|
return vNode;
|
8269
8660
|
}
|
8270
8661
|
/**
|
8271
8662
|
* Update bindings for an existing item
|
8272
8663
|
*/
|
8273
8664
|
#updateItemBindings(vNode, context) {
|
8274
|
-
const bindings = vNode.bindings || {};
|
8275
|
-
const updatedBindings = { ...bindings };
|
8276
|
-
if (this.#itemName) {
|
8277
|
-
updatedBindings[this.#itemName] = context.item;
|
8278
|
-
}
|
8279
|
-
if (this.#indexName) {
|
8280
|
-
updatedBindings[this.#indexName] = context.index;
|
8281
|
-
}
|
8282
|
-
// Update data attributes
|
8283
|
-
const element = vNode.node;
|
8284
|
-
element.setAttribute('data-v-for-key', String(context.key));
|
8285
|
-
element.setAttribute('data-v-for-index', String(context.index));
|
8286
|
-
// Trigger reactivity update by calling update with the new bindings
|
8287
|
-
const changedIdentifiers = [];
|
8288
8665
|
if (this.#itemName) {
|
8289
|
-
|
8666
|
+
vNode.bindings?.set(this.#itemName, context.item);
|
8290
8667
|
}
|
8291
8668
|
if (this.#indexName) {
|
8292
|
-
|
8669
|
+
vNode.bindings?.set(this.#indexName, context.index);
|
8293
8670
|
}
|
8294
|
-
vNode.update(
|
8295
|
-
bindings: updatedBindings,
|
8296
|
-
changedIdentifiers
|
8297
|
-
});
|
8671
|
+
vNode.update();
|
8298
8672
|
}
|
8299
8673
|
/**
|
8300
8674
|
* Get iterations from various data types
|
@@ -8382,7 +8756,7 @@ class VModelDirective {
|
|
8382
8756
|
/**
|
8383
8757
|
* A list of variable and function names used in the directive's expression.
|
8384
8758
|
*/
|
8385
|
-
#
|
8759
|
+
#dependentIdentifiers;
|
8386
8760
|
/**
|
8387
8761
|
* A function that evaluates the directive's expression.
|
8388
8762
|
* It returns the evaluated value of the expression.
|
@@ -8416,7 +8790,7 @@ class VModelDirective {
|
|
8416
8790
|
const expression = context.attribute.value;
|
8417
8791
|
if (expression) {
|
8418
8792
|
this.#expression = expression;
|
8419
|
-
this.#
|
8793
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
8420
8794
|
this.#evaluate = this.#createEvaluator(expression);
|
8421
8795
|
// Attach event listener for two-way binding
|
8422
8796
|
this.#attachEventListener();
|
@@ -8452,11 +8826,11 @@ class VModelDirective {
|
|
8452
8826
|
* @inheritdoc
|
8453
8827
|
*/
|
8454
8828
|
get domUpdater() {
|
8455
|
-
const identifiers = this.#
|
8829
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8456
8830
|
const render = () => this.#render();
|
8457
8831
|
// Create and return the DOM updater
|
8458
8832
|
const updater = {
|
8459
|
-
get
|
8833
|
+
get dependentIdentifiers() {
|
8460
8834
|
return identifiers;
|
8461
8835
|
},
|
8462
8836
|
applyToDOM() {
|
@@ -8465,6 +8839,18 @@ class VModelDirective {
|
|
8465
8839
|
};
|
8466
8840
|
return updater;
|
8467
8841
|
}
|
8842
|
+
/**
|
8843
|
+
* @inheritdoc
|
8844
|
+
*/
|
8845
|
+
get templatize() {
|
8846
|
+
return false;
|
8847
|
+
}
|
8848
|
+
/**
|
8849
|
+
* @inheritdoc
|
8850
|
+
*/
|
8851
|
+
get dependentIdentifiers() {
|
8852
|
+
return this.#dependentIdentifiers ?? [];
|
8853
|
+
}
|
8468
8854
|
/**
|
8469
8855
|
* @inheritdoc
|
8470
8856
|
*/
|
@@ -8535,8 +8921,6 @@ class VModelDirective {
|
|
8535
8921
|
newValue = this.#applyModifiers(newValue);
|
8536
8922
|
// Update the binding
|
8537
8923
|
this.#updateBinding(newValue);
|
8538
|
-
// Schedule a DOM update
|
8539
|
-
this.#vNode.vApplication.scheduleUpdate();
|
8540
8924
|
};
|
8541
8925
|
element.addEventListener(eventName, this.#listener);
|
8542
8926
|
}
|
@@ -8593,21 +8977,12 @@ class VModelDirective {
|
|
8593
8977
|
if (!this.#expression) {
|
8594
8978
|
return;
|
8595
8979
|
}
|
8596
|
-
const
|
8597
|
-
|
8598
|
-
|
8599
|
-
}
|
8600
|
-
|
8601
|
-
|
8602
|
-
const trimmed = this.#expression.trim();
|
8603
|
-
if (trimmed && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed)) {
|
8604
|
-
bindings[trimmed] = newValue;
|
8605
|
-
}
|
8606
|
-
else {
|
8607
|
-
// For complex expressions like "user.name", we'd need more sophisticated parsing
|
8608
|
-
this.#vNode.vApplication.logManager.getLogger('VModelDirective')
|
8609
|
-
.warn(`v-model only supports simple identifiers for now: ${this.#expression}`);
|
8610
|
-
}
|
8980
|
+
const expression = this.#expression.trim();
|
8981
|
+
const values = [newValue];
|
8982
|
+
const args = ['$newValue'].join(", ");
|
8983
|
+
const funcBody = `(this.${expression} = $newValue);`;
|
8984
|
+
const func = new Function(args, funcBody);
|
8985
|
+
func.call(this.#vNode.bindings?.raw, ...values);
|
8611
8986
|
}
|
8612
8987
|
/**
|
8613
8988
|
* Creates a function to evaluate the directive's condition.
|
@@ -8615,15 +8990,15 @@ class VModelDirective {
|
|
8615
8990
|
* @returns A function that evaluates the directive's condition.
|
8616
8991
|
*/
|
8617
8992
|
#createEvaluator(expression) {
|
8618
|
-
const identifiers = this.#
|
8993
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8619
8994
|
const args = identifiers.join(", ");
|
8620
8995
|
const funcBody = `return (${expression});`;
|
8621
8996
|
// Create a dynamic function with the identifiers as parameters
|
8622
8997
|
const func = new Function(args, funcBody);
|
8623
|
-
// Return a function that calls the dynamic function with the current values from
|
8998
|
+
// Return a function that calls the dynamic function with the current values from bindings
|
8624
8999
|
return () => {
|
8625
9000
|
// Gather the current values of the identifiers from the bindings
|
8626
|
-
const values = identifiers.map(id => this.#vNode.bindings?.
|
9001
|
+
const values = identifiers.map(id => this.#vNode.bindings?.get(id));
|
8627
9002
|
// Call the dynamic function with the gathered values
|
8628
9003
|
return func(...values);
|
8629
9004
|
};
|
@@ -8652,7 +9027,7 @@ class VOnDirective {
|
|
8652
9027
|
/**
|
8653
9028
|
* A list of variable and function names used in the directive's expression.
|
8654
9029
|
*/
|
8655
|
-
#
|
9030
|
+
#dependentIdentifiers;
|
8656
9031
|
/**
|
8657
9032
|
* The event handler wrapper function, generated once and reused.
|
8658
9033
|
*/
|
@@ -8691,7 +9066,7 @@ class VOnDirective {
|
|
8691
9066
|
// Parse the expression to extract identifiers and create the handler wrapper
|
8692
9067
|
const expression = context.attribute.value;
|
8693
9068
|
if (expression) {
|
8694
|
-
this.#
|
9069
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
8695
9070
|
this.#handlerWrapper = this.#createHandlerWrapper(expression);
|
8696
9071
|
}
|
8697
9072
|
// Create and attach the event listener
|
@@ -8731,6 +9106,18 @@ class VOnDirective {
|
|
8731
9106
|
get domUpdater() {
|
8732
9107
|
return undefined;
|
8733
9108
|
}
|
9109
|
+
/**
|
9110
|
+
* @inheritdoc
|
9111
|
+
*/
|
9112
|
+
get templatize() {
|
9113
|
+
return false;
|
9114
|
+
}
|
9115
|
+
/**
|
9116
|
+
* @inheritdoc
|
9117
|
+
*/
|
9118
|
+
get dependentIdentifiers() {
|
9119
|
+
return this.#dependentIdentifiers ?? [];
|
9120
|
+
}
|
8734
9121
|
/**
|
8735
9122
|
* @inheritdoc
|
8736
9123
|
*/
|
@@ -8755,6 +9142,35 @@ class VOnDirective {
|
|
8755
9142
|
const isOnce = this.#modifiers.has('once');
|
8756
9143
|
// Create the event listener function
|
8757
9144
|
this.#listener = (event) => {
|
9145
|
+
// Check key modifiers for keyboard events
|
9146
|
+
if (event instanceof KeyboardEvent) {
|
9147
|
+
const keyModifiers = ['enter', 'tab', 'delete', 'esc', 'space', 'up', 'down', 'left', 'right'];
|
9148
|
+
const hasKeyModifier = keyModifiers.some(key => this.#modifiers.has(key));
|
9149
|
+
if (hasKeyModifier) {
|
9150
|
+
const keyMap = {
|
9151
|
+
'enter': 'Enter',
|
9152
|
+
'tab': 'Tab',
|
9153
|
+
'delete': 'Delete',
|
9154
|
+
'esc': 'Escape',
|
9155
|
+
'space': ' ',
|
9156
|
+
'up': 'ArrowUp',
|
9157
|
+
'down': 'ArrowDown',
|
9158
|
+
'left': 'ArrowLeft',
|
9159
|
+
'right': 'ArrowRight'
|
9160
|
+
};
|
9161
|
+
let keyMatched = false;
|
9162
|
+
for (const [modifier, keyValue] of Object.entries(keyMap)) {
|
9163
|
+
if (this.#modifiers.has(modifier) && event.key === keyValue) {
|
9164
|
+
keyMatched = true;
|
9165
|
+
break;
|
9166
|
+
}
|
9167
|
+
}
|
9168
|
+
// If key modifier specified but key doesn't match, return early
|
9169
|
+
if (!keyMatched) {
|
9170
|
+
return;
|
9171
|
+
}
|
9172
|
+
}
|
9173
|
+
}
|
8758
9174
|
// Apply event modifiers
|
8759
9175
|
if (this.#modifiers.has('stop')) {
|
8760
9176
|
event.stopPropagation();
|
@@ -8783,27 +9199,26 @@ class VOnDirective {
|
|
8783
9199
|
* @returns A function that handles the event.
|
8784
9200
|
*/
|
8785
9201
|
#createHandlerWrapper(expression) {
|
8786
|
-
const identifiers = this.#
|
9202
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8787
9203
|
const vNode = this.#vNode;
|
8788
9204
|
// Return a function that handles the event with proper scope
|
8789
9205
|
return (event) => {
|
8790
|
-
|
8791
|
-
const bindings = vNode.bindings ?? {};
|
9206
|
+
const bindings = vNode.bindings;
|
8792
9207
|
// If the expression is just a method name, call it with bindings as 'this'
|
8793
9208
|
const trimmedExpr = expression.trim();
|
8794
|
-
if (identifiers.includes(trimmedExpr) && typeof bindings
|
9209
|
+
if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
|
8795
9210
|
const methodName = trimmedExpr;
|
8796
|
-
const originalMethod = bindings
|
9211
|
+
const originalMethod = bindings?.get(methodName);
|
8797
9212
|
// Call the method with bindings as 'this' context
|
8798
9213
|
// This allows the method to access and modify bindings properties via 'this'
|
8799
|
-
return originalMethod
|
9214
|
+
return originalMethod(event);
|
8800
9215
|
}
|
8801
9216
|
// For inline expressions, evaluate normally
|
8802
|
-
const values = identifiers.map(id => bindings
|
9217
|
+
const values = identifiers.map(id => vNode.bindings?.get(id));
|
8803
9218
|
const args = identifiers.join(", ");
|
8804
9219
|
const funcBody = `return (${expression});`;
|
8805
9220
|
const func = new Function(args, funcBody);
|
8806
|
-
return func(...values);
|
9221
|
+
return func.call(bindings?.raw, ...values, event);
|
8807
9222
|
};
|
8808
9223
|
}
|
8809
9224
|
}
|
@@ -8827,7 +9242,7 @@ class VShowDirective {
|
|
8827
9242
|
/**
|
8828
9243
|
* A list of variable and function names used in the directive's expression.
|
8829
9244
|
*/
|
8830
|
-
#
|
9245
|
+
#dependentIdentifiers;
|
8831
9246
|
/*
|
8832
9247
|
* A function that evaluates the directive's condition.
|
8833
9248
|
* It returns true if the condition is met, otherwise false.
|
@@ -8844,7 +9259,7 @@ class VShowDirective {
|
|
8844
9259
|
this.#vNode = context.vNode;
|
8845
9260
|
// Parse the expression to extract identifiers and create the evaluator
|
8846
9261
|
const expression = context.attribute.value;
|
8847
|
-
this.#
|
9262
|
+
this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
|
8848
9263
|
this.#evaluate = this.#createEvaluator(expression);
|
8849
9264
|
// Remove the directive attribute from the element
|
8850
9265
|
this.#vNode.node.removeAttribute(context.attribute.name);
|
@@ -8880,13 +9295,13 @@ class VShowDirective {
|
|
8880
9295
|
* @inheritdoc
|
8881
9296
|
*/
|
8882
9297
|
get domUpdater() {
|
8883
|
-
const identifiers = this.#
|
9298
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8884
9299
|
const evaluate = this.#evaluate;
|
8885
9300
|
const visibleNode = () => this.visibleNode();
|
8886
9301
|
const invisibleNode = () => this.invisibleNode();
|
8887
9302
|
// Create an updater that handles the conditional rendering
|
8888
9303
|
const updater = {
|
8889
|
-
get
|
9304
|
+
get dependentIdentifiers() {
|
8890
9305
|
return identifiers;
|
8891
9306
|
},
|
8892
9307
|
applyToDOM() {
|
@@ -8901,6 +9316,18 @@ class VShowDirective {
|
|
8901
9316
|
};
|
8902
9317
|
return updater;
|
8903
9318
|
}
|
9319
|
+
/**
|
9320
|
+
* @inheritdoc
|
9321
|
+
*/
|
9322
|
+
get templatize() {
|
9323
|
+
return false;
|
9324
|
+
}
|
9325
|
+
/**
|
9326
|
+
* @inheritdoc
|
9327
|
+
*/
|
9328
|
+
get dependentIdentifiers() {
|
9329
|
+
return this.#dependentIdentifiers ?? [];
|
9330
|
+
}
|
8904
9331
|
/**
|
8905
9332
|
* Makes the node visible by resetting its display style.
|
8906
9333
|
* If the node is already visible, no action is taken.
|
@@ -8942,7 +9369,7 @@ class VShowDirective {
|
|
8942
9369
|
* @returns A function that evaluates the directive's condition.
|
8943
9370
|
*/
|
8944
9371
|
#createEvaluator(expression) {
|
8945
|
-
const identifiers = this.#
|
9372
|
+
const identifiers = this.#dependentIdentifiers ?? [];
|
8946
9373
|
const args = identifiers.join(", ");
|
8947
9374
|
const funcBody = `return (${expression});`;
|
8948
9375
|
// Create a dynamic function with the identifiers as parameters
|
@@ -8950,7 +9377,7 @@ class VShowDirective {
|
|
8950
9377
|
// Return a function that calls the dynamic function with the current values from the virtual node's bindings
|
8951
9378
|
return () => {
|
8952
9379
|
// Gather the current values of the identifiers from the bindings
|
8953
|
-
const values = identifiers.map(id => this.#vNode.bindings?.
|
9380
|
+
const values = identifiers.map(id => this.#vNode.bindings?.get(id));
|
8954
9381
|
// Call the dynamic function with the gathered values and return the result as a boolean
|
8955
9382
|
return Boolean(func(...values));
|
8956
9383
|
};
|
@@ -9098,96 +9525,6 @@ class VLogManager {
|
|
9098
9525
|
}
|
9099
9526
|
}
|
9100
9527
|
|
9101
|
-
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
9102
|
-
/**
|
9103
|
-
* Utility class for creating reactive proxies that automatically track changes.
|
9104
|
-
*/
|
9105
|
-
class ReactiveProxy {
|
9106
|
-
/**
|
9107
|
-
* A WeakMap to store the original target for each proxy.
|
9108
|
-
* This allows us to avoid creating multiple proxies for the same object.
|
9109
|
-
*/
|
9110
|
-
static proxyMap = new WeakMap();
|
9111
|
-
/**
|
9112
|
-
* Creates a reactive proxy for the given object.
|
9113
|
-
* The proxy will call the onChange callback whenever a property is modified.
|
9114
|
-
*
|
9115
|
-
* @param target The object to make reactive.
|
9116
|
-
* @param onChange Callback function to call when the object changes. Receives the changed key name.
|
9117
|
-
* @returns A reactive proxy of the target object.
|
9118
|
-
*/
|
9119
|
-
static create(target, onChange) {
|
9120
|
-
// If the target is not an object or is null, return it as-is
|
9121
|
-
if (typeof target !== 'object' || target === null) {
|
9122
|
-
return target;
|
9123
|
-
}
|
9124
|
-
// If this object already has a proxy, return the existing proxy
|
9125
|
-
if (this.proxyMap.has(target)) {
|
9126
|
-
return this.proxyMap.get(target);
|
9127
|
-
}
|
9128
|
-
// Create the proxy
|
9129
|
-
const proxy = new Proxy(target, {
|
9130
|
-
get(obj, key) {
|
9131
|
-
const value = Reflect.get(obj, key);
|
9132
|
-
// If the value is an object or array, make it reactive too
|
9133
|
-
if (typeof value === 'object' && value !== null) {
|
9134
|
-
return ReactiveProxy.create(value, onChange);
|
9135
|
-
}
|
9136
|
-
// For arrays, intercept mutation methods
|
9137
|
-
if (Array.isArray(obj) && typeof value === 'function') {
|
9138
|
-
const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
9139
|
-
if (arrayMutationMethods.includes(key)) {
|
9140
|
-
return function (...args) {
|
9141
|
-
const result = value.apply(this, args);
|
9142
|
-
onChange();
|
9143
|
-
return result;
|
9144
|
-
};
|
9145
|
-
}
|
9146
|
-
}
|
9147
|
-
return value;
|
9148
|
-
},
|
9149
|
-
set(obj, key, value) {
|
9150
|
-
const oldValue = Reflect.get(obj, key);
|
9151
|
-
const result = Reflect.set(obj, key, value);
|
9152
|
-
// Only trigger onChange if the value actually changed
|
9153
|
-
if (oldValue !== value) {
|
9154
|
-
onChange(key);
|
9155
|
-
}
|
9156
|
-
return result;
|
9157
|
-
},
|
9158
|
-
deleteProperty(obj, key) {
|
9159
|
-
const result = Reflect.deleteProperty(obj, key);
|
9160
|
-
onChange(key);
|
9161
|
-
return result;
|
9162
|
-
}
|
9163
|
-
});
|
9164
|
-
// Store the proxy so we can return it if requested again
|
9165
|
-
this.proxyMap.set(target, proxy);
|
9166
|
-
return proxy;
|
9167
|
-
}
|
9168
|
-
/**
|
9169
|
-
* Checks if the given object is a reactive proxy.
|
9170
|
-
*
|
9171
|
-
* @param obj The object to check.
|
9172
|
-
* @returns True if the object is a reactive proxy, false otherwise.
|
9173
|
-
*/
|
9174
|
-
static isReactive(obj) {
|
9175
|
-
return this.proxyMap.has(obj);
|
9176
|
-
}
|
9177
|
-
/**
|
9178
|
-
* Unwraps a reactive proxy to get the original object.
|
9179
|
-
* If the object is not a proxy, returns it as-is.
|
9180
|
-
*
|
9181
|
-
* @param obj The object to unwrap.
|
9182
|
-
* @returns The original object.
|
9183
|
-
*/
|
9184
|
-
static unwrap(obj) {
|
9185
|
-
// This is a simplified implementation
|
9186
|
-
// In a full implementation, we'd need to store a reverse mapping
|
9187
|
-
return obj;
|
9188
|
-
}
|
9189
|
-
}
|
9190
|
-
|
9191
9528
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
9192
9529
|
/**
|
9193
9530
|
* Represents a virtual application instance.
|
@@ -9229,18 +9566,10 @@ class VApplication {
|
|
9229
9566
|
* A dictionary mapping computed property names to their dependencies.
|
9230
9567
|
*/
|
9231
9568
|
#computedDependencies;
|
9232
|
-
/**
|
9233
|
-
* Gets the list of identifiers that can trigger updates.
|
9234
|
-
*/
|
9235
|
-
#preparableIdentifiers;
|
9236
9569
|
/**
|
9237
9570
|
* Flag to indicate if an update is already scheduled.
|
9238
9571
|
*/
|
9239
9572
|
#updateScheduled = false;
|
9240
|
-
/**
|
9241
|
-
* Set of keys that have changed since the last update.
|
9242
|
-
*/
|
9243
|
-
#changedKeys = new Set();
|
9244
9573
|
/**
|
9245
9574
|
* Creates an instance of the virtual application.
|
9246
9575
|
* @param options The application options.
|
@@ -9259,65 +9588,7 @@ class VApplication {
|
|
9259
9588
|
// Analyze computed dependencies
|
9260
9589
|
this.#computedDependencies = ExpressionUtils.analyzeFunctionDependencies(options.computed || {});
|
9261
9590
|
// Initialize bindings from data, computed, and methods
|
9262
|
-
this.#
|
9263
|
-
// Prepare the list of identifiers that can trigger updates
|
9264
|
-
this.#preparableIdentifiers = [...Object.keys(this.#bindings)];
|
9265
|
-
}
|
9266
|
-
/**
|
9267
|
-
* Initializes bindings from data, computed properties, and methods.
|
9268
|
-
* @returns The initialized bindings object.
|
9269
|
-
*/
|
9270
|
-
#initializeBindings() {
|
9271
|
-
const bindings = {};
|
9272
|
-
// 1. Add data properties with reactive proxy for each property
|
9273
|
-
if (this.#options.data) {
|
9274
|
-
const data = this.#options.data();
|
9275
|
-
if (data && typeof data === 'object') {
|
9276
|
-
for (const [key, value] of Object.entries(data)) {
|
9277
|
-
if (typeof value === 'object' && value !== null) {
|
9278
|
-
// Wrap objects/arrays with reactive proxy, tracking the root key
|
9279
|
-
bindings[key] = ReactiveProxy.create(value, () => {
|
9280
|
-
this.#changedKeys.add(key);
|
9281
|
-
this.scheduleUpdate();
|
9282
|
-
});
|
9283
|
-
}
|
9284
|
-
else {
|
9285
|
-
// Primitive values are added as-is
|
9286
|
-
bindings[key] = value;
|
9287
|
-
}
|
9288
|
-
}
|
9289
|
-
}
|
9290
|
-
}
|
9291
|
-
// 2. Add computed properties
|
9292
|
-
if (this.#options.computed) {
|
9293
|
-
for (const [key, computedFn] of Object.entries(this.#options.computed)) {
|
9294
|
-
try {
|
9295
|
-
// Evaluate computed property with bindings as 'this' context
|
9296
|
-
bindings[key] = computedFn.call(bindings);
|
9297
|
-
}
|
9298
|
-
catch (error) {
|
9299
|
-
this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
|
9300
|
-
bindings[key] = undefined;
|
9301
|
-
}
|
9302
|
-
}
|
9303
|
-
}
|
9304
|
-
// 3. Add methods
|
9305
|
-
if (this.#options.methods) {
|
9306
|
-
Object.assign(bindings, this.#options.methods);
|
9307
|
-
}
|
9308
|
-
// 4. Wrap the entire bindings object with a proxy for primitive value changes
|
9309
|
-
return new Proxy(bindings, {
|
9310
|
-
set: (obj, key, value) => {
|
9311
|
-
const oldValue = Reflect.get(obj, key);
|
9312
|
-
const result = Reflect.set(obj, key, value);
|
9313
|
-
// Track changes to primitive values
|
9314
|
-
if (oldValue !== value) {
|
9315
|
-
this.#changedKeys.add(key);
|
9316
|
-
this.scheduleUpdate();
|
9317
|
-
}
|
9318
|
-
return result;
|
9319
|
-
}
|
9320
|
-
});
|
9591
|
+
this.#initializeBindings();
|
9321
9592
|
}
|
9322
9593
|
/**
|
9323
9594
|
* Gets the global directive parser registry.
|
@@ -9355,12 +9626,6 @@ class VApplication {
|
|
9355
9626
|
get functionDependencies() {
|
9356
9627
|
return this.#functionDependencies;
|
9357
9628
|
}
|
9358
|
-
/**
|
9359
|
-
* Gets the list of identifiers that can trigger updates.
|
9360
|
-
*/
|
9361
|
-
get preparableIdentifiers() {
|
9362
|
-
return this.#preparableIdentifiers;
|
9363
|
-
}
|
9364
9629
|
/**
|
9365
9630
|
* Mounts the application.
|
9366
9631
|
* @param selectors The CSS selectors to identify the root element.
|
@@ -9370,8 +9635,8 @@ class VApplication {
|
|
9370
9635
|
if (!element) {
|
9371
9636
|
throw new Error(`Element not found for selectors: ${selectors}`);
|
9372
9637
|
}
|
9373
|
-
//
|
9374
|
-
this.#
|
9638
|
+
// Clean the element by removing unnecessary whitespace text nodes
|
9639
|
+
this.#cleanElement(element);
|
9375
9640
|
// Create the root virtual node
|
9376
9641
|
this.#vNode = new VNode({
|
9377
9642
|
node: element,
|
@@ -9379,70 +9644,180 @@ class VApplication {
|
|
9379
9644
|
bindings: this.#bindings
|
9380
9645
|
});
|
9381
9646
|
// Initial rendering
|
9382
|
-
this.#vNode.update(
|
9383
|
-
bindings: this.#bindings,
|
9384
|
-
changedIdentifiers: [],
|
9385
|
-
isInitial: true
|
9386
|
-
});
|
9647
|
+
this.#vNode.update();
|
9387
9648
|
this.#logger.info('Application mounted.');
|
9388
9649
|
}
|
9650
|
+
/**
|
9651
|
+
* Cleans the element by removing unnecessary whitespace text nodes.
|
9652
|
+
* @param element The element to clean.
|
9653
|
+
*/
|
9654
|
+
#cleanElement(element) {
|
9655
|
+
let buffer = null;
|
9656
|
+
for (const node of Array.from(element.childNodes)) {
|
9657
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
9658
|
+
const text = node;
|
9659
|
+
if (/^[\s\n\r\t]*$/.test(text.nodeValue || '')) {
|
9660
|
+
element.removeChild(text);
|
9661
|
+
}
|
9662
|
+
else {
|
9663
|
+
if (buffer) {
|
9664
|
+
buffer.nodeValue += text.nodeValue || '';
|
9665
|
+
element.removeChild(text);
|
9666
|
+
}
|
9667
|
+
else {
|
9668
|
+
buffer = text;
|
9669
|
+
}
|
9670
|
+
}
|
9671
|
+
}
|
9672
|
+
else {
|
9673
|
+
buffer = null;
|
9674
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
9675
|
+
this.#cleanElement(node);
|
9676
|
+
}
|
9677
|
+
}
|
9678
|
+
}
|
9679
|
+
}
|
9680
|
+
/**
|
9681
|
+
* Initializes bindings from data, computed properties, and methods.
|
9682
|
+
* @returns The initialized bindings object.
|
9683
|
+
*/
|
9684
|
+
#initializeBindings() {
|
9685
|
+
// Create bindings with change tracking
|
9686
|
+
this.#bindings = new VBindings({
|
9687
|
+
onChange: (identifier) => {
|
9688
|
+
this.#scheduleUpdate();
|
9689
|
+
}
|
9690
|
+
});
|
9691
|
+
// Inject utility methods into bindings
|
9692
|
+
this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
|
9693
|
+
// Add methods
|
9694
|
+
if (this.#options.methods) {
|
9695
|
+
for (const [key, method] of Object.entries(this.#options.methods)) {
|
9696
|
+
if (typeof method !== 'function') {
|
9697
|
+
this.#logger.warn(`Method '${key}' is not a function and will be ignored.`);
|
9698
|
+
continue;
|
9699
|
+
}
|
9700
|
+
// Bind the method to the raw bindings object to ensure 'this' refers to bindings
|
9701
|
+
// This allows methods to access and modify bindings properties via 'this'
|
9702
|
+
this.#bindings.set(key, method.bind(this.#bindings.raw));
|
9703
|
+
}
|
9704
|
+
}
|
9705
|
+
// Add data properties
|
9706
|
+
if (this.#options.data) {
|
9707
|
+
const data = this.#options.data();
|
9708
|
+
if (data && typeof data === 'object') {
|
9709
|
+
for (const [key, value] of Object.entries(data)) {
|
9710
|
+
this.#bindings.set(key, value);
|
9711
|
+
}
|
9712
|
+
}
|
9713
|
+
}
|
9714
|
+
// Add computed properties
|
9715
|
+
this.#recomputeProperties();
|
9716
|
+
}
|
9389
9717
|
/**
|
9390
9718
|
* Schedules a DOM update in the next microtask.
|
9391
9719
|
* Multiple calls within the same event loop will be batched into a single update.
|
9392
9720
|
*/
|
9393
|
-
scheduleUpdate() {
|
9721
|
+
#scheduleUpdate() {
|
9394
9722
|
if (this.#updateScheduled) {
|
9395
9723
|
return;
|
9396
9724
|
}
|
9397
9725
|
this.#updateScheduled = true;
|
9398
9726
|
queueMicrotask(() => {
|
9727
|
+
this.#update();
|
9399
9728
|
this.#updateScheduled = false;
|
9400
|
-
this.update();
|
9401
9729
|
});
|
9402
9730
|
}
|
9403
9731
|
/**
|
9404
9732
|
* Executes an immediate DOM update.
|
9405
9733
|
*/
|
9406
|
-
update() {
|
9407
|
-
|
9734
|
+
#update() {
|
9735
|
+
// Re-evaluate computed properties that depend on changed values
|
9736
|
+
this.#recomputeProperties();
|
9737
|
+
// Update the DOM
|
9738
|
+
this.#vNode?.update();
|
9739
|
+
// Clear the set of changed identifiers after the update
|
9740
|
+
this.#bindings?.clearChanges();
|
9741
|
+
}
|
9742
|
+
/**
|
9743
|
+
* Recursively recomputes computed properties based on changed identifiers.
|
9744
|
+
*/
|
9745
|
+
#recomputeProperties() {
|
9746
|
+
if (!this.#options.computed) {
|
9408
9747
|
return;
|
9409
9748
|
}
|
9410
|
-
|
9411
|
-
const
|
9412
|
-
|
9413
|
-
|
9414
|
-
|
9415
|
-
|
9416
|
-
|
9417
|
-
|
9418
|
-
|
9419
|
-
|
9420
|
-
|
9421
|
-
|
9422
|
-
|
9423
|
-
|
9424
|
-
|
9425
|
-
|
9749
|
+
const computed = new Set();
|
9750
|
+
const processing = new Set();
|
9751
|
+
// Gather all changed identifiers, including parent properties for array items
|
9752
|
+
const allChanges = new Set();
|
9753
|
+
this.#bindings?.changes.forEach(id => {
|
9754
|
+
allChanges.add(id);
|
9755
|
+
const idx = id.indexOf('[');
|
9756
|
+
if (idx !== -1) {
|
9757
|
+
allChanges.add(id.substring(0, idx));
|
9758
|
+
}
|
9759
|
+
});
|
9760
|
+
// Helper function to recursively compute a property
|
9761
|
+
const compute = (key) => {
|
9762
|
+
// Skip if already computed in this update cycle
|
9763
|
+
if (computed.has(key)) {
|
9764
|
+
return;
|
9765
|
+
}
|
9766
|
+
// Detect circular dependency
|
9767
|
+
if (processing.has(key)) {
|
9768
|
+
this.#logger.error(`Circular dependency detected for computed property '${key}'`);
|
9769
|
+
return;
|
9770
|
+
}
|
9771
|
+
processing.add(key);
|
9772
|
+
// Get the dependencies for this computed property
|
9773
|
+
const deps = this.#computedDependencies[key] || [];
|
9774
|
+
// If none of the dependencies have changed, skip recomputation
|
9775
|
+
if (!deps.some(dep => allChanges.has(dep))) {
|
9776
|
+
computed.add(key);
|
9777
|
+
return;
|
9778
|
+
}
|
9779
|
+
// First, recursively compute any dependent computed properties
|
9780
|
+
for (const dep of deps) {
|
9781
|
+
if (this.#options.computed[dep]) {
|
9782
|
+
compute(dep);
|
9426
9783
|
}
|
9427
|
-
|
9428
|
-
|
9784
|
+
}
|
9785
|
+
// Now compute this property
|
9786
|
+
const computedFn = this.#options.computed[key];
|
9787
|
+
try {
|
9788
|
+
const oldValue = this.#bindings?.get(key);
|
9789
|
+
const newValue = computedFn.call(this.#bindings?.raw);
|
9790
|
+
// Track if the computed value actually changed
|
9791
|
+
if (oldValue !== newValue) {
|
9792
|
+
this.#bindings?.set(key, newValue);
|
9793
|
+
this.#bindings?.changes.forEach(id => {
|
9794
|
+
allChanges.add(id);
|
9795
|
+
const idx = id.indexOf('[');
|
9796
|
+
if (idx !== -1) {
|
9797
|
+
allChanges.add(id.substring(0, idx));
|
9798
|
+
}
|
9799
|
+
});
|
9429
9800
|
}
|
9430
9801
|
}
|
9802
|
+
catch (error) {
|
9803
|
+
this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
|
9804
|
+
}
|
9805
|
+
computed.add(key);
|
9806
|
+
processing.delete(key);
|
9807
|
+
};
|
9808
|
+
// Find all computed properties that need to be recomputed
|
9809
|
+
for (const [key, deps] of Object.entries(this.#computedDependencies)) {
|
9810
|
+
// Check if any dependency has changed
|
9811
|
+
if (deps.some(dep => allChanges.has(dep))) {
|
9812
|
+
compute(key);
|
9813
|
+
}
|
9431
9814
|
}
|
9432
|
-
// Combine all changes
|
9433
|
-
const allChanges = [...dataChanges, ...computedChanges];
|
9434
|
-
// Update the DOM
|
9435
|
-
this.#vNode.update({
|
9436
|
-
bindings: this.#bindings,
|
9437
|
-
changedIdentifiers: allChanges,
|
9438
|
-
isInitial: false
|
9439
|
-
});
|
9440
9815
|
}
|
9441
9816
|
/**
|
9442
9817
|
* Executes a callback after the next DOM update.
|
9443
9818
|
* @param callback The callback to execute.
|
9444
9819
|
*/
|
9445
|
-
nextTick(callback) {
|
9820
|
+
#nextTick(callback) {
|
9446
9821
|
if (this.#updateScheduled) {
|
9447
9822
|
queueMicrotask(() => {
|
9448
9823
|
queueMicrotask(callback);
|