@cocreate/element-prototype 1.29.3 → 1.31.0
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/getAttribute.js +21 -2
- package/src/getValue.js +54 -0
- package/src/operators.js +360 -53
- package/src/setValue.js +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.31.0](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.30.0...v1.31.0) (2025-11-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enhance date handling in getValue and add custom operators for number formatting and UID generation ([280a469](https://github.com/CoCreate-app/CoCreate-element-prototype/commit/280a469d7c8b007c4cb2ed35ac7f4645327ac25e))
|
|
7
|
+
|
|
8
|
+
# [1.30.0](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.29.3...v1.30.0) (2025-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add async support for getAttribute and enhance processOperators with async handling ([88cfe33](https://github.com/CoCreate-app/CoCreate-element-prototype/commit/88cfe33deff47ecd3c80b1c4c8b2de04b38530b1))
|
|
14
|
+
|
|
1
15
|
## [1.29.3](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.29.2...v1.29.3) (2025-07-13)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/element-prototype",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "A simple element-prototype component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"element-prototype",
|
package/src/getAttribute.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { processOperators } from "./operators";
|
|
1
|
+
import { processOperators, processOperatorsAsync } from "./operators";
|
|
2
2
|
|
|
3
3
|
// Store a reference to the original getAttribute function
|
|
4
4
|
const originalGetAttribute = Element.prototype.getAttribute;
|
|
@@ -41,10 +41,29 @@ function getAttribute(element, name) {
|
|
|
41
41
|
return processOperators(element, value);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Asynchronously gets an attribute and processes its value for operators.
|
|
46
|
+
* @param {Element} element - The element from which to get the attribute.
|
|
47
|
+
* @param {string} name - The attribute name.
|
|
48
|
+
* @returns {Promise<string|object>} - A promise that resolves to the processed attribute value.
|
|
49
|
+
*/
|
|
50
|
+
async function getAttributeAsync(element, name) {
|
|
51
|
+
if (!(element instanceof Element)) {
|
|
52
|
+
throw new Error("First argument must be an Element");
|
|
53
|
+
}
|
|
54
|
+
let value = originalGetAttribute.call(element, name);
|
|
55
|
+
return await processOperatorsAsync(element, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// Override the getAttribute method on Element prototype
|
|
45
59
|
Element.prototype.getAttribute = function (name) {
|
|
46
60
|
return getAttribute(this, name); // Use the custom getAttribute function
|
|
47
61
|
};
|
|
48
62
|
|
|
63
|
+
// Add getAttributeAsync to the Element prototype
|
|
64
|
+
Element.prototype.getAttributeAsync = function (name) {
|
|
65
|
+
return getAttributeAsync(this, name);
|
|
66
|
+
};
|
|
67
|
+
|
|
49
68
|
// Export the custom getAttribute function
|
|
50
|
-
export { getAttribute };
|
|
69
|
+
export { getAttribute, getAttributeAsync };
|
package/src/getValue.js
CHANGED
|
@@ -203,6 +203,8 @@ const handleDateTime = (element, value, valueType) => {
|
|
|
203
203
|
} else if (value) {
|
|
204
204
|
// Initialize a new Date from the string or object
|
|
205
205
|
value = new Date(value);
|
|
206
|
+
} else {
|
|
207
|
+
value = new Date();
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
// Check if value is a valid date
|
|
@@ -255,6 +257,58 @@ const handleDateTime = (element, value, valueType) => {
|
|
|
255
257
|
let locale = element.getAttribute("locale") || "en-US";
|
|
256
258
|
value = value.toLocaleString(locale);
|
|
257
259
|
break;
|
|
260
|
+
case "addDays":
|
|
261
|
+
// Add days to the current date
|
|
262
|
+
const addDays = parseInt(element.getAttribute("add-days") || 0, 10);
|
|
263
|
+
value.setDate(value.getDate() + addDays);
|
|
264
|
+
value = value.toISOString();
|
|
265
|
+
break;
|
|
266
|
+
case "subtractDays":
|
|
267
|
+
// Subtract days from the current date
|
|
268
|
+
const subtractDays = parseInt(element.getAttribute("subtract-days") || 0, 10);
|
|
269
|
+
value.setDate(value.getDate() - subtractDays);
|
|
270
|
+
value = value.toISOString();
|
|
271
|
+
break;
|
|
272
|
+
case "startOfDay":
|
|
273
|
+
// Get the start of the current day (12:00 midnight)
|
|
274
|
+
const startOfDay = new Date(value);
|
|
275
|
+
startOfDay.setHours(0, 0, 0, 0); // Set time to midnight
|
|
276
|
+
value = startOfDay.toISOString();
|
|
277
|
+
break;
|
|
278
|
+
case "startOfWeek":
|
|
279
|
+
// Get the start of the current week (Sunday by default)
|
|
280
|
+
const startOfWeekOffset = parseInt(element.getAttribute("week-start-day") || 0, 10); // Default to Sunday (0)
|
|
281
|
+
const startOfWeek = new Date(value);
|
|
282
|
+
startOfWeek.setDate(value.getDate() - value.getDay() + startOfWeekOffset);
|
|
283
|
+
startOfWeek.setHours(0, 0, 0, 0); // Set to midnight
|
|
284
|
+
value = startOfWeek.toISOString();
|
|
285
|
+
break;
|
|
286
|
+
case "endOfWeek":
|
|
287
|
+
// Get the end of the current week (Saturday by default)
|
|
288
|
+
const endOfWeekOffset = parseInt(element.getAttribute("week-start-day") || 0, 10); // Default to Sunday (0)
|
|
289
|
+
const endOfWeek = new Date(value);
|
|
290
|
+
endOfWeek.setDate(value.getDate() - value.getDay() + 6 + endOfWeekOffset);
|
|
291
|
+
endOfWeek.setHours(23, 59, 59, 999); // Set to the end of the day
|
|
292
|
+
value = endOfWeek.toISOString();
|
|
293
|
+
break;
|
|
294
|
+
case "startOfMonth":
|
|
295
|
+
// Get the start of the month
|
|
296
|
+
value = new Date(value.getFullYear(), value.getMonth(), 1).toISOString();
|
|
297
|
+
break;
|
|
298
|
+
case "endOfMonth":
|
|
299
|
+
// Get the end of the month
|
|
300
|
+
value = new Date(value.getFullYear(), value.getMonth() + 1, 0).toISOString();
|
|
301
|
+
break;
|
|
302
|
+
case "startOfYear":
|
|
303
|
+
// Get the start of the year
|
|
304
|
+
value = new Date(value.getFullYear(), 0, 1).toISOString();
|
|
305
|
+
break;
|
|
306
|
+
case "endOfYear":
|
|
307
|
+
// Get the end of the year
|
|
308
|
+
value = new Date(value.getFullYear(), 11, 31).toISOString();
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
|
|
258
312
|
default:
|
|
259
313
|
if (typeof value[valueType] === "function") {
|
|
260
314
|
value = value[valueType]();
|
package/src/operators.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObjectId, queryElements } from "@cocreate/utils";
|
|
1
|
+
import { ObjectId, queryElements, getValueFromObject, uid } from "@cocreate/utils";
|
|
2
2
|
|
|
3
3
|
// Operators handled directly for simple, synchronous value retrieval
|
|
4
4
|
const customOperators = new Map(
|
|
@@ -36,6 +36,12 @@ const customOperators = new Map(
|
|
|
36
36
|
return path === "/" ? "" : path;
|
|
37
37
|
},
|
|
38
38
|
$param: (element, args) => args,
|
|
39
|
+
$getObjectValue: (element, args) => {
|
|
40
|
+
if (Array.isArray(args) && args.length >= 2) {
|
|
41
|
+
return getValueFromObject(args[0], args[1]);
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
},
|
|
39
45
|
$setValue: (element, args) => element.setValue(...args) || "",
|
|
40
46
|
$true: () => true,
|
|
41
47
|
$false: () => false,
|
|
@@ -48,7 +54,28 @@ const customOperators = new Map(
|
|
|
48
54
|
} catch (e) {
|
|
49
55
|
return value;
|
|
50
56
|
}
|
|
51
|
-
}
|
|
57
|
+
},
|
|
58
|
+
// TODO: Implement number formatting
|
|
59
|
+
$numberFormat: (element, args) => {
|
|
60
|
+
let number = parseFloat(args[0]);
|
|
61
|
+
// Simple, fixed arg mapping:
|
|
62
|
+
// args[0] = locale (internationalization)
|
|
63
|
+
// args[1] = options (object)
|
|
64
|
+
// args[2] = number (if provided). If not provided, fall back to legacy behavior where args[0] might be the number.
|
|
65
|
+
if (!Array.isArray(args)) args = [args];
|
|
66
|
+
|
|
67
|
+
const locale = args[0] || undefined;
|
|
68
|
+
const options = args[1] || {};
|
|
69
|
+
const numCandidate = args[2] !== undefined ? args[2] : args[0];
|
|
70
|
+
|
|
71
|
+
number = parseFloat(numCandidate);
|
|
72
|
+
|
|
73
|
+
if (isNaN(number)) return String(numCandidate ?? "");
|
|
74
|
+
|
|
75
|
+
return new Intl.NumberFormat(locale, options).format(number);
|
|
76
|
+
},
|
|
77
|
+
$uid: (element, args) => uid(args[0]) || "",
|
|
78
|
+
|
|
52
79
|
})
|
|
53
80
|
);
|
|
54
81
|
|
|
@@ -73,6 +100,189 @@ const knownOperatorKeys = [
|
|
|
73
100
|
...propertyOperators
|
|
74
101
|
].sort((a, b) => b.length - a.length);
|
|
75
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Helper function to check if a string path starts with a known bare operator.
|
|
105
|
+
* This logic is necessary to separate '$value' from '[0].src' when no parentheses remain.
|
|
106
|
+
*/
|
|
107
|
+
const findBareOperatorInPath = (path, knownOperatorKeys) => {
|
|
108
|
+
const trimmedPath = path.trim();
|
|
109
|
+
for (const key of knownOperatorKeys) {
|
|
110
|
+
if (trimmedPath.startsWith(key)) {
|
|
111
|
+
const remaining = trimmedPath.substring(key.length);
|
|
112
|
+
// Edge Case Fix: Ensure the operator is followed by a space, bracket, dot, or end-of-string.
|
|
113
|
+
// This prevents "valueThing" from being incorrectly split into "$value" + "Thing".
|
|
114
|
+
if (remaining.length === 0 || /^\s|\[|\./.test(remaining)) {
|
|
115
|
+
return key;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Finds the innermost function call (operator + its balanced parentheses argument)
|
|
124
|
+
* in the expression. This is the core logic for iterative parsing.
|
|
125
|
+
* * @param {string} expression The full expression string.
|
|
126
|
+
* @returns {{operator: string, args: string, fullMatch: string} | null} Details of the innermost function call, or null.
|
|
127
|
+
*/
|
|
128
|
+
const findInnermostFunctionCall = (expression) => {
|
|
129
|
+
let balance = 0;
|
|
130
|
+
let deepestStart = -1;
|
|
131
|
+
let deepestEnd = -1;
|
|
132
|
+
let deepestBalance = -1;
|
|
133
|
+
let inSingleQuote = false;
|
|
134
|
+
let inDoubleQuote = false;
|
|
135
|
+
|
|
136
|
+
// First pass: Find the indices of the DEEPEST balanced parenthesis pair.
|
|
137
|
+
for (let i = 0; i < expression.length; i++) {
|
|
138
|
+
const char = expression[i];
|
|
139
|
+
|
|
140
|
+
if (char === '"' && !inSingleQuote) {
|
|
141
|
+
inDoubleQuote = !inDoubleQuote;
|
|
142
|
+
continue;
|
|
143
|
+
} else if (char === "'" && !inDoubleQuote) {
|
|
144
|
+
inSingleQuote = !inDoubleQuote;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (char === '(') {
|
|
153
|
+
balance++;
|
|
154
|
+
// Track the index of the open parenthesis that belongs to the deepest balance level
|
|
155
|
+
if (balance > deepestBalance) {
|
|
156
|
+
deepestBalance = balance;
|
|
157
|
+
deepestStart = i;
|
|
158
|
+
deepestEnd = -1; // Reset end until match is found
|
|
159
|
+
}
|
|
160
|
+
} else if (char === ')') {
|
|
161
|
+
if (balance === deepestBalance) {
|
|
162
|
+
// This is the closing parenthesis that matches the deepest open parenthesis found
|
|
163
|
+
deepestEnd = i;
|
|
164
|
+
}
|
|
165
|
+
balance--;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If we didn't find a balanced pair, or the match is invalid, exit.
|
|
170
|
+
if (deepestStart === -1 || deepestEnd === -1 || deepestEnd <= deepestStart) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Now we have the innermost argument content indices (deepestStart + 1 to deepestEnd - 1)
|
|
175
|
+
const rawArgs = expression.substring(deepestStart + 1, deepestEnd).trim();
|
|
176
|
+
|
|
177
|
+
// Step 2: Find the operator name backward from the deepestStart index.
|
|
178
|
+
let operatorStart = -1;
|
|
179
|
+
let nonWhitespaceFound = false;
|
|
180
|
+
|
|
181
|
+
for (let i = deepestStart - 1; i >= 0; i--) {
|
|
182
|
+
const char = expression[i];
|
|
183
|
+
|
|
184
|
+
// Skip trailing whitespace between operator and '('
|
|
185
|
+
if (!nonWhitespaceFound) {
|
|
186
|
+
if (/\s/.test(char)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
nonWhitespaceFound = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Find the start of the operator name
|
|
193
|
+
// This regex captures letters, numbers, hyphens, and the required $
|
|
194
|
+
let isOperatorChar = /[\w\-\$]/.test(char);
|
|
195
|
+
|
|
196
|
+
if (!isOperatorChar) {
|
|
197
|
+
operatorStart = i + 1;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
operatorStart = i;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (operatorStart === -1) operatorStart = 0;
|
|
204
|
+
|
|
205
|
+
const operatorNameCandidate = expression.substring(operatorStart, deepestStart).trim();
|
|
206
|
+
|
|
207
|
+
// Step 3: Validate the operator name
|
|
208
|
+
if (knownOperatorKeys.includes(operatorNameCandidate)) {
|
|
209
|
+
// Construct the full match string: operatorNameCandidate(rawArgs)
|
|
210
|
+
const fullMatch = expression.substring(operatorStart, deepestEnd + 1);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
operator: operatorNameCandidate,
|
|
214
|
+
args: rawArgs,
|
|
215
|
+
fullMatch: fullMatch
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null; // Operator name invalid or not found
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Main function to find the innermost operator.
|
|
224
|
+
* * Logic flow is updated to prioritize finding the innermost FUNCTION CALL,
|
|
225
|
+
* then fall back to finding a BARE OPERATOR if no function calls remain.
|
|
226
|
+
* * @param {string} expression The expression to parse.
|
|
227
|
+
* @returns {{operator: string, args: string, rawContent: string, fullMatch?: string} | {operator: null, args: string, rawContent: string}}
|
|
228
|
+
*/
|
|
229
|
+
const findInnermostOperator = (expression) => {
|
|
230
|
+
// Helper function to strip leading and trailing parentheses from a string
|
|
231
|
+
function stripParentheses(str) {
|
|
232
|
+
let result = str;
|
|
233
|
+
if (result.startsWith("(")) {
|
|
234
|
+
result = result.substring(1);
|
|
235
|
+
}
|
|
236
|
+
if (result.endsWith(")")) {
|
|
237
|
+
result = result.substring(0, result.length - 1);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
let args;
|
|
242
|
+
|
|
243
|
+
// --- 1. PRIORITY: Find Innermost FUNCTION CALL (Operator with Parentheses) ---
|
|
244
|
+
const functionCall = findInnermostFunctionCall(expression);
|
|
245
|
+
|
|
246
|
+
if (functionCall) {
|
|
247
|
+
// Return the full function expression details, including the full string section
|
|
248
|
+
args = stripParentheses(functionCall.args);
|
|
249
|
+
return {
|
|
250
|
+
operator: functionCall.operator,
|
|
251
|
+
args, // Arguments without parentheses
|
|
252
|
+
rawContent: functionCall.args, // The content inside the parentheses (arguments)
|
|
253
|
+
fullMatch: functionCall.fullMatch // The operator(args) string (the complete section to replace)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- 2. FALLBACK: Find BARE OPERATOR (e.g., $value path) ---
|
|
258
|
+
|
|
259
|
+
// If no function calls are found, the entire expression is treated as the raw content
|
|
260
|
+
const rawContent = expression.trim();
|
|
261
|
+
|
|
262
|
+
// Now check the raw content to see if it starts with a bare operator ($value)
|
|
263
|
+
const innermostOperator = findBareOperatorInPath(rawContent, knownOperatorKeys);
|
|
264
|
+
|
|
265
|
+
if (innermostOperator) {
|
|
266
|
+
const operatorArgs = rawContent.substring(innermostOperator.length).trim();
|
|
267
|
+
args = stripParentheses(operatorArgs);
|
|
268
|
+
return {
|
|
269
|
+
operator: innermostOperator,
|
|
270
|
+
args, // Arguments without parentheses
|
|
271
|
+
rawContent: rawContent,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
args = stripParentheses(rawContent);
|
|
277
|
+
|
|
278
|
+
// Fallback if no known operator is found
|
|
279
|
+
return {
|
|
280
|
+
operator: null,
|
|
281
|
+
args,
|
|
282
|
+
rawContent: rawContent
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
76
286
|
function escapeRegexKey(key) {
|
|
77
287
|
if (key.startsWith("$")) {
|
|
78
288
|
return "\\" + key; // Escape the leading $
|
|
@@ -82,71 +292,150 @@ function escapeRegexKey(key) {
|
|
|
82
292
|
return key; // Should not happen with current keys, but fallback
|
|
83
293
|
}
|
|
84
294
|
|
|
85
|
-
const operatorPattern = knownOperatorKeys.map(escapeRegexKey).join("|");
|
|
86
|
-
|
|
87
|
-
// Regex to find potential operators and their arguments
|
|
88
|
-
// $1: Potential operator identifier (e.g., $user_id, $closestDiv)
|
|
89
|
-
// $2: Arguments within parentheses (optional)
|
|
90
|
-
const regex = new RegExp(`(${operatorPattern})(?:\\s*\\((.*?)\\))?`, "g");
|
|
91
|
-
|
|
92
295
|
/**
|
|
93
296
|
* Synchronously processes a string, finding and replacing operators recursively.
|
|
94
297
|
* Assumes ALL underlying operations (getValue, queryElements) are synchronous.
|
|
95
298
|
* @param {Element | null} element - Context element.
|
|
96
299
|
* @param {string} value - String containing operators.
|
|
97
300
|
* @param {string[]} [exclude=[]] - Operator prefixes to ignore.
|
|
98
|
-
* @returns {string} - Processed string.
|
|
301
|
+
* @returns {string | {value: string, params: Promise[]}} - Processed string or an object containing the partially processed string and unresolved Promises.
|
|
99
302
|
*/
|
|
100
|
-
function processOperators(
|
|
303
|
+
function processOperators(
|
|
304
|
+
element,
|
|
305
|
+
value,
|
|
306
|
+
exclude = [],
|
|
307
|
+
parent,
|
|
308
|
+
params = []
|
|
309
|
+
) {
|
|
101
310
|
// Early exit if no operators are possible or value is not a string
|
|
102
311
|
if (typeof value !== "string" || !value.includes("$")) {
|
|
103
312
|
return value;
|
|
104
313
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
314
|
+
|
|
315
|
+
let processedValue = value;
|
|
316
|
+
let hasPromise = false;
|
|
317
|
+
let parsedValue = null
|
|
318
|
+
|
|
319
|
+
while (processedValue.includes("$")) {
|
|
320
|
+
|
|
321
|
+
// --- PROMISE TOKEN RESOLUTION ---
|
|
322
|
+
// If the processedValue starts with a resolved parameter token from the previous async step,
|
|
323
|
+
// substitute the token with its actual (now resolved) value from the params array.
|
|
324
|
+
const paramMatch = processedValue.match(/^\$\$PARAM_(\d+)\$\$/);
|
|
325
|
+
if (paramMatch && Array.isArray(params) && params.length > 0) {
|
|
326
|
+
const index = parseInt(paramMatch[1], 10);
|
|
327
|
+
if (index < params.length) {
|
|
328
|
+
const resolvedTokenValue = params[index];
|
|
329
|
+
processedValue = processedValue.replace(paramMatch[0], resolvedTokenValue);
|
|
330
|
+
// After replacement, we restart the loop to find the *next* innermost operator
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// --- END TOKEN RESOLUTION ---
|
|
335
|
+
|
|
336
|
+
const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
|
|
337
|
+
|
|
338
|
+
if (!operator) {
|
|
339
|
+
break; // No more operators found
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (operator === "$param" && !args) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (operator && !exclude.includes(operator)) {
|
|
347
|
+
|
|
348
|
+
// --- Determine textToReplace ---
|
|
349
|
+
// The fullMatch property from findInnermostOperator ensures we correctly replace
|
|
350
|
+
// the whole expression (e.g., "$param(...)") or just the bare operator ($value path).
|
|
351
|
+
const textToReplace = fullMatch || rawContent;
|
|
352
|
+
// --- END textToReplace CALCULATION ---
|
|
353
|
+
|
|
354
|
+
let resolvedValue = resolveOperator(element, operator, args, parent, params);
|
|
355
|
+
|
|
356
|
+
if (resolvedValue instanceof Promise) {
|
|
357
|
+
const paramIndex = params.length; // Get the index *before* push
|
|
358
|
+
params.push(resolvedValue); // Store the Promise
|
|
359
|
+
|
|
360
|
+
// CRITICAL FIX: Replace the matched expression with a unique token, then break.
|
|
361
|
+
processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
|
|
362
|
+
hasPromise = true;
|
|
363
|
+
break; // Stop processing and yield
|
|
115
364
|
}
|
|
116
365
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
let resolvedValue = resolveOperator(
|
|
122
|
-
element,
|
|
123
|
-
operator,
|
|
124
|
-
args.replace(/^['"]|['"]$/g, "").trim(),
|
|
125
|
-
parent
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
if (operator === "$param") {
|
|
129
|
-
params.push(resolvedValue);
|
|
130
|
-
return "";
|
|
131
|
-
}
|
|
366
|
+
if (params.some((p) => p instanceof Promise)) {
|
|
367
|
+
hasPromise = true;
|
|
368
|
+
break; // A nested call found a promise, stop and yield
|
|
369
|
+
}
|
|
132
370
|
|
|
133
|
-
|
|
371
|
+
let replacement = "";
|
|
372
|
+
if (operator === "$param") {
|
|
373
|
+
params.push(resolvedValue);
|
|
134
374
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
375
|
+
replacement = resolvedValue ?? "";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (processedValue === textToReplace) {
|
|
379
|
+
processedValue = replacement;
|
|
380
|
+
break;
|
|
138
381
|
}
|
|
382
|
+
|
|
383
|
+
processedValue = processedValue.replace(textToReplace, replacement);
|
|
384
|
+
|
|
385
|
+
if (!processedValue.includes("$")) {
|
|
386
|
+
// If there are still unresolved operators, we need to continue processing
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
} else {
|
|
391
|
+
// If operator is excluded, we need to advance past it to avoid infinite loop
|
|
392
|
+
break;
|
|
139
393
|
}
|
|
140
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (hasPromise) {
|
|
397
|
+
return { value: processedValue, params };
|
|
398
|
+
}
|
|
141
399
|
|
|
142
400
|
if (params.length) {
|
|
143
|
-
|
|
401
|
+
if (processedValue.trim() === "") {
|
|
402
|
+
return params;
|
|
403
|
+
}
|
|
144
404
|
}
|
|
145
405
|
|
|
146
406
|
return processedValue;
|
|
147
407
|
}
|
|
148
408
|
|
|
149
|
-
|
|
409
|
+
async function processOperatorsAsync(
|
|
410
|
+
element,
|
|
411
|
+
value,
|
|
412
|
+
exclude = [],
|
|
413
|
+
parent,
|
|
414
|
+
params = []
|
|
415
|
+
) {
|
|
416
|
+
let result = processOperators(element, value, exclude, parent, params);
|
|
417
|
+
|
|
418
|
+
while (typeof result === "object" && result.params) {
|
|
419
|
+
const resolvedParams = await Promise.all(result.params);
|
|
420
|
+
// Note: The second argument passed to processOperators here is the partially processed string (result.value)
|
|
421
|
+
// which now contains the PARAM tokens. The third argument is the array of resolved values (resolvedParams)
|
|
422
|
+
// which will be used to replace those tokens in the subsequent processOperators call.
|
|
423
|
+
result = processOperators(
|
|
424
|
+
element,
|
|
425
|
+
result.value,
|
|
426
|
+
exclude,
|
|
427
|
+
parent,
|
|
428
|
+
resolvedParams
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (result instanceof Promise) {
|
|
433
|
+
return await result;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
150
439
|
/**
|
|
151
440
|
* Synchronously determines and executes the action for processing a single operator token.
|
|
152
441
|
* @param {HTMLElement|null} element - The context element from which to derive values or execute methods.
|
|
@@ -155,18 +444,28 @@ function processOperators(element, value, exclude = [], parent) {
|
|
|
155
444
|
* @param {string} parent - Context in which the function is called, potentially affecting behavior or processing.
|
|
156
445
|
* @returns {string} The final resolved value after applying the operator to the given elements.
|
|
157
446
|
*/
|
|
158
|
-
function resolveOperator(element, operator, args, parent) {
|
|
447
|
+
function resolveOperator(element, operator, args, parent, params) {
|
|
448
|
+
// If a promise is already in the params, we must stop and wait for it to be resolved.
|
|
449
|
+
if (params.some((p) => p instanceof Promise)) {
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
|
|
159
453
|
// If args contain any operators (indicated by '$'), process them recursively
|
|
160
|
-
if (args && args.includes("$")) {
|
|
454
|
+
if (args && typeof args === "string" && args.includes("$")) {
|
|
161
455
|
// Reprocess args to resolve any nested operators
|
|
162
|
-
args = processOperators(element, args, "", operator);
|
|
456
|
+
args = processOperators(element, args, "", operator, params);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (params.some((p) => p instanceof Promise)) {
|
|
460
|
+
return operator;
|
|
163
461
|
}
|
|
164
462
|
|
|
165
463
|
// Initialize an array of elements to operate on, starting with the single element reference if provided
|
|
166
464
|
let targetElements = element ? [element] : [];
|
|
167
|
-
|
|
168
|
-
// If
|
|
169
|
-
|
|
465
|
+
|
|
466
|
+
// If the argument is a string and the operator is NOT a custom utility that expects raw data
|
|
467
|
+
// (like $param, $parse), we assume the argument is a selector.
|
|
468
|
+
if (args && typeof args === "string" && !customOperators.has(operator)) {
|
|
170
469
|
targetElements = queryElements({
|
|
171
470
|
element, // Use the context element as the base for querying
|
|
172
471
|
selector: args // Selector from args to find matching elements
|
|
@@ -182,7 +481,7 @@ function resolveOperator(element, operator, args, parent) {
|
|
|
182
481
|
// If the result is a string and still contains unresolved operators, process them further
|
|
183
482
|
if (value && typeof value === "string" && value.includes("$")) {
|
|
184
483
|
// Resolve any remaining operators within the value string
|
|
185
|
-
value = processOperators(element, value, parent);
|
|
484
|
+
value = processOperators(element, value, parent, params);
|
|
186
485
|
}
|
|
187
486
|
|
|
188
487
|
// Return the final processed value, fully resolved
|
|
@@ -216,8 +515,10 @@ function processValues(elements, operator, args, parent) {
|
|
|
216
515
|
if (typeof rawValue === "function") {
|
|
217
516
|
// If arguments are provided as an array
|
|
218
517
|
if (Array.isArray(args)) {
|
|
219
|
-
// If
|
|
220
|
-
if (args.
|
|
518
|
+
// If the custom operator is NOT $param and has arguments, something is wrong with the flow.
|
|
519
|
+
// However, if it's a generic method on an element (like $setValue), the args are passed via spread.
|
|
520
|
+
if (customOperators.has(operator) && operator !== "$setValue" && operator !== "$getObjectValue" && args.length) {
|
|
521
|
+
// For simple custom operators that don't take multiple args, return an empty string.
|
|
221
522
|
return "";
|
|
222
523
|
}
|
|
223
524
|
// Invoke the function using the element and spread array arguments
|
|
@@ -235,6 +536,12 @@ function processValues(elements, operator, args, parent) {
|
|
|
235
536
|
return rawValue;
|
|
236
537
|
}
|
|
237
538
|
} else {
|
|
539
|
+
if (
|
|
540
|
+
rawValue instanceof Promise ||
|
|
541
|
+
(typeof rawValue === "object" && rawValue !== null)
|
|
542
|
+
) {
|
|
543
|
+
return rawValue;
|
|
544
|
+
}
|
|
238
545
|
// Otherwise, append the stringified rawValue to the aggregated result, defaulting to an empty string if it's nullish
|
|
239
546
|
aggregatedString += String(rawValue ?? "");
|
|
240
547
|
}
|
|
@@ -267,4 +574,4 @@ function getSubdomain() {
|
|
|
267
574
|
return null;
|
|
268
575
|
}
|
|
269
576
|
|
|
270
|
-
export { processOperators };
|
|
577
|
+
export { processOperators, processOperatorsAsync };
|
package/src/setValue.js
CHANGED
|
@@ -87,7 +87,14 @@ const setValue = (el, value, dispatch) => {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
} else if (el.type === "radio") {
|
|
90
|
-
|
|
90
|
+
const wasChecked = el.checked;
|
|
91
|
+
if (el.value == value) {
|
|
92
|
+
el.checked = true;
|
|
93
|
+
} else {
|
|
94
|
+
el.checked = false;
|
|
95
|
+
}
|
|
96
|
+
// Only continue if checked state changed
|
|
97
|
+
if (el.checked === wasChecked) return;
|
|
91
98
|
} else if (el.type === "password") {
|
|
92
99
|
el.value = __decryptPassword(value);
|
|
93
100
|
} else if (el.tagName == "SELECT") {
|