@schukai/monster 4.63.0 → 4.65.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.
@@ -0,0 +1,1483 @@
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 { instanceSymbol } from "../../constants.mjs";
16
+ import { addAttributeToken } from "../../dom/attributes.mjs";
17
+ import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
18
+ import { CustomControl } from "../../dom/customcontrol.mjs";
19
+ import {
20
+ assembleMethodSymbol,
21
+ registerCustomElement,
22
+ } from "../../dom/customelement.mjs";
23
+ import { fireCustomEvent } from "../../dom/events.mjs";
24
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
25
+ import { Formatter } from "../../text/formatter.mjs";
26
+ import { isFunction, isObject, isString } from "../../types/is.mjs";
27
+ import { validateArray } from "../../types/validate.mjs";
28
+ import { Pathfinder } from "../../data/pathfinder.mjs";
29
+ import { CommonStyleSheet } from "../stylesheet/common.mjs";
30
+ import { ButtonStyleSheet } from "../stylesheet/button.mjs";
31
+ import { FormStyleSheet } from "../stylesheet/form.mjs";
32
+ import { VariantSelectStyleSheet } from "./stylesheet/variant-select.mjs";
33
+ import { Select } from "./select.mjs";
34
+
35
+ export { VariantSelect };
36
+
37
+ /**
38
+ * @private
39
+ * @type {symbol}
40
+ */
41
+ const controlElementSymbol = Symbol("controlElement");
42
+
43
+ /**
44
+ * @private
45
+ * @type {symbol}
46
+ */
47
+ const dimensionsElementSymbol = Symbol("dimensionsElement");
48
+
49
+ /**
50
+ * @private
51
+ * @type {symbol}
52
+ */
53
+ const messageElementSymbol = Symbol("messageElement");
54
+
55
+ /**
56
+ * @private
57
+ * @type {symbol}
58
+ */
59
+ const variantsSymbol = Symbol("variants");
60
+
61
+ /**
62
+ * @private
63
+ * @type {symbol}
64
+ */
65
+ const dimensionValuesSymbol = Symbol("dimensionValues");
66
+
67
+ /**
68
+ * @private
69
+ * @type {symbol}
70
+ */
71
+ const layoutSymbol = Symbol("layout");
72
+
73
+ /**
74
+ * @private
75
+ * @type {symbol}
76
+ */
77
+ const selectControlSymbol = Symbol("selectControl");
78
+
79
+ /**
80
+ * @private
81
+ * @type {symbol}
82
+ */
83
+ const combinedSelectSymbol = Symbol("combinedSelect");
84
+
85
+ /**
86
+ * @private
87
+ * @type {symbol}
88
+ */
89
+ const resizeObserverSymbol = Symbol("resizeObserver");
90
+
91
+ /**
92
+ * @private
93
+ * @type {symbol}
94
+ */
95
+ const lastValiditySymbol = Symbol("lastValidity");
96
+
97
+ /**
98
+ * @private
99
+ * @type {symbol}
100
+ */
101
+ const initGuardSymbol = Symbol("initGuard");
102
+
103
+ /**
104
+ * VariantSelect
105
+ *
106
+ * @summary A control to pick valid variant combinations (e.g., color/size).
107
+ * @fires monster-variant-select-change
108
+ * @fires monster-variant-select-valid
109
+ * @fires monster-variant-select-invalid
110
+ * @fires monster-variant-select-lazy-load
111
+ * @fires monster-variant-select-lazy-loaded
112
+ * @fires monster-variant-select-lazy-error
113
+ */
114
+ class VariantSelect extends CustomControl {
115
+ /**
116
+ * This method is called by the `instanceof` operator.
117
+ * @return {symbol}
118
+ */
119
+ static get [instanceSymbol]() {
120
+ return Symbol.for(
121
+ "@schukai/monster/components/form/variant-select@@instance",
122
+ );
123
+ }
124
+
125
+ /**
126
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
127
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
128
+ *
129
+ * @property {Array<Object>} dimensions Dimension definitions ({key, label, presentation, valueTemplate, labelTemplate})
130
+ * @property {Array<Object>|Object|null} dimensions.values Optional label map for a dimension
131
+ * @property {Array<Object>} data Variant items
132
+ * @property {string|null} url Endpoint to fetch variants
133
+ * @property {Object} fetch Fetch options (method, headers, body)
134
+ * @property {Object} mapping Data mapping for fetched results
135
+ * @property {string} mapping.selector="*" Path to array in fetched data
136
+ * @property {string} mapping.valueTemplate Template for variant value
137
+ * @property {string} mapping.labelTemplate Template for combined variant label
138
+ * @property {Function|null} mapping.filter Filter function
139
+ * @property {Object} layout Layout options
140
+ * @property {boolean} layout.autoSelect=true Use select when buttons do not fit
141
+ * @property {number} layout.buttonMinWidth=84 Minimum width used to estimate button layout
142
+ * @property {Object} features Feature toggles
143
+ * @property {boolean} features.messages=true Show status message
144
+ * @property {Object} messages Message text
145
+ * @property {string} messages.valid Message for valid selection
146
+ * @property {string} messages.invalid Message for invalid selection
147
+ * @property {string} messages.incomplete Message for incomplete selection
148
+ * @property {Object} actions Callback actions
149
+ * @property {Function} actions.onchange called on any selection change
150
+ * @property {Function} actions.onvalid called when a valid variant is selected
151
+ * @property {Function} actions.oninvalid called when selection becomes invalid
152
+ * @property {Function} actions.onlazyload called before fetching variants
153
+ * @property {Function} actions.onlazyloaded called after fetching variants
154
+ * @property {Function} actions.onlazyerror called on fetch errors
155
+ * @property {Object} classes CSS classes
156
+ * @property {string} classes.option Base class for option buttons
157
+ * @property {string} classes.optionSelected Class for selected options
158
+ * @property {string} classes.optionDisabled Class for disabled options
159
+ */
160
+ get defaults() {
161
+ return Object.assign({}, super.defaults, {
162
+ templates: {
163
+ main: getTemplate(),
164
+ },
165
+ dimensions: [],
166
+ data: [],
167
+ url: null,
168
+ fetch: {
169
+ method: "GET",
170
+ headers: {
171
+ accept: "application/json",
172
+ },
173
+ body: null,
174
+ },
175
+ mapping: {
176
+ selector: "*",
177
+ valueTemplate: "",
178
+ labelTemplate: "",
179
+ filter: null,
180
+ },
181
+ layout: {
182
+ autoSelect: false,
183
+ buttonMinWidth: 84,
184
+ combineSelect: true,
185
+ },
186
+ features: {
187
+ messages: true,
188
+ rowSelects: false,
189
+ combinedSelect: false,
190
+ },
191
+ messages: getTranslations(),
192
+ selection: {},
193
+ value: null,
194
+ classes: {
195
+ option: "monster-button-outline-primary",
196
+ optionSelected: "is-selected",
197
+ optionDisabled: "is-disabled",
198
+ },
199
+ actions: {
200
+ onchange: null,
201
+ onvalid: null,
202
+ oninvalid: null,
203
+ onlazyload: null,
204
+ onlazyloaded: null,
205
+ onlazyerror: null,
206
+ },
207
+ });
208
+ }
209
+
210
+ /**
211
+ * @return {VariantSelect}
212
+ */
213
+ [assembleMethodSymbol]() {
214
+ super[assembleMethodSymbol]();
215
+ initControlReferences.call(this);
216
+ initEventHandler.call(this);
217
+ initResizeObserver.call(this);
218
+ refreshData.call(this);
219
+ return this;
220
+ }
221
+
222
+ /**
223
+ * @return {CSSStyleSheet[]}
224
+ */
225
+ static getCSSStyleSheet() {
226
+ return [
227
+ CommonStyleSheet,
228
+ ButtonStyleSheet,
229
+ FormStyleSheet,
230
+ VariantSelectStyleSheet,
231
+ ];
232
+ }
233
+
234
+ /**
235
+ * @return {string}
236
+ */
237
+ static getTag() {
238
+ return "monster-variant-select";
239
+ }
240
+
241
+ /**
242
+ * @return {boolean}
243
+ */
244
+ static get formAssociated() {
245
+ return true;
246
+ }
247
+
248
+ /**
249
+ * @return {string|null}
250
+ */
251
+ get value() {
252
+ return this.getOption("value");
253
+ }
254
+
255
+ /**
256
+ * @param {string|null} value
257
+ */
258
+ set value(value) {
259
+ this.setOption("value", value);
260
+ if (typeof this.setFormValue === "function") {
261
+ this.setFormValue(value ?? "");
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Reload the data from `data` or `url`.
267
+ */
268
+ refresh() {
269
+ refreshData.call(this);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * @private
275
+ */
276
+ function initControlReferences() {
277
+ this[controlElementSymbol] = this.shadowRoot.querySelector(
278
+ "[data-monster-role=control]",
279
+ );
280
+ this[dimensionsElementSymbol] = this.shadowRoot.querySelector(
281
+ "[data-monster-role=dimensions]",
282
+ );
283
+ this[messageElementSymbol] = this.shadowRoot.querySelector(
284
+ "[data-monster-role=message]",
285
+ );
286
+ this[variantsSymbol] = [];
287
+ this[dimensionValuesSymbol] = new Map();
288
+ this[layoutSymbol] = new Map();
289
+ this[selectControlSymbol] = new Map();
290
+ this[combinedSelectSymbol] = null;
291
+ }
292
+
293
+ /**
294
+ * @private
295
+ */
296
+ function initEventHandler() {
297
+ this.shadowRoot.addEventListener("click", (event) => {
298
+ const button = event.target?.closest("button[data-variant-dimension]");
299
+ if (!(button instanceof HTMLButtonElement)) {
300
+ return;
301
+ }
302
+ const key = button.getAttribute("data-variant-dimension");
303
+ const value = button.getAttribute("data-variant-value");
304
+ if (!key) {
305
+ return;
306
+ }
307
+ handleSelectionChange.call(this, key, value);
308
+ });
309
+ }
310
+
311
+ /**
312
+ * @private
313
+ */
314
+ function initResizeObserver() {
315
+ if (typeof ResizeObserver !== "function") {
316
+ return;
317
+ }
318
+ if (!(this[controlElementSymbol] instanceof HTMLElement)) {
319
+ return;
320
+ }
321
+
322
+ this[resizeObserverSymbol] = new ResizeObserver(() => {
323
+ render.call(this);
324
+ });
325
+ this[resizeObserverSymbol].observe(this[controlElementSymbol]);
326
+ }
327
+
328
+ /**
329
+ * @private
330
+ */
331
+ function refreshData() {
332
+ const url = this.getOption("url");
333
+ if (isString(url) && url !== "") {
334
+ void fetchData.call(this);
335
+ return;
336
+ }
337
+ buildFromData.call(this);
338
+ }
339
+
340
+ /**
341
+ * @private
342
+ */
343
+ async function fetchData() {
344
+ const url = this.getOption("url");
345
+ if (!isString(url) || url === "") {
346
+ return;
347
+ }
348
+
349
+ const action = this.getOption("actions.onlazyload");
350
+ if (isFunction(action)) {
351
+ action.call(this);
352
+ }
353
+ fireCustomEvent(this, "monster-variant-select-lazy-load", {});
354
+
355
+ let response = null;
356
+ try {
357
+ response = await fetch(url, buildFetchOptions.call(this));
358
+ if (!response.ok) {
359
+ throw new Error("failed to fetch variants");
360
+ }
361
+ } catch (e) {
362
+ handleFetchError.call(this, e);
363
+ return;
364
+ }
365
+
366
+ let json = null;
367
+ try {
368
+ json = await response.json();
369
+ } catch (e) {
370
+ handleFetchError.call(this, e);
371
+ return;
372
+ }
373
+
374
+ const selector = this.getOption("mapping.selector", "*");
375
+ let data = json;
376
+ try {
377
+ if (selector !== "*" && selector !== "") {
378
+ data = new Pathfinder(json).getVia(selector);
379
+ }
380
+ } catch (e) {
381
+ handleFetchError.call(this, e);
382
+ return;
383
+ }
384
+
385
+ if (!Array.isArray(data)) {
386
+ handleFetchError.call(this, new Error("variant data is not an array"));
387
+ return;
388
+ }
389
+
390
+ this.setOption("data", data);
391
+ buildFromData.call(this);
392
+
393
+ const doneAction = this.getOption("actions.onlazyloaded");
394
+ if (isFunction(doneAction)) {
395
+ doneAction.call(this, data);
396
+ }
397
+ fireCustomEvent(this, "monster-variant-select-lazy-loaded", { data });
398
+ }
399
+
400
+ /**
401
+ * @private
402
+ * @param {Error} error
403
+ */
404
+ function handleFetchError(error) {
405
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${error}`);
406
+ const action = this.getOption("actions.onlazyerror");
407
+ if (isFunction(action)) {
408
+ action.call(this, error);
409
+ }
410
+ fireCustomEvent(this, "monster-variant-select-lazy-error", { error });
411
+ }
412
+
413
+ /**
414
+ * @private
415
+ * @return {Object}
416
+ */
417
+ function buildFetchOptions() {
418
+ const fetchOptions = Object.assign({}, this.getOption("fetch", {}));
419
+ if (!fetchOptions.method) {
420
+ fetchOptions.method = "GET";
421
+ }
422
+ return fetchOptions;
423
+ }
424
+
425
+ /**
426
+ * @private
427
+ */
428
+ function buildFromData() {
429
+ this[initGuardSymbol] = true;
430
+
431
+ const dimensions = this.getOption("dimensions", []);
432
+ try {
433
+ validateArray(dimensions);
434
+ } catch (e) {
435
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
436
+ return;
437
+ }
438
+
439
+ const data = this.getOption("data", []);
440
+ const mapping = this.getOption("mapping", {});
441
+ const filter = mapping?.filter;
442
+
443
+ let items = Array.isArray(data) ? data.slice() : [];
444
+ if (isFunction(filter)) {
445
+ items = items.filter((item) => filter(item));
446
+ }
447
+
448
+ const variants = [];
449
+ const valueTemplate = mapping?.valueTemplate;
450
+ for (let i = 0; i < items.length; i += 1) {
451
+ const item = items[i];
452
+ const dims = {};
453
+ let hasMissing = false;
454
+
455
+ for (const def of dimensions) {
456
+ const key = def?.key;
457
+ if (!isString(key) || key === "") {
458
+ hasMissing = true;
459
+ break;
460
+ }
461
+ const value = getDimensionValue(item, def);
462
+ if (value === null || value === undefined || value === "") {
463
+ hasMissing = true;
464
+ break;
465
+ }
466
+ dims[key] = String(value);
467
+ }
468
+
469
+ if (hasMissing) {
470
+ continue;
471
+ }
472
+
473
+ let value = null;
474
+ if (isString(valueTemplate) && valueTemplate !== "") {
475
+ value = new Formatter(item).format(valueTemplate);
476
+ } else if (item?.value !== undefined) {
477
+ value = item.value;
478
+ } else if (item?.id !== undefined) {
479
+ value = item.id;
480
+ } else {
481
+ value = `${i}`;
482
+ }
483
+
484
+ variants.push({
485
+ value: String(value),
486
+ data: item,
487
+ dimensions: dims,
488
+ });
489
+ }
490
+
491
+ this[variantsSymbol] = variants;
492
+ buildDimensionValues.call(this, dimensions, variants);
493
+ applyInitialSelection.call(this, dimensions, variants);
494
+ render.call(this);
495
+ this[initGuardSymbol] = false;
496
+ }
497
+
498
+ /**
499
+ * @private
500
+ * @param {Array} dimensions
501
+ * @param {Array} variants
502
+ */
503
+ function buildDimensionValues(dimensions, variants) {
504
+ this[dimensionValuesSymbol].clear();
505
+ for (const def of dimensions) {
506
+ const key = def?.key;
507
+ if (!isString(key) || key === "") {
508
+ continue;
509
+ }
510
+ const labelMap = new Map();
511
+ appendValuesFromDefinition(def, labelMap);
512
+ for (const variant of variants) {
513
+ const value = variant.dimensions[key];
514
+ if (!labelMap.has(String(value))) {
515
+ const label = getDimensionLabel(variant.data, def, value);
516
+ labelMap.set(value, label);
517
+ }
518
+ }
519
+ this[dimensionValuesSymbol].set(key, labelMap);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * @private
525
+ * @param {Array} dimensions
526
+ * @param {Array} variants
527
+ */
528
+ function applyInitialSelection(dimensions, variants) {
529
+ const selection = Object.assign({}, this.getOption("selection", {}));
530
+ const value = this.getOption("value");
531
+
532
+ if (isString(value) && value !== "") {
533
+ const match = variants.find((variant) => variant.value === value);
534
+ if (match) {
535
+ for (const def of dimensions) {
536
+ const key = def?.key;
537
+ if (key) {
538
+ selection[key] = match.dimensions[key];
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ this.setOption("selection", selection);
545
+ updateSelectedVariant.call(this);
546
+ }
547
+
548
+ /**
549
+ * @private
550
+ */
551
+ function render() {
552
+ if (!(this[dimensionsElementSymbol] instanceof HTMLElement)) {
553
+ return;
554
+ }
555
+
556
+ const dimensions = this.getOption("dimensions", []);
557
+ const containerWidth = getContainerWidth.call(this);
558
+ const valuesMap = this[dimensionValuesSymbol];
559
+ const selection = this.getOption("selection", {});
560
+
561
+ this[dimensionsElementSymbol].innerHTML = "";
562
+ this[selectControlSymbol].clear();
563
+ this[layoutSymbol].clear();
564
+ this[combinedSelectSymbol] = null;
565
+
566
+ const useCombinedSelect = shouldUseCombinedSelect.call(
567
+ this,
568
+ dimensions,
569
+ valuesMap,
570
+ containerWidth,
571
+ );
572
+
573
+ if (useCombinedSelect) {
574
+ renderCombinedSelect.call(this, dimensions, valuesMap, selection);
575
+ updateUI.call(this);
576
+ updateMessage.call(this);
577
+ return;
578
+ }
579
+
580
+ for (const def of dimensions) {
581
+ const key = def?.key;
582
+ if (!isString(key) || key === "") {
583
+ continue;
584
+ }
585
+ const label = def?.label || key;
586
+ const valueMap = valuesMap.get(key) || new Map();
587
+
588
+ const row = document.createElement("div");
589
+ row.setAttribute("data-monster-role", "dimension");
590
+ row.setAttribute("data-variant-dimension", key);
591
+ row.setAttribute("part", "dimension");
592
+
593
+ const labelNode = document.createElement("div");
594
+ labelNode.setAttribute("data-monster-role", "dimension-label");
595
+ labelNode.textContent = label;
596
+ labelNode.setAttribute("part", "dimension-label");
597
+
598
+ const controls = document.createElement("div");
599
+ controls.setAttribute("data-monster-role", "dimension-controls");
600
+ controls.setAttribute("part", "dimension-controls");
601
+
602
+ const mode = resolvePresentationMode.call(
603
+ this,
604
+ def,
605
+ valueMap.size,
606
+ containerWidth,
607
+ );
608
+ this[layoutSymbol].set(key, mode);
609
+
610
+ if (mode === "select") {
611
+ const select = document.createElement(Select.getTag());
612
+ select.setAttribute("data-monster-options", "{}");
613
+ select.setAttribute("part", "dimension-select");
614
+ select.addEventListener("monster-change", (event) => {
615
+ const value = event?.detail?.value ?? "";
616
+ if (value === "") {
617
+ handleSelectionChange.call(this, key, null);
618
+ } else {
619
+ handleSelectionChange.call(this, key, value);
620
+ }
621
+ });
622
+ controls.appendChild(select);
623
+ this[selectControlSymbol].set(key, select);
624
+ configureSelect.call(this, select, valueMap, selection?.[key] ?? "");
625
+ } else {
626
+ const options = document.createElement("div");
627
+ options.setAttribute("data-monster-role", "options");
628
+ options.setAttribute("part", "dimension-options");
629
+
630
+ for (const [value, optionLabel] of valueMap.entries()) {
631
+ const button = document.createElement("button");
632
+ button.type = "button";
633
+ button.className = this.getOption("classes.option");
634
+ button.textContent = optionLabel;
635
+ button.setAttribute("data-variant-dimension", key);
636
+ button.setAttribute("data-variant-value", value);
637
+ button.setAttribute("part", "dimension-option");
638
+ options.appendChild(button);
639
+ }
640
+ controls.appendChild(options);
641
+ }
642
+
643
+ row.appendChild(labelNode);
644
+ row.appendChild(controls);
645
+ this[dimensionsElementSymbol].appendChild(row);
646
+ }
647
+
648
+ updateUI.call(this);
649
+ updateMessage.call(this);
650
+ }
651
+
652
+ /**
653
+ * @private
654
+ * @param {Object} def
655
+ * @param {number} count
656
+ * @param {number} width
657
+ * @return {string}
658
+ */
659
+ function resolvePresentationMode(def, count, width) {
660
+ const presentation = def?.presentation;
661
+ if (presentation === "select" || presentation === "buttons") {
662
+ if (
663
+ presentation === "select" &&
664
+ this.getOption("features.rowSelects") !== true
665
+ ) {
666
+ return "buttons";
667
+ }
668
+ return presentation;
669
+ }
670
+
671
+ if (this.getOption("features.rowSelects") !== true) {
672
+ return "buttons";
673
+ }
674
+
675
+ const auto = this.getOption("layout.autoSelect") !== false;
676
+ if (!auto) {
677
+ return "buttons";
678
+ }
679
+
680
+ const minWidth = Number(this.getOption("layout.buttonMinWidth", 84));
681
+ if (!Number.isFinite(minWidth) || minWidth <= 0) {
682
+ return "buttons";
683
+ }
684
+ return count * minWidth > width ? "select" : "buttons";
685
+ }
686
+
687
+ /**
688
+ * @private
689
+ * @return {number}
690
+ */
691
+ function getContainerWidth() {
692
+ if (!(this[controlElementSymbol] instanceof HTMLElement)) {
693
+ return 320;
694
+ }
695
+ const width = this[controlElementSymbol].getBoundingClientRect().width;
696
+ if (!Number.isFinite(width) || width <= 0) {
697
+ return 320;
698
+ }
699
+ return width;
700
+ }
701
+
702
+ /**
703
+ * @private
704
+ * @param {Map} values
705
+ * @return {Array}
706
+ */
707
+ function buildSelectOptions(values) {
708
+ const options = [];
709
+ for (const [value, label] of values.entries()) {
710
+ options.push({ label, value });
711
+ }
712
+ return options;
713
+ }
714
+
715
+ /**
716
+ * @private
717
+ * @param {string} key
718
+ * @param {string|null} value
719
+ */
720
+ function handleSelectionChange(key, value) {
721
+ const selection = Object.assign({}, this.getOption("selection", {}));
722
+ const nextValue = value === null ? null : String(value);
723
+ if (selection[key] === nextValue) {
724
+ selection[key] = null;
725
+ } else {
726
+ selection[key] = nextValue;
727
+ }
728
+
729
+ normalizeSelection.call(this, selection, key);
730
+ this.setOption("selection", selection);
731
+ updateSelectedVariant.call(this);
732
+ updateUI.call(this);
733
+ emitSelectionEvents.call(this);
734
+ updateMessage.call(this);
735
+ }
736
+
737
+ /**
738
+ * @private
739
+ * @param {Object} selection
740
+ * @param {string|null} fixedKey
741
+ */
742
+ function normalizeSelection(selection, fixedKey) {
743
+ const dimensions = this.getOption("dimensions", []);
744
+ const keys = dimensions
745
+ .map((def) => def?.key)
746
+ .filter((key) => isString(key) && key !== "");
747
+
748
+ if (keys.length === 0) {
749
+ return;
750
+ }
751
+
752
+ const fixedKeys =
753
+ isString(fixedKey) && selection[fixedKey] != null ? [fixedKey] : [];
754
+ const candidateKeys = keys.filter(
755
+ (key) => !fixedKeys.includes(key) && selection[key] != null,
756
+ );
757
+
758
+ const best = findBestSelection.call(
759
+ this,
760
+ selection,
761
+ fixedKeys,
762
+ candidateKeys,
763
+ );
764
+
765
+ for (const key of candidateKeys) {
766
+ if (!best.includes(key)) {
767
+ selection[key] = null;
768
+ }
769
+ }
770
+ }
771
+
772
+ /**
773
+ * @private
774
+ * @param {Object} selection
775
+ * @param {Array} fixedKeys
776
+ * @param {Array} candidateKeys
777
+ * @return {Array}
778
+ */
779
+ function findBestSelection(selection, fixedKeys, candidateKeys) {
780
+ const total = 1 << candidateKeys.length;
781
+ let best = [];
782
+
783
+ for (let mask = 0; mask < total; mask += 1) {
784
+ const subset = [];
785
+ for (let i = 0; i < candidateKeys.length; i += 1) {
786
+ if (mask & (1 << i)) {
787
+ subset.push(candidateKeys[i]);
788
+ }
789
+ }
790
+ const testSelection = buildSelectionForKeys(selection, fixedKeys, subset);
791
+ if (hasVariantMatch.call(this, testSelection)) {
792
+ if (subset.length > best.length) {
793
+ best = subset;
794
+ }
795
+ }
796
+ }
797
+
798
+ return best;
799
+ }
800
+
801
+ /**
802
+ * @private
803
+ * @param {Object} selection
804
+ * @param {Array} fixedKeys
805
+ * @param {Array} subsetKeys
806
+ * @return {Object}
807
+ */
808
+ function buildSelectionForKeys(selection, fixedKeys, subsetKeys) {
809
+ const result = {};
810
+ for (const key of fixedKeys) {
811
+ result[key] = selection[key];
812
+ }
813
+ for (const key of subsetKeys) {
814
+ result[key] = selection[key];
815
+ }
816
+ return result;
817
+ }
818
+
819
+ /**
820
+ * @private
821
+ * @param {Object} selection
822
+ * @return {boolean}
823
+ */
824
+ function hasVariantMatch(selection) {
825
+ const variants = this[variantsSymbol] || [];
826
+ for (const variant of variants) {
827
+ if (matchesSelection(variant, selection)) {
828
+ return true;
829
+ }
830
+ }
831
+ return false;
832
+ }
833
+
834
+ /**
835
+ * @private
836
+ */
837
+ function updateSelectedVariant() {
838
+ const selection = this.getOption("selection", {});
839
+ const dimensions = this.getOption("dimensions", []);
840
+ const keys = dimensions
841
+ .map((def) => def?.key)
842
+ .filter((key) => isString(key) && key !== "");
843
+
844
+ let complete = true;
845
+ for (const key of keys) {
846
+ if (!isString(selection?.[key]) || selection[key] === "") {
847
+ complete = false;
848
+ break;
849
+ }
850
+ }
851
+
852
+ if (!complete) {
853
+ this.value = null;
854
+ return;
855
+ }
856
+
857
+ const variants = this[variantsSymbol] || [];
858
+ const match = variants.find((variant) =>
859
+ matchesSelection(variant, selection),
860
+ );
861
+ this.value = match ? match.value : null;
862
+ }
863
+
864
+ /**
865
+ * @private
866
+ */
867
+ function updateUI() {
868
+ const selection = this.getOption("selection", {});
869
+ const dimensions = this.getOption("dimensions", []);
870
+ const valuesMap = this[dimensionValuesSymbol];
871
+ const combined = this[combinedSelectSymbol];
872
+
873
+ if (combined instanceof Select && combined.shadowRoot) {
874
+ updateCombinedSelect.call(this);
875
+ return;
876
+ }
877
+
878
+ for (const def of dimensions) {
879
+ const key = def?.key;
880
+ if (!isString(key) || key === "") {
881
+ continue;
882
+ }
883
+ const available = getAvailableValues.call(this, key, selection);
884
+ const mode = this[layoutSymbol].get(key);
885
+
886
+ if (mode === "select") {
887
+ const select = this[selectControlSymbol].get(key);
888
+ if (select instanceof Select && select.shadowRoot) {
889
+ const allValues = valuesMap.get(key) || new Map();
890
+ const options = [];
891
+ for (const [value, label] of allValues.entries()) {
892
+ if (available.has(value)) {
893
+ options.push({ label, value });
894
+ }
895
+ }
896
+ select.setOption("options", options);
897
+ select.value = selection?.[key] ?? "";
898
+ }
899
+ continue;
900
+ }
901
+
902
+ const buttons = this.shadowRoot.querySelectorAll(
903
+ `button[data-variant-dimension=\"${key}\"]`,
904
+ );
905
+ for (const button of buttons) {
906
+ const value = button.getAttribute("data-variant-value");
907
+ const isSelected = selection?.[key] === value;
908
+ const isAvailable = available.has(value);
909
+
910
+ button.classList.toggle(
911
+ this.getOption("classes.optionSelected"),
912
+ isSelected,
913
+ );
914
+ button.classList.toggle(
915
+ this.getOption("classes.optionDisabled"),
916
+ !isAvailable,
917
+ );
918
+ button.setAttribute("aria-pressed", isSelected ? "true" : "false");
919
+ button.setAttribute("aria-disabled", isAvailable ? "false" : "true");
920
+ button.setAttribute(
921
+ "data-variant-disabled",
922
+ isAvailable ? "false" : "true",
923
+ );
924
+ }
925
+ }
926
+ }
927
+
928
+ /**
929
+ * @private
930
+ * @param {Select} select
931
+ * @param {Map} valueMap
932
+ * @param {string} selectedValue
933
+ */
934
+ function configureSelect(select, valueMap, selectedValue) {
935
+ const init = () => {
936
+ if (!select.shadowRoot) {
937
+ return false;
938
+ }
939
+ select.setOption("filter.mode", "disabled");
940
+ select.setOption("features.lazyLoad", false);
941
+ select.setOption("options", buildSelectOptions(valueMap));
942
+ select.value = selectedValue;
943
+ return true;
944
+ };
945
+
946
+ if (init()) {
947
+ return;
948
+ }
949
+
950
+ let attempts = 0;
951
+ const retry = () => {
952
+ attempts += 1;
953
+ if (init() || attempts > 10) {
954
+ return;
955
+ }
956
+ requestAnimationFrame(retry);
957
+ };
958
+
959
+ requestAnimationFrame(retry);
960
+ }
961
+
962
+ /**
963
+ * @private
964
+ * @param {Array} dimensions
965
+ * @param {Map} valuesMap
966
+ * @param {number} containerWidth
967
+ * @return {boolean}
968
+ */
969
+ function shouldUseCombinedSelect(dimensions, valuesMap, containerWidth) {
970
+ const combine =
971
+ this.getOption("layout.combineSelect") !== false &&
972
+ this.getOption("features.combinedSelect") === true;
973
+ if (!combine) {
974
+ return false;
975
+ }
976
+
977
+ for (const def of dimensions) {
978
+ const key = def?.key;
979
+ if (!isString(key) || key === "") {
980
+ continue;
981
+ }
982
+ const valueMap = valuesMap.get(key) || new Map();
983
+ const mode = resolvePresentationMode.call(
984
+ this,
985
+ def,
986
+ valueMap.size,
987
+ containerWidth,
988
+ );
989
+ if (mode !== "select") {
990
+ return false;
991
+ }
992
+ }
993
+
994
+ return true;
995
+ }
996
+
997
+ /**
998
+ * @private
999
+ * @param {Array} dimensions
1000
+ * @param {Map} valuesMap
1001
+ * @param {Object} selection
1002
+ */
1003
+ function renderCombinedSelect(dimensions, valuesMap, selection) {
1004
+ const select = document.createElement(Select.getTag());
1005
+ select.setAttribute("data-monster-options", "{}");
1006
+ select.setAttribute("part", "combined-select");
1007
+ select.setAttribute("data-monster-role", "combined-select");
1008
+ select.style.width = "100%";
1009
+
1010
+ select.addEventListener("monster-change", (event) => {
1011
+ const value = event?.detail?.value ?? "";
1012
+ if (value === "") {
1013
+ this.setOption("selection", {});
1014
+ this.value = null;
1015
+ updateMessage.call(this);
1016
+ emitSelectionEvents.call(this);
1017
+ return;
1018
+ }
1019
+ applyVariantSelection.call(this, value);
1020
+ });
1021
+
1022
+ this[dimensionsElementSymbol].appendChild(select);
1023
+ this[combinedSelectSymbol] = select;
1024
+
1025
+ configureCombinedSelect.call(this, select, selection);
1026
+ }
1027
+
1028
+ /**
1029
+ * @private
1030
+ * @param {Select} select
1031
+ * @param {Object} selection
1032
+ */
1033
+ function configureCombinedSelect(select, selection) {
1034
+ const init = () => {
1035
+ if (!select.shadowRoot) {
1036
+ return false;
1037
+ }
1038
+ select.setOption("filter.mode", "options");
1039
+ select.setOption("filter.position", "popper");
1040
+ select.setOption("features.lazyLoad", false);
1041
+ select.setOption("options", buildCombinedOptions.call(this));
1042
+ select.value = this.getOption("value") ?? "";
1043
+ return true;
1044
+ };
1045
+
1046
+ if (init()) {
1047
+ return;
1048
+ }
1049
+
1050
+ let attempts = 0;
1051
+ const retry = () => {
1052
+ attempts += 1;
1053
+ if (init() || attempts > 10) {
1054
+ return;
1055
+ }
1056
+ requestAnimationFrame(retry);
1057
+ };
1058
+ requestAnimationFrame(retry);
1059
+ }
1060
+
1061
+ /**
1062
+ * @private
1063
+ */
1064
+ function updateCombinedSelect() {
1065
+ const select = this[combinedSelectSymbol];
1066
+ if (!(select instanceof Select) || !select.shadowRoot) {
1067
+ return;
1068
+ }
1069
+ select.setOption("options", buildCombinedOptions.call(this));
1070
+ select.value = this.getOption("value") ?? "";
1071
+ }
1072
+
1073
+ /**
1074
+ * @private
1075
+ * @return {Array}
1076
+ */
1077
+ function buildCombinedOptions() {
1078
+ const variants = this[variantsSymbol] || [];
1079
+ const dimensions = this.getOption("dimensions", []);
1080
+ const valuesMap = this[dimensionValuesSymbol];
1081
+ const labelTemplate = this.getOption("mapping.labelTemplate", "");
1082
+ const options = [];
1083
+
1084
+ for (const variant of variants) {
1085
+ let label = "";
1086
+ if (isString(labelTemplate) && labelTemplate !== "") {
1087
+ label = new Formatter(variant.data).format(labelTemplate);
1088
+ } else {
1089
+ label = buildVariantLabel(variant, dimensions, valuesMap);
1090
+ }
1091
+ options.push({
1092
+ label,
1093
+ value: variant.value,
1094
+ });
1095
+ }
1096
+
1097
+ return options;
1098
+ }
1099
+
1100
+ /**
1101
+ * @private
1102
+ * @param {Object} variant
1103
+ * @param {Array} dimensions
1104
+ * @param {Map} valuesMap
1105
+ * @return {string}
1106
+ */
1107
+ function buildVariantLabel(variant, dimensions, valuesMap) {
1108
+ const parts = [];
1109
+ for (const def of dimensions) {
1110
+ const key = def?.key;
1111
+ if (!isString(key) || key === "") {
1112
+ continue;
1113
+ }
1114
+ const labelName = def?.label || key;
1115
+ const value = variant.dimensions?.[key];
1116
+ const labelMap = valuesMap.get(key) || new Map();
1117
+ const labelValue = labelMap.get(String(value)) ?? value;
1118
+ parts.push(`${labelName}: ${labelValue}`);
1119
+ }
1120
+ return parts.join(", ");
1121
+ }
1122
+
1123
+ /**
1124
+ * @private
1125
+ * @param {string} value
1126
+ */
1127
+ function applyVariantSelection(value) {
1128
+ const variants = this[variantsSymbol] || [];
1129
+ const match = variants.find((entry) => entry.value === value);
1130
+ if (!match) {
1131
+ this.setOption("selection", {});
1132
+ this.value = null;
1133
+ updateMessage.call(this);
1134
+ emitSelectionEvents.call(this);
1135
+ return;
1136
+ }
1137
+
1138
+ this.setOption("selection", Object.assign({}, match.dimensions));
1139
+ this.value = match.value;
1140
+ updateMessage.call(this);
1141
+ emitSelectionEvents.call(this);
1142
+ }
1143
+
1144
+ /**
1145
+ * @private
1146
+ */
1147
+ function emitSelectionEvents() {
1148
+ if (this[initGuardSymbol]) {
1149
+ return;
1150
+ }
1151
+
1152
+ const selection = this.getOption("selection", {});
1153
+ const value = this.getOption("value");
1154
+ const variants = this[variantsSymbol] || [];
1155
+ const variant = variants.find((entry) => entry.value === value) || null;
1156
+ const detail = { selection, value, variant };
1157
+
1158
+ const changeAction = this.getOption("actions.onchange");
1159
+ if (isFunction(changeAction)) {
1160
+ changeAction.call(this, detail);
1161
+ }
1162
+ fireCustomEvent(this, "monster-variant-select-change", detail);
1163
+
1164
+ const isValid = isString(value) && value !== "";
1165
+ if (this[lastValiditySymbol] === isValid) {
1166
+ return;
1167
+ }
1168
+ this[lastValiditySymbol] = isValid;
1169
+
1170
+ if (isValid) {
1171
+ const action = this.getOption("actions.onvalid");
1172
+ if (isFunction(action)) {
1173
+ action.call(this, detail);
1174
+ }
1175
+ fireCustomEvent(this, "monster-variant-select-valid", detail);
1176
+ return;
1177
+ }
1178
+
1179
+ const action = this.getOption("actions.oninvalid");
1180
+ if (isFunction(action)) {
1181
+ action.call(this, detail);
1182
+ }
1183
+ fireCustomEvent(this, "monster-variant-select-invalid", detail);
1184
+ }
1185
+
1186
+ /**
1187
+ * @private
1188
+ */
1189
+ function updateMessage() {
1190
+ if (this.getOption("features.messages") === false) {
1191
+ if (this[messageElementSymbol] instanceof HTMLElement) {
1192
+ this[messageElementSymbol].textContent = "";
1193
+ }
1194
+ return;
1195
+ }
1196
+
1197
+ if (!(this[messageElementSymbol] instanceof HTMLElement)) {
1198
+ return;
1199
+ }
1200
+
1201
+ const selection = this.getOption("selection", {});
1202
+ const value = this.getOption("value");
1203
+ const dimensions = this.getOption("dimensions", []);
1204
+ const keys = dimensions
1205
+ .map((def) => def?.key)
1206
+ .filter((key) => isString(key) && key !== "");
1207
+
1208
+ let complete = true;
1209
+ for (const key of keys) {
1210
+ if (!isString(selection?.[key]) || selection[key] === "") {
1211
+ complete = false;
1212
+ break;
1213
+ }
1214
+ }
1215
+
1216
+ let message = "";
1217
+ if (!complete) {
1218
+ message = this.getOption("messages.incomplete");
1219
+ } else if (!isString(value) || value === "") {
1220
+ message = this.getOption("messages.invalid");
1221
+ } else {
1222
+ message = this.getOption("messages.valid");
1223
+ }
1224
+
1225
+ this[messageElementSymbol].textContent = message || "";
1226
+ }
1227
+
1228
+ /**
1229
+ * @private
1230
+ * @param {string} key
1231
+ * @param {Object} selection
1232
+ * @return {Set}
1233
+ */
1234
+ function getAvailableValues(key, selection) {
1235
+ const variants = this[variantsSymbol] || [];
1236
+ const available = new Set();
1237
+
1238
+ for (const variant of variants) {
1239
+ if (matchesSelection(variant, selection, key)) {
1240
+ available.add(variant.dimensions[key]);
1241
+ }
1242
+ }
1243
+
1244
+ return available;
1245
+ }
1246
+
1247
+ /**
1248
+ * @private
1249
+ * @param {Object} variant
1250
+ * @param {Object} selection
1251
+ * @param {string|null} ignoreKey
1252
+ * @return {boolean}
1253
+ */
1254
+ function matchesSelection(variant, selection, ignoreKey = null) {
1255
+ for (const [key, value] of Object.entries(selection || {})) {
1256
+ if (ignoreKey && key === ignoreKey) {
1257
+ continue;
1258
+ }
1259
+ if (value === null || value === undefined || value === "") {
1260
+ continue;
1261
+ }
1262
+ if (variant.dimensions[key] !== value) {
1263
+ return false;
1264
+ }
1265
+ }
1266
+ return true;
1267
+ }
1268
+
1269
+ /**
1270
+ * @private
1271
+ * @param {Object} item
1272
+ * @param {Object} def
1273
+ * @return {string|null}
1274
+ */
1275
+ function getDimensionValue(item, def) {
1276
+ const template = def?.valueTemplate;
1277
+ if (isString(template) && template !== "") {
1278
+ return new Formatter(item).format(template);
1279
+ }
1280
+
1281
+ const key = def?.key;
1282
+ if (!isString(key) || key === "") {
1283
+ return null;
1284
+ }
1285
+
1286
+ try {
1287
+ return new Pathfinder(item).getVia(key);
1288
+ } catch (_e) {
1289
+ return item?.[key];
1290
+ }
1291
+ }
1292
+
1293
+ /**
1294
+ * @private
1295
+ * @param {Object} item
1296
+ * @param {Object} def
1297
+ * @param {string} fallback
1298
+ * @return {string}
1299
+ */
1300
+ function getDimensionLabel(item, def, fallback) {
1301
+ const template = def?.labelTemplate;
1302
+ if (isString(template) && template !== "") {
1303
+ return new Formatter(item).format(template);
1304
+ }
1305
+ return fallback;
1306
+ }
1307
+
1308
+ /**
1309
+ * @private
1310
+ * @param {Object} def
1311
+ * @param {Map} labelMap
1312
+ */
1313
+ function appendValuesFromDefinition(def, labelMap) {
1314
+ const values = def?.values;
1315
+ if (!values) {
1316
+ return;
1317
+ }
1318
+ if (Array.isArray(values)) {
1319
+ for (const entry of values) {
1320
+ const value = entry?.value;
1321
+ if (!isString(value) || value === "") {
1322
+ continue;
1323
+ }
1324
+ const label = entry?.label ?? value;
1325
+ labelMap.set(String(value), String(label));
1326
+ }
1327
+ return;
1328
+ }
1329
+ if (isObject(values)) {
1330
+ for (const [value, label] of Object.entries(values)) {
1331
+ if (!isString(value) || value === "") {
1332
+ continue;
1333
+ }
1334
+ labelMap.set(String(value), String(label ?? value));
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ /**
1340
+ * @private
1341
+ * @return {string}
1342
+ */
1343
+ function getTemplate() {
1344
+ // language=HTML
1345
+ return `
1346
+ <div data-monster-role="control" part="control">
1347
+ <div data-monster-role="dimensions" part="dimensions"></div>
1348
+ <div data-monster-role="message" part="message"></div>
1349
+ </div>
1350
+ `;
1351
+ }
1352
+
1353
+ registerCustomElement(VariantSelect);
1354
+
1355
+ /**
1356
+ * @private
1357
+ * @returns {object}
1358
+ */
1359
+ function getTranslations() {
1360
+ const locale = getLocaleOfDocument();
1361
+ switch (locale.language) {
1362
+ case "de":
1363
+ return {
1364
+ valid: "Auswahl ist gueltig.",
1365
+ invalid: "Diese Variantenkombination existiert nicht.",
1366
+ incomplete: "Bitte alle Varianten auswaehlen.",
1367
+ };
1368
+ case "es":
1369
+ return {
1370
+ valid: "La seleccion es valida.",
1371
+ invalid: "Esta combinacion no existe.",
1372
+ incomplete: "Seleccione todas las opciones.",
1373
+ };
1374
+ case "zh":
1375
+ return {
1376
+ valid: "选择有效。",
1377
+ invalid: "该组合不存在。",
1378
+ incomplete: "请完成所有选项。",
1379
+ };
1380
+ case "hi":
1381
+ return {
1382
+ valid: "चयन मान्य है।",
1383
+ invalid: "यह संयोजन उपलब्ध नहीं है।",
1384
+ incomplete: "कृपया सभी विकल्प चुनें।",
1385
+ };
1386
+ case "bn":
1387
+ return {
1388
+ valid: "নির্বাচন বৈধ।",
1389
+ invalid: "এই সংযোজনটি নেই।",
1390
+ incomplete: "অনুগ্রহ করে সব বিকল্প নির্বাচন করুন।",
1391
+ };
1392
+ case "pt":
1393
+ return {
1394
+ valid: "Selecao valida.",
1395
+ invalid: "Esta combinacao nao existe.",
1396
+ incomplete: "Selecione todas as opcoes.",
1397
+ };
1398
+ case "ru":
1399
+ return {
1400
+ valid: "Выбор корректен.",
1401
+ invalid: "Такой комбинации нет.",
1402
+ incomplete: "Выберите все опции.",
1403
+ };
1404
+ case "ja":
1405
+ return {
1406
+ valid: "選択は有効です。",
1407
+ invalid: "この組み合わせは存在しません。",
1408
+ incomplete: "すべての項目を選択してください。",
1409
+ };
1410
+ case "pa":
1411
+ return {
1412
+ valid: "ਚੋਣ ਠੀਕ ਹੈ।",
1413
+ invalid: "ਇਹ ਸੰਯੋਜਨ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।",
1414
+ incomplete: "ਕਿਰਪਾ ਕਰਕੇ ਸਾਰੇ ਵਿਕਲਪ ਚੁਣੋ।",
1415
+ };
1416
+ case "mr":
1417
+ return {
1418
+ valid: "निवड वैध आहे.",
1419
+ invalid: "हा संयोजन उपलब्ध नाही.",
1420
+ incomplete: "कृपया सर्व पर्याय निवडा.",
1421
+ };
1422
+ case "fr":
1423
+ return {
1424
+ valid: "Selection valide.",
1425
+ invalid: "Cette combinaison n'existe pas.",
1426
+ incomplete: "Veuillez choisir toutes les options.",
1427
+ };
1428
+ case "it":
1429
+ return {
1430
+ valid: "Selezione valida.",
1431
+ invalid: "Questa combinazione non esiste.",
1432
+ incomplete: "Seleziona tutte le opzioni.",
1433
+ };
1434
+ case "nl":
1435
+ return {
1436
+ valid: "Selectie is geldig.",
1437
+ invalid: "Deze combinatie bestaat niet.",
1438
+ incomplete: "Selecteer alle opties.",
1439
+ };
1440
+ case "sv":
1441
+ return {
1442
+ valid: "Valet ar giltigt.",
1443
+ invalid: "Denna kombination finns inte.",
1444
+ incomplete: "Valj alla alternativ.",
1445
+ };
1446
+ case "pl":
1447
+ return {
1448
+ valid: "Wybor jest poprawny.",
1449
+ invalid: "Ta kombinacja nie istnieje.",
1450
+ incomplete: "Wybierz wszystkie opcje.",
1451
+ };
1452
+ case "da":
1453
+ return {
1454
+ valid: "Valget er gyldigt.",
1455
+ invalid: "Denne kombination findes ikke.",
1456
+ incomplete: "Vaelg alle muligheder.",
1457
+ };
1458
+ case "fi":
1459
+ return {
1460
+ valid: "Valinta on kelvollinen.",
1461
+ invalid: "Tallaista yhdistelmaa ei ole.",
1462
+ incomplete: "Valitse kaikki vaihtoehdot.",
1463
+ };
1464
+ case "no":
1465
+ return {
1466
+ valid: "Valget er gyldig.",
1467
+ invalid: "Denne kombinasjonen finnes ikke.",
1468
+ incomplete: "Velg alle alternativer.",
1469
+ };
1470
+ case "cs":
1471
+ return {
1472
+ valid: "Vyber je platny.",
1473
+ invalid: "Tato kombinace neexistuje.",
1474
+ incomplete: "Vyberte vsechny moznosti.",
1475
+ };
1476
+ default:
1477
+ return {
1478
+ valid: "Selection is valid.",
1479
+ invalid: "This combination does not exist.",
1480
+ incomplete: "Please select all options.",
1481
+ };
1482
+ }
1483
+ }