@schukai/monster 4.124.1 → 4.125.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 +19 -0
- package/package.json +1 -1
- package/source/components/datatable/filter.mjs +13 -3
- package/source/components/form/digits.mjs +41 -16
- package/source/components/form/toggle-switch.mjs +31 -0
- package/source/dom/constants.mjs +8 -0
- package/source/dom/updater.mjs +275 -0
- package/test/cases/components/form/toggle-switch.mjs +15 -0
- package/test/cases/dom/updater.mjs +113 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.125.0] - 2026-03-03
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Add new issue 385 HTML and JS files with property attribute updates
|
|
10
|
+
### Changes
|
|
11
|
+
|
|
12
|
+
- close issue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [4.124.2] - 2026-03-02
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- **digits:** stabilize rendering and input behavior ([#384](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/384))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
## [4.124.1] - 2026-02-23
|
|
6
25
|
|
|
7
26
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.5","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.125.0"}
|
|
@@ -1311,8 +1311,14 @@ function collectSearchQueries() {
|
|
|
1311
1311
|
}
|
|
1312
1312
|
|
|
1313
1313
|
const controlValue = getControlValuesFromLabel(element);
|
|
1314
|
-
|
|
1315
|
-
|
|
1314
|
+
const hasEmptyArrayValue =
|
|
1315
|
+
isArray(controlValue) && controlValue.length === 0;
|
|
1316
|
+
|
|
1317
|
+
if (!controlValue || hasEmptyArrayValue) {
|
|
1318
|
+
if (
|
|
1319
|
+
(controlValue === "" || hasEmptyArrayValue) &&
|
|
1320
|
+
currentHash?.[this.id]?.[id]
|
|
1321
|
+
) {
|
|
1316
1322
|
delete currentHash[this.id][id];
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
@@ -1324,9 +1330,13 @@ function collectSearchQueries() {
|
|
|
1324
1330
|
}
|
|
1325
1331
|
currentHash[this.id][id] = controlValue;
|
|
1326
1332
|
|
|
1333
|
+
const formatterValue = isArray(controlValue)
|
|
1334
|
+
? controlValue.join(",")
|
|
1335
|
+
: controlValue;
|
|
1336
|
+
|
|
1327
1337
|
const mapping = {
|
|
1328
1338
|
id,
|
|
1329
|
-
value:
|
|
1339
|
+
value: formatterValue,
|
|
1330
1340
|
label,
|
|
1331
1341
|
};
|
|
1332
1342
|
|
|
@@ -100,10 +100,11 @@ class Digits extends CustomControl {
|
|
|
100
100
|
* @property {string} value
|
|
101
101
|
*/
|
|
102
102
|
set value(value) {
|
|
103
|
-
const
|
|
103
|
+
const normalizedValue = value == null ? "" : String(value);
|
|
104
|
+
const chars = normalizedValue.split("");
|
|
104
105
|
|
|
105
|
-
this.setOption("value",
|
|
106
|
-
this.setFormValue(
|
|
106
|
+
this.setOption("value", normalizedValue);
|
|
107
|
+
this.setFormValue(normalizedValue);
|
|
107
108
|
|
|
108
109
|
if (chars.every(checkCharacter.bind(this))) {
|
|
109
110
|
this.setValidity({ badInput: false }, "");
|
|
@@ -209,7 +210,9 @@ function updateDigitControls() {
|
|
|
209
210
|
|
|
210
211
|
const controls = [];
|
|
211
212
|
|
|
212
|
-
const values = this.getOption("value")
|
|
213
|
+
const values = this.getOption("value") == null
|
|
214
|
+
? ""
|
|
215
|
+
: String(this.getOption("value"));
|
|
213
216
|
|
|
214
217
|
if (this[digitsElementSymbol]) {
|
|
215
218
|
this[digitsElementSymbol].style.gridTemplateColumns =
|
|
@@ -218,11 +221,17 @@ function updateDigitControls() {
|
|
|
218
221
|
|
|
219
222
|
for (let i = 0; i < digits; i++) {
|
|
220
223
|
controls.push({
|
|
221
|
-
value: values[i] ?? "
|
|
224
|
+
value: values[i] ?? "",
|
|
222
225
|
});
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
this.setOption("digitsControls", controls);
|
|
229
|
+
|
|
230
|
+
// Keep real input.value in sync with the option model. Attribute updates alone
|
|
231
|
+
// can drift after user interaction because inputs keep an internal dirty value.
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
syncInputValues.call(this, controls);
|
|
234
|
+
}, 0);
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
/**
|
|
@@ -287,7 +296,7 @@ function initEventHandler() {
|
|
|
287
296
|
}
|
|
288
297
|
|
|
289
298
|
if (pressedKey === "Backspace") {
|
|
290
|
-
if (inputControl.value !== ""
|
|
299
|
+
if (inputControl.value !== "") {
|
|
291
300
|
event.preventDefault();
|
|
292
301
|
inputControl.value = "";
|
|
293
302
|
collectValues.call(self);
|
|
@@ -313,16 +322,14 @@ function initEventHandler() {
|
|
|
313
322
|
return;
|
|
314
323
|
}
|
|
315
324
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
nextControl.focus();
|
|
322
|
-
}
|
|
323
|
-
collectValues.call(self);
|
|
324
|
-
return;
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
inputControl.value = pressedKey;
|
|
327
|
+
const nextControl = inputControl.nextElementSibling;
|
|
328
|
+
if (nextControl && nextControl.tagName === "INPUT") {
|
|
329
|
+
nextControl.focus();
|
|
325
330
|
}
|
|
331
|
+
collectValues.call(self);
|
|
332
|
+
return;
|
|
326
333
|
}
|
|
327
334
|
});
|
|
328
335
|
|
|
@@ -344,10 +351,28 @@ function initEventHandler() {
|
|
|
344
351
|
function collectValues() {
|
|
345
352
|
const controlsValues = Array.from(
|
|
346
353
|
this[digitsElementSymbol].querySelectorAll("input"),
|
|
347
|
-
).map((input) => input.value || "
|
|
354
|
+
).map((input) => input.value || "");
|
|
348
355
|
this.value = controlsValues.join("");
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
/**
|
|
359
|
+
* @private
|
|
360
|
+
* @param {Array<{value:string}>} controls
|
|
361
|
+
* @returns {void}
|
|
362
|
+
*/
|
|
363
|
+
function syncInputValues(controls) {
|
|
364
|
+
if (!this[digitsElementSymbol]) return;
|
|
365
|
+
const inputs = this[digitsElementSymbol].querySelectorAll("input");
|
|
366
|
+
if (!inputs.length) return;
|
|
367
|
+
|
|
368
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
369
|
+
const nextValue = controls?.[i]?.value ?? "";
|
|
370
|
+
if (inputs[i].value !== nextValue) {
|
|
371
|
+
inputs[i].value = nextValue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
351
376
|
/**
|
|
352
377
|
* @private
|
|
353
378
|
* @return {void}
|
|
@@ -297,6 +297,8 @@ class ToggleSwitch extends CustomControl {
|
|
|
297
297
|
) {
|
|
298
298
|
if (this.getOption("value") !== normalized) {
|
|
299
299
|
this.setOption("value", normalized);
|
|
300
|
+
} else {
|
|
301
|
+
validateAndSetValue.call(this);
|
|
300
302
|
}
|
|
301
303
|
return;
|
|
302
304
|
}
|
|
@@ -350,6 +352,7 @@ function toggleOn() {
|
|
|
350
352
|
return;
|
|
351
353
|
}
|
|
352
354
|
|
|
355
|
+
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
|
|
353
356
|
this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
|
|
354
357
|
this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color
|
|
355
358
|
|
|
@@ -371,6 +374,7 @@ function toggleOff() {
|
|
|
371
374
|
return;
|
|
372
375
|
}
|
|
373
376
|
|
|
377
|
+
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
|
|
374
378
|
this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
|
|
375
379
|
this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
|
|
376
380
|
|
|
@@ -397,6 +401,17 @@ function showError() {
|
|
|
397
401
|
this[switchElementSymbol].classList.add(this.getOption("classes.error"));
|
|
398
402
|
}
|
|
399
403
|
|
|
404
|
+
/**
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
function clearError() {
|
|
408
|
+
if (this[switchElementSymbol]) {
|
|
409
|
+
this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
413
|
+
}
|
|
414
|
+
|
|
400
415
|
/**
|
|
401
416
|
* @private
|
|
402
417
|
*/
|
|
@@ -407,6 +422,21 @@ function validateAndSetValue() {
|
|
|
407
422
|
this.setOption("value", value);
|
|
408
423
|
}
|
|
409
424
|
|
|
425
|
+
const offValue = this.getOption("values.off");
|
|
426
|
+
if (value === undefined || value === null || value === "") {
|
|
427
|
+
clearError.call(this);
|
|
428
|
+
if (this[invalidDisabledSymbol]) {
|
|
429
|
+
this.setOption("disabled", false);
|
|
430
|
+
this.formDisabledCallback(false);
|
|
431
|
+
this[invalidDisabledSymbol] = false;
|
|
432
|
+
}
|
|
433
|
+
if (this.getOption("value") !== offValue) {
|
|
434
|
+
this.setOption("value", offValue);
|
|
435
|
+
}
|
|
436
|
+
toggleOff.call(this);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
410
440
|
const validatedValues = [];
|
|
411
441
|
validatedValues.push(this.getOption("values.on"));
|
|
412
442
|
validatedValues.push(this.getOption("values.off"));
|
|
@@ -437,6 +467,7 @@ function validateAndSetValue() {
|
|
|
437
467
|
this.formDisabledCallback(false);
|
|
438
468
|
this[invalidDisabledSymbol] = false;
|
|
439
469
|
}
|
|
470
|
+
clearError.call(this);
|
|
440
471
|
|
|
441
472
|
if (value === this.getOption("values.on")) {
|
|
442
473
|
toggleOn.call(this);
|
package/source/dom/constants.mjs
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
ATTRIBUTE_THEME_PREFIX,
|
|
21
21
|
ATTRIBUTE_THEME_NAME,
|
|
22
22
|
ATTRIBUTE_UPDATER_ATTRIBUTES,
|
|
23
|
+
ATTRIBUTE_UPDATER_PROPERTIES,
|
|
23
24
|
ATTRIBUTE_UPDATER_SELECT_THIS,
|
|
24
25
|
ATTRIBUTE_UPDATER_REPLACE,
|
|
25
26
|
ATTRIBUTE_UPDATER_INSERT,
|
|
@@ -151,6 +152,13 @@ const ATTRIBUTE_THEME_NAME = `${ATTRIBUTE_THEME_PREFIX}name`;
|
|
|
151
152
|
*/
|
|
152
153
|
const ATTRIBUTE_UPDATER_ATTRIBUTES = `${ATTRIBUTE_PREFIX}attributes`;
|
|
153
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @type {string}
|
|
157
|
+
* @license AGPLv3
|
|
158
|
+
* @since 4.125.0
|
|
159
|
+
*/
|
|
160
|
+
const ATTRIBUTE_UPDATER_PROPERTIES = `${ATTRIBUTE_PREFIX}properties`;
|
|
161
|
+
|
|
154
162
|
/**
|
|
155
163
|
* @type {string}
|
|
156
164
|
* @license AGPLv3
|
package/source/dom/updater.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ATTRIBUTE_UPDATER_BIND_TYPE,
|
|
24
24
|
ATTRIBUTE_UPDATER_INSERT,
|
|
25
25
|
ATTRIBUTE_UPDATER_INSERT_REFERENCE,
|
|
26
|
+
ATTRIBUTE_UPDATER_PROPERTIES,
|
|
26
27
|
ATTRIBUTE_UPDATER_REMOVE,
|
|
27
28
|
ATTRIBUTE_UPDATER_REPLACE,
|
|
28
29
|
ATTRIBUTE_UPDATER_SELECT_THIS,
|
|
@@ -190,6 +191,7 @@ class Updater extends Base {
|
|
|
190
191
|
for (const path of updatePaths.values()) {
|
|
191
192
|
updateContent.call(this, { path });
|
|
192
193
|
updateAttributes.call(this, { path });
|
|
194
|
+
updateProperties.call(this, { path });
|
|
193
195
|
}
|
|
194
196
|
} else {
|
|
195
197
|
for (const change of Object.values(diffResult)) {
|
|
@@ -211,6 +213,7 @@ class Updater extends Base {
|
|
|
211
213
|
updateContent.call(this, change);
|
|
212
214
|
await Promise.resolve();
|
|
213
215
|
updateAttributes.call(this, change);
|
|
216
|
+
updateProperties.call(this, change);
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
/**
|
|
@@ -855,6 +858,14 @@ function applyRecursive(node, key, path) {
|
|
|
855
858
|
);
|
|
856
859
|
}
|
|
857
860
|
|
|
861
|
+
if (node.hasAttribute(ATTRIBUTE_UPDATER_PROPERTIES)) {
|
|
862
|
+
const value = node.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
|
|
863
|
+
node.setAttribute(
|
|
864
|
+
ATTRIBUTE_UPDATER_PROPERTIES,
|
|
865
|
+
value.replaceAll(`path:${key}`, `path:${path}`),
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
858
869
|
if (node.hasAttribute(ATTRIBUTE_UPDATER_BIND)) {
|
|
859
870
|
const value = node.getAttribute(ATTRIBUTE_UPDATER_BIND);
|
|
860
871
|
node.setAttribute(
|
|
@@ -984,6 +995,18 @@ function updateAttributes(change) {
|
|
|
984
995
|
runUpdateAttributes.call(this, this[internalSymbol].element, p, subject);
|
|
985
996
|
}
|
|
986
997
|
|
|
998
|
+
/**
|
|
999
|
+
* @private
|
|
1000
|
+
* @since 4.125.0
|
|
1001
|
+
* @param {object} change
|
|
1002
|
+
* @return {void}
|
|
1003
|
+
*/
|
|
1004
|
+
function updateProperties(change) {
|
|
1005
|
+
const subject = this[internalSymbol].subject.getRealSubject();
|
|
1006
|
+
const p = clone(change?.["path"]);
|
|
1007
|
+
runUpdateProperties.call(this, this[internalSymbol].element, p, subject);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
987
1010
|
/**
|
|
988
1011
|
* @private
|
|
989
1012
|
* @param {HTMLElement} container
|
|
@@ -1059,6 +1082,75 @@ function runUpdateAttributes(container, parts, subject) {
|
|
|
1059
1082
|
}
|
|
1060
1083
|
}
|
|
1061
1084
|
|
|
1085
|
+
/**
|
|
1086
|
+
* @private
|
|
1087
|
+
* @param {HTMLElement} container
|
|
1088
|
+
* @param {array} parts
|
|
1089
|
+
* @param {object} subject
|
|
1090
|
+
* @return {void}
|
|
1091
|
+
* @this Updater
|
|
1092
|
+
*/
|
|
1093
|
+
function runUpdateProperties(container, parts, subject) {
|
|
1094
|
+
if (!isArray(parts)) return;
|
|
1095
|
+
parts = clone(parts);
|
|
1096
|
+
|
|
1097
|
+
const mem = new WeakSet();
|
|
1098
|
+
|
|
1099
|
+
while (parts.length > 0) {
|
|
1100
|
+
const current = parts.join(".");
|
|
1101
|
+
parts.pop();
|
|
1102
|
+
|
|
1103
|
+
let iterator = new Set();
|
|
1104
|
+
|
|
1105
|
+
const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_PROPERTIES}], [${ATTRIBUTE_UPDATER_PROPERTIES}*="path:${current}"], [${ATTRIBUTE_UPDATER_PROPERTIES}^="static:"], [${ATTRIBUTE_UPDATER_PROPERTIES}^="i18n:"]`;
|
|
1106
|
+
|
|
1107
|
+
const e = container.querySelectorAll(query);
|
|
1108
|
+
|
|
1109
|
+
if (e.length > 0) {
|
|
1110
|
+
iterator = new Set([...e]);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (container.matches(query)) {
|
|
1114
|
+
iterator.add(container);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
for (const [element] of iterator.entries()) {
|
|
1118
|
+
if (mem.has(element)) return;
|
|
1119
|
+
mem.add(element);
|
|
1120
|
+
|
|
1121
|
+
// this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
|
|
1122
|
+
if (!element.hasAttribute(ATTRIBUTE_UPDATER_PROPERTIES)) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const properties = element.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
|
|
1127
|
+
|
|
1128
|
+
for (let [, def] of Object.entries(properties.split(","))) {
|
|
1129
|
+
def = trimSpaces(def);
|
|
1130
|
+
const i = def.indexOf(" ");
|
|
1131
|
+
const name = trimSpaces(def.substr(0, i));
|
|
1132
|
+
const cmd = trimSpaces(def.substr(i));
|
|
1133
|
+
|
|
1134
|
+
const pipe = new Pipe(cmd);
|
|
1135
|
+
|
|
1136
|
+
this[internalSymbol].callbacks.forEach((f, n) => {
|
|
1137
|
+
pipe.setCallback(n, f, element);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
let value;
|
|
1141
|
+
try {
|
|
1142
|
+
element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
|
|
1143
|
+
value = pipe.run(subject);
|
|
1144
|
+
} catch (e) {
|
|
1145
|
+
addErrorAttribute(element, e);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
handleInputControlPropertyUpdate.call(this, element, name, value);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1062
1154
|
/**
|
|
1063
1155
|
* @private
|
|
1064
1156
|
* @param {HTMLElement|*} element
|
|
@@ -1133,6 +1225,189 @@ function handleInputControlAttributeUpdate(element, name, value) {
|
|
|
1133
1225
|
}
|
|
1134
1226
|
}
|
|
1135
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* @private
|
|
1230
|
+
* @param {HTMLElement|*} element
|
|
1231
|
+
* @param {string} name
|
|
1232
|
+
* @param {*} value
|
|
1233
|
+
* @return {void}
|
|
1234
|
+
* @this Updater
|
|
1235
|
+
*/
|
|
1236
|
+
function handleInputControlPropertyUpdate(element, name, value) {
|
|
1237
|
+
if (element instanceof HTMLSelectElement && name === "value") {
|
|
1238
|
+
switch (element.type) {
|
|
1239
|
+
case "select-multiple":
|
|
1240
|
+
if (isArray(value)) {
|
|
1241
|
+
for (const [, opt] of Object.entries(element.options)) {
|
|
1242
|
+
opt.selected = value.indexOf(opt.value) !== -1;
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
break;
|
|
1247
|
+
case "select-one":
|
|
1248
|
+
if (value === undefined) {
|
|
1249
|
+
element.selectedIndex = -1;
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (element instanceof HTMLInputElement) {
|
|
1257
|
+
switch (name) {
|
|
1258
|
+
case "checked":
|
|
1259
|
+
element.checked =
|
|
1260
|
+
value === true || value === "true" || value === "1" || value === "on";
|
|
1261
|
+
return;
|
|
1262
|
+
case "value":
|
|
1263
|
+
if (element.type === "color") {
|
|
1264
|
+
if (value === undefined || value === "") {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (element instanceof HTMLTextAreaElement && name === "value") {
|
|
1273
|
+
element.value = value === undefined ? "" : value;
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const optionPath = getControlOptionPathFromPropertyName(name);
|
|
1278
|
+
if (
|
|
1279
|
+
optionPath !== null &&
|
|
1280
|
+
typeof element?.setOption === "function" &&
|
|
1281
|
+
typeof element?.getOption === "function"
|
|
1282
|
+
) {
|
|
1283
|
+
try {
|
|
1284
|
+
const current = element.getOption(optionPath);
|
|
1285
|
+
const normalized = normalizeValueForOptionTarget(value, current);
|
|
1286
|
+
element.setOption(optionPath, normalized);
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
addErrorAttribute(element, e);
|
|
1289
|
+
}
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (
|
|
1294
|
+
optionPath !== null &&
|
|
1295
|
+
typeof element?.setOption === "function" &&
|
|
1296
|
+
typeof element?.getOption !== "function"
|
|
1297
|
+
) {
|
|
1298
|
+
try {
|
|
1299
|
+
element.setOption(optionPath, value);
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
addErrorAttribute(element, e);
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
element[name] = value;
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
addErrorAttribute(element, e);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* @private
|
|
1315
|
+
* @param {string} name
|
|
1316
|
+
* @return {string|null}
|
|
1317
|
+
*/
|
|
1318
|
+
function getControlOptionPathFromPropertyName(name) {
|
|
1319
|
+
if (!isString(name)) {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let path = null;
|
|
1324
|
+
|
|
1325
|
+
if (name.startsWith("option:")) {
|
|
1326
|
+
path = name.substring(7);
|
|
1327
|
+
} else if (name.startsWith("option.")) {
|
|
1328
|
+
path = name.substring(7);
|
|
1329
|
+
} else if (name.startsWith("options.")) {
|
|
1330
|
+
path = name.substring(8);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (!isString(path)) {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
path = path
|
|
1338
|
+
.split(".")
|
|
1339
|
+
.map((part) => trimSpaces(part))
|
|
1340
|
+
.filter((part) => part !== "")
|
|
1341
|
+
.join(".");
|
|
1342
|
+
|
|
1343
|
+
if (path === "") {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
return path;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* @private
|
|
1352
|
+
* @param {*} value
|
|
1353
|
+
* @param {*} current
|
|
1354
|
+
* @return {*}
|
|
1355
|
+
*/
|
|
1356
|
+
function normalizeValueForOptionTarget(value, current) {
|
|
1357
|
+
if (current === undefined || current === null) {
|
|
1358
|
+
return value;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (typeof current === "boolean") {
|
|
1362
|
+
return (
|
|
1363
|
+
value === true || value === "true" || value === "1" || value === "on"
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (typeof current === "number") {
|
|
1368
|
+
const numberValue = Number(value);
|
|
1369
|
+
return isNaN(numberValue) ? current : numberValue;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (typeof current === "string") {
|
|
1373
|
+
return value === undefined || value === null ? "" : `${value}`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (isArray(current)) {
|
|
1377
|
+
if (isArray(value)) {
|
|
1378
|
+
return value;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (isString(value)) {
|
|
1382
|
+
if (value.trim() === "") {
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
return value.split("::");
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (value === undefined || value === null) {
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
return [value];
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (typeof current === "object") {
|
|
1395
|
+
if (value !== null && typeof value === "object") {
|
|
1396
|
+
return value;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (isString(value)) {
|
|
1400
|
+
try {
|
|
1401
|
+
return JSON.parse(value);
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
return current;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return value;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1136
1411
|
/**
|
|
1137
1412
|
* @param {NodeList|HTMLElement|Set<HTMLElement>} elements
|
|
1138
1413
|
* @param {Symbol} symbol
|
|
@@ -397,6 +397,21 @@ describe('ToggleSwitch', function () {
|
|
|
397
397
|
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
it('does not set error state for empty initial values', function (done) {
|
|
401
|
+
|
|
402
|
+
const toggleSwitch = document.createElement('monster-toggle-switch');
|
|
403
|
+
toggleSwitch.setOption('value', undefined);
|
|
404
|
+
document.getElementById('mocks').appendChild(toggleSwitch);
|
|
405
|
+
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
const switchEl = toggleSwitch.shadowRoot.querySelector('[data-monster-role="switch"]');
|
|
408
|
+
expect(toggleSwitch.getAttribute('data-monster-error')).is.equal(null);
|
|
409
|
+
expect(switchEl.classList.contains(toggleSwitch.getOption('classes.error'))).is.false;
|
|
410
|
+
expect(toggleSwitch.value).is.equal(toggleSwitch.getOption('values.off'));
|
|
411
|
+
done();
|
|
412
|
+
}, 0);
|
|
413
|
+
});
|
|
414
|
+
|
|
400
415
|
});
|
|
401
416
|
|
|
402
417
|
|
|
@@ -48,6 +48,11 @@ let html1 = `
|
|
|
48
48
|
|
|
49
49
|
<textarea name="textarea" id="textarea" data-monster-attributes="value path:a.textarea"></textarea>
|
|
50
50
|
|
|
51
|
+
<input id="property-checkbox" type="checkbox" data-monster-properties="checked path:a.checkboxBool">
|
|
52
|
+
<input id="property-input" type="text" data-monster-properties="value path:a.checkboxBool">
|
|
53
|
+
<monster-test-property id="property-custom" data-monster-properties="active path:a.checkboxBool, payload path:a.payload"></monster-test-property>
|
|
54
|
+
<monster-test-option-control id="property-option-control" data-monster-properties="option:features.enabled path:a.checkboxBool, option:labels.count path:a.optionCount"></monster-test-option-control>
|
|
55
|
+
|
|
51
56
|
</div>
|
|
52
57
|
</div>
|
|
53
58
|
|
|
@@ -64,7 +69,7 @@ let html2 = `
|
|
|
64
69
|
let html3 = `
|
|
65
70
|
|
|
66
71
|
<template id="myinnerid">
|
|
67
|
-
<span data-monster-replace="path:myinnerid | tojson"></span>
|
|
72
|
+
<span data-monster-replace="path:myinnerid | tojson" data-monster-properties="title path:myinnerid.i"></span>
|
|
68
73
|
</template>
|
|
69
74
|
|
|
70
75
|
<template id="myid">
|
|
@@ -127,6 +132,67 @@ describe("DOM", function () {
|
|
|
127
132
|
import("../../../source/dom/updater.mjs")
|
|
128
133
|
.then((m) => {
|
|
129
134
|
Updater = m.Updater;
|
|
135
|
+
|
|
136
|
+
if (!customElements.get("monster-test-property")) {
|
|
137
|
+
class MonsterTestProperty extends HTMLElement {
|
|
138
|
+
set active(value) {
|
|
139
|
+
this._active = value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get active() {
|
|
143
|
+
return this._active;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set payload(value) {
|
|
147
|
+
this._payload = value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get payload() {
|
|
151
|
+
return this._payload;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
customElements.define("monster-test-property", MonsterTestProperty);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!customElements.get("monster-test-option-control")) {
|
|
159
|
+
class MonsterTestOptionControl extends HTMLElement {
|
|
160
|
+
constructor() {
|
|
161
|
+
super();
|
|
162
|
+
this._options = {
|
|
163
|
+
features: {
|
|
164
|
+
enabled: false,
|
|
165
|
+
},
|
|
166
|
+
labels: {
|
|
167
|
+
count: 0,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getOption(path) {
|
|
173
|
+
return path.split(".").reduce((ref, part) => ref?.[part], this._options);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setOption(path, value) {
|
|
177
|
+
const parts = path.split(".");
|
|
178
|
+
let ref = this._options;
|
|
179
|
+
while (parts.length > 1) {
|
|
180
|
+
const part = parts.shift();
|
|
181
|
+
if (ref[part] === undefined || ref[part] === null) {
|
|
182
|
+
ref[part] = {};
|
|
183
|
+
}
|
|
184
|
+
ref = ref[part];
|
|
185
|
+
}
|
|
186
|
+
ref[parts[0]] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
customElements.define(
|
|
191
|
+
"monster-test-option-control",
|
|
192
|
+
MonsterTestOptionControl,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
130
196
|
done();
|
|
131
197
|
})
|
|
132
198
|
.catch((e) => {
|
|
@@ -714,8 +780,12 @@ describe("DOM", function () {
|
|
|
714
780
|
'<p data-monster-insert="myinnerid path:a.b" data-monster-insert-reference="myid-0">',
|
|
715
781
|
);
|
|
716
782
|
expect(element).contain.html(
|
|
717
|
-
'
|
|
783
|
+
'data-monster-replace="path:a.b.0 | tojson"',
|
|
784
|
+
);
|
|
785
|
+
expect(element).contain.html(
|
|
786
|
+
'data-monster-properties="title path:a.b.0.i"',
|
|
718
787
|
);
|
|
788
|
+
expect(element).contain.html('title="0"');
|
|
719
789
|
|
|
720
790
|
done();
|
|
721
791
|
}, 100);
|
|
@@ -750,6 +820,22 @@ describe("DOM", function () {
|
|
|
750
820
|
let textarea = document.getElementById("textarea");
|
|
751
821
|
expect(textarea.value).to.be.equal("");
|
|
752
822
|
|
|
823
|
+
let propertyCheckbox = document.getElementById("property-checkbox");
|
|
824
|
+
expect(propertyCheckbox.checked).to.be.false;
|
|
825
|
+
|
|
826
|
+
let propertyInput = document.getElementById("property-input");
|
|
827
|
+
expect(propertyInput.value).to.be.equal("");
|
|
828
|
+
|
|
829
|
+
let propertyCustom = document.getElementById("property-custom");
|
|
830
|
+
expect(propertyCustom.active).to.be.equal(undefined);
|
|
831
|
+
expect(propertyCustom.payload).to.be.equal(undefined);
|
|
832
|
+
|
|
833
|
+
let propertyOptionControl = document.getElementById(
|
|
834
|
+
"property-option-control",
|
|
835
|
+
);
|
|
836
|
+
expect(propertyOptionControl.getOption("features.enabled")).to.be.false;
|
|
837
|
+
expect(propertyOptionControl.getOption("labels.count")).to.be.equal(0);
|
|
838
|
+
|
|
753
839
|
let d = new Updater(element, {
|
|
754
840
|
a: {
|
|
755
841
|
b: "div-class",
|
|
@@ -760,6 +846,11 @@ describe("DOM", function () {
|
|
|
760
846
|
multiselect: ["value3", "value4", "other-value5"],
|
|
761
847
|
select: "value2",
|
|
762
848
|
checkbox: "true",
|
|
849
|
+
checkboxBool: true,
|
|
850
|
+
optionCount: 1234,
|
|
851
|
+
payload: {
|
|
852
|
+
state: "ok",
|
|
853
|
+
},
|
|
763
854
|
},
|
|
764
855
|
});
|
|
765
856
|
|
|
@@ -800,6 +891,26 @@ describe("DOM", function () {
|
|
|
800
891
|
"multiselect control",
|
|
801
892
|
).to.be.equal(JSON.stringify(d.getSubject()["a"]["multiselect"]));
|
|
802
893
|
expect(checkbox.checked, "checkbox control").to.be.true;
|
|
894
|
+
expect(propertyCheckbox.checked, "property checkbox control").to.be
|
|
895
|
+
.true;
|
|
896
|
+
expect(propertyInput.value, "property input value").to.be.equal(
|
|
897
|
+
"true",
|
|
898
|
+
);
|
|
899
|
+
expect(propertyCustom.active, "typed property boolean").to.be.true;
|
|
900
|
+
expect(propertyCustom.payload, "typed property object").to.deep.equal(
|
|
901
|
+
{
|
|
902
|
+
state: "ok",
|
|
903
|
+
},
|
|
904
|
+
);
|
|
905
|
+
expect(propertyCustom.getAttribute("active")).to.be.equal(null);
|
|
906
|
+
expect(
|
|
907
|
+
propertyOptionControl.getOption("features.enabled"),
|
|
908
|
+
"control option boolean",
|
|
909
|
+
).to.be.true;
|
|
910
|
+
expect(
|
|
911
|
+
propertyOptionControl.getOption("labels.count"),
|
|
912
|
+
"control option number",
|
|
913
|
+
).to.be.equal(1234);
|
|
803
914
|
|
|
804
915
|
done();
|
|
805
916
|
}, 100);
|