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