@cocreate/element-prototype 1.29.2 → 1.30.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.30.0](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.29.3...v1.30.0) (2025-10-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * add async support for getAttribute and enhance processOperators with async handling ([88cfe33](https://github.com/CoCreate-app/CoCreate-element-prototype/commit/88cfe33deff47ecd3c80b1c4c8b2de04b38530b1))
7
+
8
+ ## [1.29.3](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.29.2...v1.29.3) (2025-07-13)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * improve handling of radio inputs and select elements in setValue function ([8fb0fba](https://github.com/CoCreate-app/CoCreate-element-prototype/commit/8fb0fbade7136dfd8eefd1109d30f3ec02bcc32a))
14
+
1
15
  ## [1.29.2](https://github.com/CoCreate-app/CoCreate-element-prototype/compare/v1.29.1...v1.29.2) (2025-05-01)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,18 +1,10 @@
1
1
  {
2
2
  "name": "@cocreate/element-prototype",
3
- "version": "1.29.2",
3
+ "version": "1.30.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",
7
- "cocreate",
8
- "low-code-framework",
9
- "no-code-framework",
10
- "cocreatejs",
11
- "cocreatejs-component",
12
- "cocreate-framework",
13
- "no-code",
14
7
  "low-code",
15
- "collaborative-framework",
16
8
  "realtime",
17
9
  "realtime-framework",
18
10
  "collaboration",
@@ -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
@@ -56,7 +56,10 @@ const getValue = (element, valueType) => {
56
56
  case "radio":
57
57
  const key = element.getAttribute("key");
58
58
  // Handles radio inputs by selecting the checked radio's value in the group with the same key
59
- value = document.querySelector(`input[key="${key}"]:checked`).value;
59
+ let el = document.querySelector(`input[key="${key}"]:checked`);
60
+ if (el) {
61
+ value = el.value;
62
+ }
60
63
  break;
61
64
 
62
65
  case "number":
package/src/operators.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ObjectId, queryElements } from "@cocreate/utils";
1
+ import { ObjectId, queryElements, getValueFromObject } from "@cocreate/utils";
2
2
 
3
3
  // Operators handled directly for simple, synchronous value retrieval
4
4
  const customOperators = new Map(
@@ -8,6 +8,8 @@ const customOperators = new Map(
8
8
  $clientId: () => localStorage.getItem("clientId"),
9
9
  $session_id: () => localStorage.getItem("session_id"),
10
10
  $value: (element) => element.getValue() || "",
11
+ // TODO: get length of value
12
+ // $length: (element) => {element.getValue() || ""},
11
13
  $innerWidth: () => window.innerWidth,
12
14
  $innerHeight: () => window.innerHeight,
13
15
  $href: () => window.location.href.replace(/\/$/, ""),
@@ -34,7 +36,25 @@ const customOperators = new Map(
34
36
  return path === "/" ? "" : path;
35
37
  },
36
38
  $param: (element, args) => args,
37
- $setValue: (element, args) => element.setValue(...args) || ""
39
+ $getObjectValue: (element, args) => {
40
+ if (Array.isArray(args) && args.length >= 2) {
41
+ return getValueFromObject(args[0], args[1]);
42
+ }
43
+ return "";
44
+ },
45
+ $setValue: (element, args) => element.setValue(...args) || "",
46
+ $true: () => true,
47
+ $false: () => false,
48
+ // TODO: Handle uuid generation
49
+ // $uid: () => uid.generate(6),
50
+ $parse: (element, args) => {
51
+ let value = args || "";
52
+ try {
53
+ return JSON.parse(value);
54
+ } catch (e) {
55
+ return value;
56
+ }
57
+ }
38
58
  })
39
59
  );
40
60
 
@@ -49,7 +69,8 @@ const propertyOperators = new Set([
49
69
  "$className",
50
70
  "$textContent",
51
71
  "$innerHTML",
52
- "$getValue"
72
+ "$getValue",
73
+ "$reset"
53
74
  ]);
54
75
 
55
76
  // Combine all known operator keys for the main parsing regex
@@ -58,6 +79,172 @@ const knownOperatorKeys = [
58
79
  ...propertyOperators
59
80
  ].sort((a, b) => b.length - a.length);
60
81
 
82
+ /**
83
+ * Helper function to check if a string path starts with a known bare operator.
84
+ * This logic is necessary to separate '$value' from '[0].src' when no parentheses remain.
85
+ */
86
+ const findBareOperatorInPath = (path, knownOperatorKeys) => {
87
+ const trimmedPath = path.trim();
88
+ for (const key of knownOperatorKeys) {
89
+ if (trimmedPath.startsWith(key)) {
90
+ const remaining = trimmedPath.substring(key.length);
91
+ // Edge Case Fix: Ensure the operator is followed by a space, bracket, dot, or end-of-string.
92
+ // This prevents "valueThing" from being incorrectly split into "$value" + "Thing".
93
+ if (remaining.length === 0 || /^\s|\[|\./.test(remaining)) {
94
+ return key;
95
+ }
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Finds the innermost function call (operator + its balanced parentheses argument)
103
+ * in the expression. This is the core logic for iterative parsing.
104
+ * * @param {string} expression The full expression string.
105
+ * @returns {{operator: string, args: string, fullMatch: string} | null} Details of the innermost function call, or null.
106
+ */
107
+ const findInnermostFunctionCall = (expression) => {
108
+ let balance = 0;
109
+ let deepestStart = -1;
110
+ let deepestEnd = -1;
111
+ let deepestBalance = -1;
112
+ let inSingleQuote = false;
113
+ let inDoubleQuote = false;
114
+
115
+ // First pass: Find the indices of the DEEPEST balanced parenthesis pair.
116
+ for (let i = 0; i < expression.length; i++) {
117
+ const char = expression[i];
118
+
119
+ if (char === '"' && !inSingleQuote) {
120
+ inDoubleQuote = !inDoubleQuote;
121
+ continue;
122
+ } else if (char === "'" && !inDoubleQuote) {
123
+ inSingleQuote = !inDoubleQuote;
124
+ continue;
125
+ }
126
+
127
+ if (inSingleQuote || inDoubleQuote) {
128
+ continue;
129
+ }
130
+
131
+ if (char === '(') {
132
+ balance++;
133
+ // Track the index of the open parenthesis that belongs to the deepest balance level
134
+ if (balance > deepestBalance) {
135
+ deepestBalance = balance;
136
+ deepestStart = i;
137
+ deepestEnd = -1; // Reset end until match is found
138
+ }
139
+ } else if (char === ')') {
140
+ if (balance === deepestBalance) {
141
+ // This is the closing parenthesis that matches the deepest open parenthesis found
142
+ deepestEnd = i;
143
+ }
144
+ balance--;
145
+ }
146
+ }
147
+
148
+ // If we didn't find a balanced pair, or the match is invalid, exit.
149
+ if (deepestStart === -1 || deepestEnd === -1 || deepestEnd <= deepestStart) {
150
+ return null;
151
+ }
152
+
153
+ // Now we have the innermost argument content indices (deepestStart + 1 to deepestEnd - 1)
154
+ const rawArgs = expression.substring(deepestStart + 1, deepestEnd).trim();
155
+
156
+ // Step 2: Find the operator name backward from the deepestStart index.
157
+ let operatorStart = -1;
158
+ let nonWhitespaceFound = false;
159
+
160
+ for (let i = deepestStart - 1; i >= 0; i--) {
161
+ const char = expression[i];
162
+
163
+ // Skip trailing whitespace between operator and '('
164
+ if (!nonWhitespaceFound) {
165
+ if (/\s/.test(char)) {
166
+ continue;
167
+ }
168
+ nonWhitespaceFound = true;
169
+ }
170
+
171
+ // Find the start of the operator name
172
+ // This regex captures letters, numbers, hyphens, and the required $
173
+ let isOperatorChar = /[\w\-\$]/.test(char);
174
+
175
+ if (!isOperatorChar) {
176
+ operatorStart = i + 1;
177
+ break;
178
+ }
179
+ operatorStart = i;
180
+ }
181
+
182
+ if (operatorStart === -1) operatorStart = 0;
183
+
184
+ const operatorNameCandidate = expression.substring(operatorStart, deepestStart).trim();
185
+
186
+ // Step 3: Validate the operator name
187
+ if (knownOperatorKeys.includes(operatorNameCandidate)) {
188
+ // Construct the full match string: operatorNameCandidate(rawArgs)
189
+ const fullMatch = expression.substring(operatorStart, deepestEnd + 1);
190
+
191
+ return {
192
+ operator: operatorNameCandidate,
193
+ args: rawArgs,
194
+ fullMatch: fullMatch
195
+ };
196
+ }
197
+
198
+ return null; // Operator name invalid or not found
199
+ };
200
+
201
+ /**
202
+ * Main function to find the innermost operator.
203
+ * * Logic flow is updated to prioritize finding the innermost FUNCTION CALL,
204
+ * then fall back to finding a BARE OPERATOR if no function calls remain.
205
+ * * @param {string} expression The expression to parse.
206
+ * @returns {{operator: string, args: string, rawContent: string, fullMatch?: string} | {operator: null, args: string, rawContent: string}}
207
+ */
208
+ const findInnermostOperator = (expression) => {
209
+
210
+ // --- 1. PRIORITY: Find Innermost FUNCTION CALL (Operator with Parentheses) ---
211
+ const functionCall = findInnermostFunctionCall(expression);
212
+
213
+ if (functionCall) {
214
+ // Return the full function expression details, including the full string section
215
+ return {
216
+ operator: functionCall.operator,
217
+ args: functionCall.args,
218
+ rawContent: functionCall.args, // The content inside the parentheses (arguments)
219
+ fullMatch: functionCall.fullMatch // The operator(args) string (the complete section to replace)
220
+ };
221
+ }
222
+
223
+ // --- 2. FALLBACK: Find BARE OPERATOR (e.g., $value path) ---
224
+
225
+ // If no function calls are found, the entire expression is treated as the raw content
226
+ const rawContent = expression.trim();
227
+
228
+ // Now check the raw content to see if it starts with a bare operator ($value)
229
+ const innermostOperator = findBareOperatorInPath(rawContent, knownOperatorKeys);
230
+
231
+ if (innermostOperator) {
232
+ const operatorArgs = rawContent.substring(innermostOperator.length).trim();
233
+ return {
234
+ operator: innermostOperator,
235
+ args: operatorArgs,
236
+ rawContent: rawContent,
237
+ };
238
+ }
239
+
240
+ // Fallback if no known operator is found
241
+ return {
242
+ operator: null,
243
+ args: rawContent,
244
+ rawContent: rawContent
245
+ };
246
+ };
247
+
61
248
  function escapeRegexKey(key) {
62
249
  if (key.startsWith("$")) {
63
250
  return "\\" + key; // Escape the leading $
@@ -67,71 +254,140 @@ function escapeRegexKey(key) {
67
254
  return key; // Should not happen with current keys, but fallback
68
255
  }
69
256
 
70
- const operatorPattern = knownOperatorKeys.map(escapeRegexKey).join("|");
71
-
72
- // Regex to find potential operators and their arguments
73
- // $1: Potential operator identifier (e.g., $user_id, $closestDiv)
74
- // $2: Arguments within parentheses (optional)
75
- const regex = new RegExp(`(${operatorPattern})(?:\\s*\\((.*?)\\))?`, "g");
76
-
77
257
  /**
78
258
  * Synchronously processes a string, finding and replacing operators recursively.
79
259
  * Assumes ALL underlying operations (getValue, queryElements) are synchronous.
80
260
  * @param {Element | null} element - Context element.
81
261
  * @param {string} value - String containing operators.
82
262
  * @param {string[]} [exclude=[]] - Operator prefixes to ignore.
83
- * @returns {string} - Processed string.
263
+ * @returns {string | {value: string, params: Promise[]}} - Processed string or an object containing the partially processed string and unresolved Promises.
84
264
  */
85
- function processOperators(element, value, exclude = [], parent) {
265
+ function processOperators(
266
+ element,
267
+ value,
268
+ exclude = [],
269
+ parent,
270
+ params = []
271
+ ) {
86
272
  // Early exit if no operators are possible or value is not a string
87
273
  if (typeof value !== "string" || !value.includes("$")) {
88
274
  return value;
89
275
  }
90
- let params = [];
91
- const processedValue = value.replace(
92
- regex,
93
- (match, operator, args = "") => {
94
- // 'match' is the full matched string (e.g., "$closest(.myClass)")
95
- // 'operator' is the identifier part (e.g., "$closest")
96
- // 'args' is the content within parentheses (e.g., ".myClass") or "" if no parentheses
97
-
98
- if (operator === "$param" && !args) {
99
- return match;
276
+
277
+ let processedValue = value;
278
+ let hasPromise = false;
279
+
280
+ while (processedValue.includes("$")) {
281
+
282
+ // --- PROMISE TOKEN RESOLUTION ---
283
+ // If the processedValue starts with a resolved parameter token from the previous async step,
284
+ // substitute the token with its actual (now resolved) value from the params array.
285
+ const paramMatch = processedValue.match(/^\$\$PARAM_(\d+)\$\$/);
286
+ if (paramMatch && Array.isArray(params) && params.length > 0) {
287
+ const index = parseInt(paramMatch[1], 10);
288
+ if (index < params.length) {
289
+ const resolvedTokenValue = params[index];
290
+ processedValue = processedValue.replace(paramMatch[0], resolvedTokenValue);
291
+ // After replacement, we restart the loop to find the *next* innermost operator
292
+ continue;
293
+ }
294
+ }
295
+ // --- END TOKEN RESOLUTION ---
296
+
297
+ const { operator, args, rawContent, fullMatch } = findInnermostOperator(processedValue);
298
+
299
+ if (!operator) {
300
+ break; // No more operators found
301
+ }
302
+
303
+ if (operator === "$param" && !args) {
304
+ break;
305
+ }
306
+
307
+ if (operator && !exclude.includes(operator)) {
308
+
309
+ // --- Determine textToReplace ---
310
+ // The fullMatch property from findInnermostOperator ensures we correctly replace
311
+ // the whole expression (e.g., "$param(...)") or just the bare operator ($value path).
312
+ const textToReplace = fullMatch || rawContent;
313
+ // --- END textToReplace CALCULATION ---
314
+
315
+ let resolvedValue = resolveOperator(element, operator, args, parent, params);
316
+
317
+ if (resolvedValue instanceof Promise) {
318
+ const paramIndex = params.length; // Get the index *before* push
319
+ params.push(resolvedValue); // Store the Promise
320
+
321
+ // CRITICAL FIX: Replace the matched expression with a unique token, then break.
322
+ processedValue = processedValue.replace(textToReplace, `$$PARAM_${paramIndex}$$`);
323
+ hasPromise = true;
324
+ break; // Stop processing and yield
100
325
  }
101
326
 
102
- // If a valid operator was identified AND it's not in the exclude list
103
- if (operator && !exclude.includes(operator)) {
104
- // Resolve the value for the identified operator and its arguments
105
- // Pass the *trimmed* arguments to the resolver
106
- let resolvedValue = resolveOperator(
107
- element,
108
- operator,
109
- args.replace(/^['"]|['"]$/g, "").trim(),
110
- parent
111
- );
112
-
113
- if (operator === "$param") {
114
- params.push(resolvedValue);
115
- return "";
116
- }
327
+ if (params.some((p) => p instanceof Promise)) {
328
+ hasPromise = true;
329
+ break; // A nested call found a promise, stop and yield
330
+ }
117
331
 
118
- return resolvedValue ?? "";
332
+ let replacement = "";
333
+ if (operator === "$param") {
334
+ params.push(resolvedValue);
119
335
  } else {
120
- // If no known operator matched, or if it was excluded,
121
- // return the original matched string (no replacement).
122
- return match;
336
+ replacement = resolvedValue ?? "";
123
337
  }
338
+
339
+ // Manually replace the matched part of the string
340
+ processedValue = processedValue.replace(textToReplace, replacement);
341
+
342
+ } else {
343
+ // If operator is excluded, we need to advance past it to avoid infinite loop
344
+ break;
124
345
  }
125
- );
346
+ }
347
+
348
+ if (hasPromise) {
349
+ return { value: processedValue, params };
350
+ }
126
351
 
127
352
  if (params.length) {
128
- return params;
353
+ if (processedValue.trim() === "") {
354
+ return params;
355
+ }
129
356
  }
130
357
 
131
358
  return processedValue;
132
359
  }
133
360
 
134
- /**
361
+ async function processOperatorsAsync(
362
+ element,
363
+ value,
364
+ exclude = [],
365
+ parent,
366
+ params = []
367
+ ) {
368
+ let result = processOperators(element, value, exclude, parent, params);
369
+
370
+ while (typeof result === "object" && result.params) {
371
+ const resolvedParams = await Promise.all(result.params);
372
+ // Note: The second argument passed to processOperators here is the partially processed string (result.value)
373
+ // which now contains the PARAM tokens. The third argument is the array of resolved values (resolvedParams)
374
+ // which will be used to replace those tokens in the subsequent processOperators call.
375
+ result = processOperators(
376
+ element,
377
+ result.value,
378
+ exclude,
379
+ parent,
380
+ resolvedParams
381
+ );
382
+ }
383
+
384
+ if (result instanceof Promise) {
385
+ return await result;
386
+ }
387
+
388
+ return result;
389
+ }
390
+
135
391
  /**
136
392
  * Synchronously determines and executes the action for processing a single operator token.
137
393
  * @param {HTMLElement|null} element - The context element from which to derive values or execute methods.
@@ -140,18 +396,28 @@ function processOperators(element, value, exclude = [], parent) {
140
396
  * @param {string} parent - Context in which the function is called, potentially affecting behavior or processing.
141
397
  * @returns {string} The final resolved value after applying the operator to the given elements.
142
398
  */
143
- function resolveOperator(element, operator, args, parent) {
399
+ function resolveOperator(element, operator, args, parent, params) {
400
+ // If a promise is already in the params, we must stop and wait for it to be resolved.
401
+ if (params.some((p) => p instanceof Promise)) {
402
+ return "";
403
+ }
404
+
144
405
  // If args contain any operators (indicated by '$'), process them recursively
145
- if (args && args.includes("$")) {
406
+ if (args && typeof args === "string" && args.includes("$")) {
146
407
  // Reprocess args to resolve any nested operators
147
- args = processOperators(element, args, "", operator);
408
+ args = processOperators(element, args, "", operator, params);
409
+ }
410
+
411
+ if (params.some((p) => p instanceof Promise)) {
412
+ return operator;
148
413
  }
149
414
 
150
415
  // Initialize an array of elements to operate on, starting with the single element reference if provided
151
416
  let targetElements = element ? [element] : [];
152
-
153
- // If args are provided as a string, treat it as a selector to find applicable target elements
154
- if (args && typeof args === "string") {
417
+
418
+ // If the argument is a string and the operator is NOT a custom utility that expects raw data
419
+ // (like $param, $parse), we assume the argument is a selector.
420
+ if (args && typeof args === "string" && !customOperators.has(operator)) {
155
421
  targetElements = queryElements({
156
422
  element, // Use the context element as the base for querying
157
423
  selector: args // Selector from args to find matching elements
@@ -167,7 +433,7 @@ function resolveOperator(element, operator, args, parent) {
167
433
  // If the result is a string and still contains unresolved operators, process them further
168
434
  if (value && typeof value === "string" && value.includes("$")) {
169
435
  // Resolve any remaining operators within the value string
170
- value = processOperators(element, value, parent);
436
+ value = processOperators(element, value, parent, params);
171
437
  }
172
438
 
173
439
  // Return the final processed value, fully resolved
@@ -201,8 +467,10 @@ function processValues(elements, operator, args, parent) {
201
467
  if (typeof rawValue === "function") {
202
468
  // If arguments are provided as an array
203
469
  if (Array.isArray(args)) {
204
- // If there are arguments, exit by returning an empty string (assumes args should not be used here)
205
- if (args.length) {
470
+ // If the custom operator is NOT $param and has arguments, something is wrong with the flow.
471
+ // However, if it's a generic method on an element (like $setValue), the args are passed via spread.
472
+ if (customOperators.has(operator) && operator !== "$setValue" && operator !== "$getObjectValue" && args.length) {
473
+ // For simple custom operators that don't take multiple args, return an empty string.
206
474
  return "";
207
475
  }
208
476
  // Invoke the function using the element and spread array arguments
@@ -220,6 +488,12 @@ function processValues(elements, operator, args, parent) {
220
488
  return rawValue;
221
489
  }
222
490
  } else {
491
+ if (
492
+ rawValue instanceof Promise ||
493
+ (typeof rawValue === "object" && rawValue !== null)
494
+ ) {
495
+ return rawValue;
496
+ }
223
497
  // Otherwise, append the stringified rawValue to the aggregated result, defaulting to an empty string if it's nullish
224
498
  aggregatedString += String(rawValue ?? "");
225
499
  }
@@ -252,4 +526,4 @@ function getSubdomain() {
252
526
  return null;
253
527
  }
254
528
 
255
- export { processOperators };
529
+ export { processOperators, processOperatorsAsync };
package/src/setValue.js CHANGED
@@ -52,7 +52,7 @@ const setValue = (el, value, dispatch) => {
52
52
  if (
53
53
  el.tagName == "INPUT" ||
54
54
  el.tagName == "TEXTAREA" ||
55
- (el.tagName == "SELECT" && el.options.length)
55
+ el.tagName == "SELECT"
56
56
  ) {
57
57
  // TODO: attribute config undefined when used with onload-value
58
58
  let isCrdt = el.getAttribute("crdt");
@@ -71,9 +71,11 @@ const setValue = (el, value, dispatch) => {
71
71
  if (inputs[i].value) {
72
72
  if (value === true || value === false)
73
73
  inputs[i].checked = value;
74
- else if (value.includes(inputValue))
74
+ else if (value.includes(inputValue) || value === "on") {
75
75
  inputs[i].checked = true;
76
- else inputs[i].checked = false;
76
+ } else {
77
+ inputs[i].checked = false;
78
+ }
77
79
  } else {
78
80
  if (
79
81
  value === "true" ||
@@ -85,14 +87,17 @@ const setValue = (el, value, dispatch) => {
85
87
  }
86
88
  }
87
89
  } else if (el.type === "radio") {
88
- el.value == value ? (el.checked = true) : (el.checked = false);
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;
89
98
  } else if (el.type === "password") {
90
99
  el.value = __decryptPassword(value);
91
- } else if (
92
- el.tagName == "SELECT" &&
93
- el.hasAttribute("multiple") &&
94
- Array.isArray(value)
95
- ) {
100
+ } else if (el.tagName == "SELECT") {
96
101
  let options = el.options;
97
102
  for (let i = 0; i < options.length; i++) {
98
103
  if (value.includes && value.includes(options[i].value)) {