@schukai/monster 4.80.0 → 4.82.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
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.82.0] - 2026-01-07
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Update Select component to handle null values gracefully
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [4.81.0] - 2026-01-07
|
|
14
|
+
|
|
15
|
+
### Add Features
|
|
16
|
+
|
|
17
|
+
- Add new State Machine implementation and Control Flow documentation
|
|
18
|
+
### Changes
|
|
19
|
+
|
|
20
|
+
- update doc; close isse [#365](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/365)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
## [4.80.0] - 2026-01-07
|
|
6
25
|
|
|
7
26
|
### Add Features
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@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.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@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.82.0"}
|
|
@@ -408,7 +408,7 @@ class Select extends CustomControl {
|
|
|
408
408
|
* e.value=1
|
|
409
409
|
* ```
|
|
410
410
|
*
|
|
411
|
-
* @property {string|array} value
|
|
411
|
+
* @property {string|array|null} value
|
|
412
412
|
* @throws {Error} unsupported type
|
|
413
413
|
* @fires monster-selected this event is fired when the selection is set
|
|
414
414
|
*/
|
|
@@ -3140,6 +3140,8 @@ function checkOptionState() {
|
|
|
3140
3140
|
function convertValueToSelection(value) {
|
|
3141
3141
|
const selection = [];
|
|
3142
3142
|
|
|
3143
|
+
value = isValueIsEmptyThenGetNormalize.call(this, value);
|
|
3144
|
+
|
|
3143
3145
|
if (isString(value)) {
|
|
3144
3146
|
value = value
|
|
3145
3147
|
.split(",")
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
ATTRIBUTE_ROLE,
|
|
31
31
|
} from "../../dom/constants.mjs";
|
|
32
32
|
import { getWindow } from "../../dom/util.mjs";
|
|
33
|
-
import { fireEvent } from "../../dom/events.mjs";
|
|
33
|
+
import { fireCustomEvent, fireEvent } from "../../dom/events.mjs";
|
|
34
34
|
import { addErrorAttribute } from "../../dom/error.mjs";
|
|
35
35
|
|
|
36
36
|
export { ToggleSwitch };
|
|
@@ -224,6 +224,8 @@ class ToggleSwitch extends CustomControl {
|
|
|
224
224
|
toggleOn() {
|
|
225
225
|
this.setOption("value", this.getOption("values.on"));
|
|
226
226
|
fireEvent(this, "change");
|
|
227
|
+
fireCustomEvent(this, "monster-change", { value: this.value });
|
|
228
|
+
fireCustomEvent(this, "monster-changed", { value: this.value });
|
|
227
229
|
return this;
|
|
228
230
|
}
|
|
229
231
|
|
|
@@ -240,6 +242,8 @@ class ToggleSwitch extends CustomControl {
|
|
|
240
242
|
toggleOff() {
|
|
241
243
|
this.setOption("value", this.getOption("values.off"));
|
|
242
244
|
fireEvent(this, "change");
|
|
245
|
+
fireCustomEvent(this, "monster-change", { value: this.value });
|
|
246
|
+
fireCustomEvent(this, "monster-changed", { value: this.value });
|
|
243
247
|
return this;
|
|
244
248
|
}
|
|
245
249
|
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
7
|
+
*
|
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
|
10
|
+
* For more information about purchasing a commercial license, please contact Volker Schukai.
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Pathfinder } from "../data/pathfinder.mjs";
|
|
16
|
+
import { ProxyObserver } from "../types/proxyobserver.mjs";
|
|
17
|
+
import { Observer } from "../types/observer.mjs";
|
|
18
|
+
import { isArray, isFunction, isObject, isString } from "../types/is.mjs";
|
|
19
|
+
import { getDocument } from "./util.mjs";
|
|
20
|
+
|
|
21
|
+
export { ControlFlow };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lightweight control flow for event-driven control dependencies.
|
|
25
|
+
* Config is defined in JS and supports callbacks per event.
|
|
26
|
+
*/
|
|
27
|
+
class ControlFlow {
|
|
28
|
+
constructor({
|
|
29
|
+
stateObserver,
|
|
30
|
+
state = {},
|
|
31
|
+
controls = {},
|
|
32
|
+
datasources = {},
|
|
33
|
+
rules = [],
|
|
34
|
+
} = {}) {
|
|
35
|
+
this[internalSymbol] = {
|
|
36
|
+
stateObserver:
|
|
37
|
+
stateObserver instanceof ProxyObserver
|
|
38
|
+
? stateObserver
|
|
39
|
+
: new ProxyObserver(state),
|
|
40
|
+
controls: resolveControls(controls),
|
|
41
|
+
datasources: resolveDatasources(datasources),
|
|
42
|
+
rules: isArray(rules) ? rules : [],
|
|
43
|
+
listeners: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.state = this[internalSymbol].stateObserver.getSubject();
|
|
47
|
+
|
|
48
|
+
this[internalSymbol].stateObserver.attachObserver(
|
|
49
|
+
new Observer(() => {
|
|
50
|
+
this.emit({
|
|
51
|
+
source: "state",
|
|
52
|
+
id: "root",
|
|
53
|
+
event: "changed",
|
|
54
|
+
value: this.state,
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
bindControlEvents.call(this);
|
|
60
|
+
bindDatasourceEvents.call(this);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create an API helper for rule handlers.
|
|
65
|
+
* @return {object}
|
|
66
|
+
*/
|
|
67
|
+
createApi() {
|
|
68
|
+
return createApi(this);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add a rule at runtime.
|
|
73
|
+
* @param {object} rule
|
|
74
|
+
*/
|
|
75
|
+
addRule(rule) {
|
|
76
|
+
if (isObject(rule)) {
|
|
77
|
+
this[internalSymbol].rules.push(rule);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove all listeners.
|
|
83
|
+
*/
|
|
84
|
+
destroy() {
|
|
85
|
+
for (const entry of this[internalSymbol].listeners) {
|
|
86
|
+
entry.element.removeEventListener(entry.type, entry.handler);
|
|
87
|
+
}
|
|
88
|
+
this[internalSymbol].listeners = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Emit a synthetic event into the rule system.
|
|
93
|
+
* @param {object} ctx
|
|
94
|
+
*/
|
|
95
|
+
emit(ctx) {
|
|
96
|
+
const context = normalizeContext(ctx, this);
|
|
97
|
+
for (const rule of this[internalSymbol].rules) {
|
|
98
|
+
if (!matchRule(rule, context)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const run = rule?.run;
|
|
102
|
+
if (isFunction(run)) {
|
|
103
|
+
run(context, createApi(this));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Rule helper: control events.
|
|
110
|
+
* @param {string} id
|
|
111
|
+
* @param {string|string[]} events
|
|
112
|
+
* @param {function} run
|
|
113
|
+
* @return {object}
|
|
114
|
+
*/
|
|
115
|
+
static onControl(id, events, run) {
|
|
116
|
+
return {
|
|
117
|
+
on: (ctx) =>
|
|
118
|
+
ctx.source === "control" &&
|
|
119
|
+
ctx.id === id &&
|
|
120
|
+
matchEvent(ctx.event, events),
|
|
121
|
+
run,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Rule helper: datasource events.
|
|
127
|
+
* @param {string} id
|
|
128
|
+
* @param {string|string[]} events
|
|
129
|
+
* @param {function} run
|
|
130
|
+
* @return {object}
|
|
131
|
+
*/
|
|
132
|
+
static onDatasource(id, events, run) {
|
|
133
|
+
return {
|
|
134
|
+
on: (ctx) =>
|
|
135
|
+
ctx.source === "datasource" &&
|
|
136
|
+
ctx.id === id &&
|
|
137
|
+
matchEvent(ctx.event, events),
|
|
138
|
+
run,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Rule helper: state events.
|
|
144
|
+
* @param {string|string[]} events
|
|
145
|
+
* @param {function} run
|
|
146
|
+
* @return {object}
|
|
147
|
+
*/
|
|
148
|
+
static onState(events, run) {
|
|
149
|
+
return {
|
|
150
|
+
on: (ctx) =>
|
|
151
|
+
ctx.source === "state" && matchEvent(ctx.event, events),
|
|
152
|
+
run,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Rule helper: custom app events.
|
|
158
|
+
* @param {string|string[]} events
|
|
159
|
+
* @param {function} run
|
|
160
|
+
* @return {object}
|
|
161
|
+
*/
|
|
162
|
+
static onApp(events, run) {
|
|
163
|
+
return {
|
|
164
|
+
on: (ctx) => ctx.source === "app" && matchEvent(ctx.event, events),
|
|
165
|
+
run,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
const internalSymbol = Symbol("controlFlowInternal");
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
function resolveControls(controls) {
|
|
179
|
+
const resolved = {};
|
|
180
|
+
for (const [id, def] of Object.entries(controls || {})) {
|
|
181
|
+
const element = resolveElement(def?.element || def);
|
|
182
|
+
if (!element) continue;
|
|
183
|
+
resolved[id] = {
|
|
184
|
+
element,
|
|
185
|
+
events: def?.events || ["monster-changed", "change"],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
function resolveDatasources(datasources) {
|
|
195
|
+
const resolved = {};
|
|
196
|
+
for (const [id, def] of Object.entries(datasources || {})) {
|
|
197
|
+
const element = resolveElement(def?.element || def);
|
|
198
|
+
if (!element) continue;
|
|
199
|
+
resolved[id] = {
|
|
200
|
+
element,
|
|
201
|
+
events: def?.events || [
|
|
202
|
+
"monster-datasource-fetched",
|
|
203
|
+
"monster-datasource-error",
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return resolved;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
function resolveElement(elementOrSelector) {
|
|
214
|
+
if (elementOrSelector instanceof HTMLElement) {
|
|
215
|
+
return elementOrSelector;
|
|
216
|
+
}
|
|
217
|
+
if (isString(elementOrSelector)) {
|
|
218
|
+
return getDocument().querySelector(elementOrSelector);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
function bindControlEvents() {
|
|
227
|
+
for (const [id, control] of Object.entries(this[internalSymbol].controls)) {
|
|
228
|
+
for (const type of control.events) {
|
|
229
|
+
const handler = (event) => {
|
|
230
|
+
this.emit({
|
|
231
|
+
source: "control",
|
|
232
|
+
id,
|
|
233
|
+
event: type,
|
|
234
|
+
value: readControlValue(control.element),
|
|
235
|
+
rawEvent: event,
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
control.element.addEventListener(type, handler);
|
|
239
|
+
this[internalSymbol].listeners.push({
|
|
240
|
+
element: control.element,
|
|
241
|
+
type,
|
|
242
|
+
handler,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
function bindDatasourceEvents() {
|
|
252
|
+
for (const [id, datasource] of Object.entries(
|
|
253
|
+
this[internalSymbol].datasources,
|
|
254
|
+
)) {
|
|
255
|
+
for (const type of datasource.events) {
|
|
256
|
+
const handler = (event) => {
|
|
257
|
+
this.emit({
|
|
258
|
+
source: "datasource",
|
|
259
|
+
id,
|
|
260
|
+
event: type,
|
|
261
|
+
value: datasource.element?.data,
|
|
262
|
+
rawEvent: event,
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
datasource.element.addEventListener(type, handler);
|
|
266
|
+
this[internalSymbol].listeners.push({
|
|
267
|
+
element: datasource.element,
|
|
268
|
+
type,
|
|
269
|
+
handler,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
function readControlValue(element) {
|
|
279
|
+
if (element && "value" in element) {
|
|
280
|
+
return element.value;
|
|
281
|
+
}
|
|
282
|
+
if (isFunction(element?.getOption)) {
|
|
283
|
+
return element.getOption("value");
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @private
|
|
290
|
+
*/
|
|
291
|
+
function matchRule(rule, ctx) {
|
|
292
|
+
if (!rule) return false;
|
|
293
|
+
if (isFunction(rule.on)) {
|
|
294
|
+
return !!rule.on(ctx);
|
|
295
|
+
}
|
|
296
|
+
if (isString(rule.on)) {
|
|
297
|
+
return rule.on === ctx.key || rule.on === ctx.topic;
|
|
298
|
+
}
|
|
299
|
+
if (isArray(rule.on)) {
|
|
300
|
+
return rule.on.includes(ctx.key) || rule.on.includes(ctx.topic);
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @private
|
|
307
|
+
*/
|
|
308
|
+
function normalizeContext(ctx, flow) {
|
|
309
|
+
const context = Object.assign({}, ctx || {});
|
|
310
|
+
context.key = `${context.source}:${context.id}:${context.event}`;
|
|
311
|
+
context.topic = `${context.source}:${context.id}`;
|
|
312
|
+
context.state = flow.state;
|
|
313
|
+
context.controls = flow[internalSymbol].controls;
|
|
314
|
+
context.datasources = flow[internalSymbol].datasources;
|
|
315
|
+
return context;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
function matchEvent(event, events) {
|
|
322
|
+
if (!events) return true;
|
|
323
|
+
if (isString(events)) {
|
|
324
|
+
return event === events;
|
|
325
|
+
}
|
|
326
|
+
if (isArray(events)) {
|
|
327
|
+
return events.includes(event);
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
function createApi(flow) {
|
|
336
|
+
return {
|
|
337
|
+
state: flow.state,
|
|
338
|
+
getState(path, fallback) {
|
|
339
|
+
try {
|
|
340
|
+
return new Pathfinder(flow.state).getVia(path);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return fallback;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
setState(path, value) {
|
|
346
|
+
new Pathfinder(flow.state).setVia(path, value);
|
|
347
|
+
},
|
|
348
|
+
getControl(id) {
|
|
349
|
+
return flow[internalSymbol].controls?.[id]?.element;
|
|
350
|
+
},
|
|
351
|
+
setControlOption(id, path, value) {
|
|
352
|
+
const control = flow[internalSymbol].controls?.[id]?.element;
|
|
353
|
+
if (isFunction(control?.setOption)) {
|
|
354
|
+
control.setOption(path, value);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
setControlOptions(id, options) {
|
|
358
|
+
const control = flow[internalSymbol].controls?.[id]?.element;
|
|
359
|
+
if (isFunction(control?.setOptions)) {
|
|
360
|
+
control.setOptions(options);
|
|
361
|
+
} else if (isFunction(control?.setOption)) {
|
|
362
|
+
control.setOption("options", options);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
setControlValue(id, value) {
|
|
366
|
+
const control = flow[internalSymbol].controls?.[id]?.element;
|
|
367
|
+
if (control && "value" in control) {
|
|
368
|
+
control.value = value;
|
|
369
|
+
} else if (isFunction(control?.setOption)) {
|
|
370
|
+
control.setOption("value", value);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
clearControl(id) {
|
|
374
|
+
const control = flow[internalSymbol].controls?.[id]?.element;
|
|
375
|
+
if (control && "value" in control) {
|
|
376
|
+
control.value = "";
|
|
377
|
+
} else if (isFunction(control?.setOption)) {
|
|
378
|
+
control.setOption("value", "");
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
disableControl(id, disabled) {
|
|
382
|
+
const control = flow[internalSymbol].controls?.[id]?.element;
|
|
383
|
+
if (isFunction(control?.setOption)) {
|
|
384
|
+
control.setOption("disabled", !!disabled);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
fetchDatasource(id, { url, transform } = {}) {
|
|
388
|
+
const datasource = flow[internalSymbol].datasources?.[id]?.element;
|
|
389
|
+
if (!datasource) {
|
|
390
|
+
return Promise.reject(new Error("datasource not found"));
|
|
391
|
+
}
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
const onFetched = () => resolve(datasource.data);
|
|
394
|
+
const onError = (event) => {
|
|
395
|
+
reject(event?.detail?.error || new Error("Datasource error"));
|
|
396
|
+
};
|
|
397
|
+
datasource.addEventListener("monster-datasource-fetched", onFetched, {
|
|
398
|
+
once: true,
|
|
399
|
+
});
|
|
400
|
+
datasource.addEventListener("monster-datasource-error", onError, {
|
|
401
|
+
once: true,
|
|
402
|
+
});
|
|
403
|
+
if (isFunction(transform)) {
|
|
404
|
+
datasource.setOption("read.responseCallback", (payload) => {
|
|
405
|
+
datasource.data = transform(payload);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (url) {
|
|
409
|
+
datasource.setOption("read.url", url);
|
|
410
|
+
}
|
|
411
|
+
datasource.read();
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
package/source/monster.mjs
CHANGED
|
@@ -161,6 +161,7 @@ export * from "./constraints/andoperator.mjs";
|
|
|
161
161
|
export * from "./constraints/isarray.mjs";
|
|
162
162
|
export * from "./constraints/abstract.mjs";
|
|
163
163
|
export * from "./constraints/valid.mjs";
|
|
164
|
+
export * from "./dom/controlflow.mjs";
|
|
164
165
|
export * from "./dom/dimension.mjs";
|
|
165
166
|
export * from "./dom/error.mjs";
|
|
166
167
|
export * from "./dom/resource/link/stylesheet.mjs";
|
|
@@ -281,6 +281,34 @@ describe('Select', function () {
|
|
|
281
281
|
}, 350);
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
it('should treat null value as empty selection', function (done) {
|
|
285
|
+
this.timeout(2000);
|
|
286
|
+
|
|
287
|
+
let mocks = document.getElementById('mocks');
|
|
288
|
+
const select = document.createElement('monster-select');
|
|
289
|
+
select.setOption('options', [{label: 'One', value: '1'}]);
|
|
290
|
+
mocks.appendChild(select);
|
|
291
|
+
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
select.value = null;
|
|
294
|
+
|
|
295
|
+
setTimeout(() => {
|
|
296
|
+
try {
|
|
297
|
+
const selection = select.getOption('selection');
|
|
298
|
+
const error = select.getAttribute('data-monster-error') ?? '';
|
|
299
|
+
expect(Array.isArray(selection)).to.equal(true);
|
|
300
|
+
expect(selection.length).to.equal(0);
|
|
301
|
+
expect(select.value).to.equal('');
|
|
302
|
+
expect(error).to.not.contain('unsupported type');
|
|
303
|
+
} catch (e) {
|
|
304
|
+
return done(e);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
done();
|
|
308
|
+
}, 50);
|
|
309
|
+
}, 50);
|
|
310
|
+
});
|
|
311
|
+
|
|
284
312
|
});
|
|
285
313
|
|
|
286
314
|
|