@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.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/source/components/form/buy-box.mjs +2516 -0
- package/source/components/form/cart-control.mjs +710 -0
- package/source/components/form/style/buy-box.pcss +124 -0
- package/source/components/form/style/cart-control.pcss +35 -0
- package/source/components/form/style/variant-select.pcss +79 -0
- package/source/components/form/stylesheet/buy-box.mjs +38 -0
- package/source/components/form/stylesheet/cart-control.mjs +38 -0
- package/source/components/form/stylesheet/variant-select.mjs +38 -0
- package/source/components/form/variant-select.mjs +1483 -0
- package/source/components/host/call-button.mjs +4 -0
- package/source/components/host/config-manager.mjs +4 -0
- package/source/components/host/host.mjs +4 -0
- package/source/components/host/toggle-button.mjs +4 -0
- package/source/components/navigation/table-of-content.mjs +2 -2
- package/source/components/tree-menu/html-tree-menu.mjs +30 -28
- package/source/components/tree-menu/tree-menu.mjs +4 -0
- package/source/monster.mjs +1 -0
- package/test/cases/components/form/buy-box.mjs +181 -0
|
@@ -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
|
+
}
|