@schukai/monster 4.64.0 → 4.66.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,2516 @@
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 { getDocument } from "../../dom/util.mjs";
26
+ import { Pathfinder } from "../../data/pathfinder.mjs";
27
+ import { Formatter } from "../../text/formatter.mjs";
28
+ import { isFunction, isObject, isString } from "../../types/is.mjs";
29
+ import { CommonStyleSheet } from "../stylesheet/common.mjs";
30
+ import { FormStyleSheet } from "../stylesheet/form.mjs";
31
+ import { VariantSelectStyleSheet } from "./stylesheet/variant-select.mjs";
32
+ import { BuyBoxStyleSheet } from "./stylesheet/buy-box.mjs";
33
+ import "./variant-select.mjs";
34
+ import "./quantity.mjs";
35
+ import "./message-state-button.mjs";
36
+ import "../datatable/datasource/dom.mjs";
37
+ import "../datatable/datasource/rest.mjs";
38
+
39
+ export { BuyBox };
40
+
41
+ /**
42
+ * @private
43
+ * @type {symbol}
44
+ */
45
+ const controlElementSymbol = Symbol("controlElement");
46
+
47
+ /**
48
+ * @private
49
+ * @type {symbol}
50
+ */
51
+ const variantElementSymbol = Symbol("variantElement");
52
+
53
+ /**
54
+ * @private
55
+ * @type {symbol}
56
+ */
57
+ const quantityElementSymbol = Symbol("quantityElement");
58
+
59
+ /**
60
+ * @private
61
+ * @type {symbol}
62
+ */
63
+ const quantityInputElementSymbol = Symbol("quantityInputElement");
64
+
65
+ /**
66
+ * @private
67
+ * @type {symbol}
68
+ */
69
+ const quantityUnitElementSymbol = Symbol("quantityUnitElement");
70
+
71
+ /**
72
+ * @private
73
+ * @type {symbol}
74
+ */
75
+ const submitElementSymbol = Symbol("submitElement");
76
+
77
+ /**
78
+ * @private
79
+ * @type {symbol}
80
+ */
81
+ const submitLabelElementSymbol = Symbol("submitLabelElement");
82
+
83
+ /**
84
+ * @private
85
+ * @type {symbol}
86
+ */
87
+ const messageElementSymbol = Symbol("messageElement");
88
+
89
+ /**
90
+ * @private
91
+ * @type {symbol}
92
+ */
93
+ const priceElementSymbol = Symbol("priceElement");
94
+
95
+ /**
96
+ * @private
97
+ * @type {symbol}
98
+ */
99
+ const sumElementSymbol = Symbol("sumElement");
100
+
101
+ /**
102
+ * @private
103
+ * @type {symbol}
104
+ */
105
+ const listPriceElementSymbol = Symbol("listPriceElement");
106
+
107
+ /**
108
+ * @private
109
+ * @type {symbol}
110
+ */
111
+ const basePriceElementSymbol = Symbol("basePriceElement");
112
+
113
+ /**
114
+ * @private
115
+ * @type {symbol}
116
+ */
117
+ const taxElementSymbol = Symbol("taxElement");
118
+
119
+ /**
120
+ * @private
121
+ * @type {symbol}
122
+ */
123
+ const totalElementSymbol = Symbol("totalElement");
124
+
125
+ /**
126
+ * @private
127
+ * @type {symbol}
128
+ */
129
+ const deliveryElementSymbol = Symbol("deliveryElement");
130
+
131
+ /**
132
+ * @private
133
+ * @type {symbol}
134
+ */
135
+ const quantityStaticElementSymbol = Symbol("quantityStaticElement");
136
+
137
+ /**
138
+ * @private
139
+ * @type {symbol}
140
+ */
141
+ const datasourceElementSymbol = Symbol("datasourceElement");
142
+
143
+ /**
144
+ * @private
145
+ * @type {symbol}
146
+ */
147
+ const productDataSymbol = Symbol("productData");
148
+
149
+ /**
150
+ * @private
151
+ * @type {symbol}
152
+ */
153
+ const cartDataSymbol = Symbol("cartData");
154
+
155
+ /**
156
+ * @private
157
+ * @type {symbol}
158
+ */
159
+ const stockRequestSymbol = Symbol("stockRequest");
160
+
161
+ /**
162
+ * @private
163
+ * @type {symbol}
164
+ */
165
+ const cartControlSymbol = Symbol("cartControl");
166
+
167
+ /**
168
+ * @private
169
+ * @type {symbol}
170
+ */
171
+ const pendingCartPayloadSymbol = Symbol("pendingCartPayload");
172
+
173
+ /**
174
+ * @private
175
+ * @type {symbol}
176
+ */
177
+ const pricingRequestSymbol = Symbol("pricingRequest");
178
+
179
+ /**
180
+ * @private
181
+ * @type {symbol}
182
+ */
183
+ const pricingCacheSymbol = Symbol("pricingCache");
184
+
185
+ /**
186
+ * BuyBox
187
+ *
188
+ * @summary Product buy box control with variants, quantity, pricing, and add-to-cart.
189
+ * @fires monster-buy-box-change
190
+ * @fires monster-buy-box-valid
191
+ * @fires monster-buy-box-invalid
192
+ * @fires monster-buy-box-submit
193
+ * @fires monster-buy-box-success
194
+ * @fires monster-buy-box-error
195
+ * @fires monster-buy-box-loaded
196
+ */
197
+ class BuyBox extends CustomControl {
198
+ /**
199
+ * This method is called by the `instanceof` operator.
200
+ * @return {symbol}
201
+ */
202
+ static get [instanceSymbol]() {
203
+ return Symbol.for("@schukai/monster/components/form/buy-box@@instance");
204
+ }
205
+
206
+ /**
207
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
208
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
209
+ *
210
+ * @property {Object} templates Template definitions
211
+ * @property {string} templates.main Main template
212
+ * @property {Object} labels Labels
213
+ * @property {Object} features Feature toggles
214
+ * @property {boolean} features.messages Show status messages
215
+ * @property {boolean} features.rowSelects Allow row selects in variant select
216
+ * @property {boolean} features.combinedSelect Allow combined variant select
217
+ * @property {boolean} features.allowDecimal Allow decimal quantities
218
+ * @property {Object} product Static product config
219
+ * @property {Object} pricing Static pricing config
220
+ * @property {Object} variants Variant configuration (passed to monster-variant-select)
221
+ * @property {Object} quantity Quantity configuration
222
+ * @property {Object} cart Cart configuration (endpoint + mapping or cart control selector)
223
+ * @property {Object} stock Stock configuration (endpoint + mapping)
224
+ * @property {Object} datasource Datasource config (selector or rest)
225
+ * @property {Object} mapping Mapping from datasource data to product values
226
+ * @property {Object} actions Callback actions
227
+ */
228
+ get defaults() {
229
+ return Object.assign({}, super.defaults, {
230
+ templates: {
231
+ main: getTemplate(),
232
+ },
233
+ labels: getTranslations(),
234
+ features: {
235
+ messages: true,
236
+ rowSelects: false,
237
+ combinedSelect: false,
238
+ allowDecimal: false,
239
+ },
240
+ product: {
241
+ sku: null,
242
+ name: null,
243
+ stock: null,
244
+ delivery: null,
245
+ unitLabel: null,
246
+ currency: "EUR",
247
+ taxIncluded: true,
248
+ taxRate: null,
249
+ },
250
+ pricing: {
251
+ price: null,
252
+ listPrice: null,
253
+ basePrice: null,
254
+ basePriceUnit: null,
255
+ tiers: [],
256
+ api: {
257
+ url: null,
258
+ features: {
259
+ alwaysFetch: false,
260
+ },
261
+ fetch: {
262
+ method: "GET",
263
+ headers: {
264
+ accept: "application/json",
265
+ },
266
+ },
267
+ mapping: {
268
+ selector: "*",
269
+ priceTemplate: null,
270
+ listPriceTemplate: null,
271
+ basePriceTemplate: null,
272
+ basePriceUnitTemplate: null,
273
+ tiersPath: null,
274
+ taxRateTemplate: null,
275
+ taxIncludedTemplate: null,
276
+ },
277
+ },
278
+ },
279
+ variants: {
280
+ dimensions: [],
281
+ data: [],
282
+ features: {},
283
+ pricing: {
284
+ priceKey: "price",
285
+ listPriceKey: "listPrice",
286
+ basePriceKey: "basePrice",
287
+ basePriceUnitKey: "basePriceUnit",
288
+ currencyKey: "currency",
289
+ taxRateKey: "taxRate",
290
+ taxIncludedKey: "taxIncluded",
291
+ tiersKey: "tiers",
292
+ },
293
+ },
294
+ quantity: {
295
+ value: 1,
296
+ min: 1,
297
+ max: Number.POSITIVE_INFINITY,
298
+ step: 1,
299
+ precision: null,
300
+ allowed: null,
301
+ rounding: "half-up",
302
+ },
303
+ datasource: {
304
+ selector: null,
305
+ rest: null,
306
+ },
307
+ mapping: {
308
+ selector: "*",
309
+ skuTemplate: null,
310
+ nameTemplate: null,
311
+ stockTemplate: null,
312
+ deliveryTemplate: null,
313
+ unitLabelTemplate: null,
314
+ priceTemplate: null,
315
+ listPriceTemplate: null,
316
+ basePriceTemplate: null,
317
+ basePriceUnitTemplate: null,
318
+ currencyTemplate: null,
319
+ taxIncludedTemplate: null,
320
+ taxRateTemplate: null,
321
+ variantsPath: null,
322
+ cartPath: null,
323
+ },
324
+ stock: {
325
+ url: null,
326
+ fetch: {
327
+ method: "GET",
328
+ headers: {
329
+ accept: "application/json",
330
+ },
331
+ },
332
+ mapping: {
333
+ selector: "*",
334
+ stockTemplate: null,
335
+ },
336
+ features: {
337
+ autoCheck: true,
338
+ },
339
+ },
340
+ cart: {
341
+ url: null,
342
+ method: "POST",
343
+ headers: {
344
+ "Content-Type": "application/json",
345
+ },
346
+ controlSelector: null,
347
+ bodyTemplate: null,
348
+ mapping: {
349
+ selector: "*",
350
+ skuTemplate: null,
351
+ qtyTemplate: null,
352
+ },
353
+ },
354
+ actions: {
355
+ onchange: null,
356
+ onvalid: null,
357
+ oninvalid: null,
358
+ onsubmit: null,
359
+ onsuccess: null,
360
+ onerror: null,
361
+ onload: null,
362
+ },
363
+ });
364
+ }
365
+
366
+ /**
367
+ * @return {BuyBox}
368
+ */
369
+ [assembleMethodSymbol]() {
370
+ super[assembleMethodSymbol]();
371
+ this[pricingCacheSymbol] = new Map();
372
+ initControlReferences.call(this);
373
+ initDatasource.call(this);
374
+ initEventHandler.call(this);
375
+ updateSubmitLabel.call(this);
376
+ applyData.call(this);
377
+ return this;
378
+ }
379
+
380
+ /**
381
+ * Re-apply options and mapped data.
382
+ * @return {BuyBox}
383
+ */
384
+ refresh() {
385
+ initDatasource.call(this);
386
+ applyData.call(this);
387
+ return this;
388
+ }
389
+
390
+ /**
391
+ * @return {CSSStyleSheet[]}
392
+ */
393
+ static getCSSStyleSheet() {
394
+ return [
395
+ CommonStyleSheet,
396
+ FormStyleSheet,
397
+ VariantSelectStyleSheet,
398
+ BuyBoxStyleSheet,
399
+ ];
400
+ }
401
+
402
+ /**
403
+ * @return {string}
404
+ */
405
+ static getTag() {
406
+ return "monster-buy-box";
407
+ }
408
+ }
409
+
410
+ /**
411
+ * @private
412
+ */
413
+ function initControlReferences() {
414
+ this[controlElementSymbol] = this.shadowRoot.querySelector(
415
+ "[data-monster-role=control]",
416
+ );
417
+ this[variantElementSymbol] = this.shadowRoot.querySelector(
418
+ "[data-monster-role=variants]",
419
+ );
420
+ this[quantityElementSymbol] = this.shadowRoot.querySelector(
421
+ "[data-monster-role=quantity]",
422
+ );
423
+ this[quantityInputElementSymbol] = this.shadowRoot.querySelector(
424
+ "[data-monster-role=quantity-input]",
425
+ );
426
+ this[quantityUnitElementSymbol] = this.shadowRoot.querySelector(
427
+ "[data-monster-role=quantity-unit]",
428
+ );
429
+ this[submitElementSymbol] = this.shadowRoot.querySelector(
430
+ "[data-monster-role=submit]",
431
+ );
432
+ this[submitLabelElementSymbol] = this.shadowRoot.querySelector(
433
+ "[data-monster-role=submit-label]",
434
+ );
435
+ this[messageElementSymbol] = this.shadowRoot.querySelector(
436
+ "[data-monster-role=message]",
437
+ );
438
+ this[quantityStaticElementSymbol] = this.shadowRoot.querySelector(
439
+ "[data-monster-role=quantity-static]",
440
+ );
441
+ this[priceElementSymbol] = this.shadowRoot.querySelector(
442
+ "[data-monster-role=price]",
443
+ );
444
+ this[sumElementSymbol] = this.shadowRoot.querySelector(
445
+ "[data-monster-role=sum]",
446
+ );
447
+ this[listPriceElementSymbol] = this.shadowRoot.querySelector(
448
+ "[data-monster-role=list-price]",
449
+ );
450
+ this[basePriceElementSymbol] = this.shadowRoot.querySelector(
451
+ "[data-monster-role=base-price]",
452
+ );
453
+ this[taxElementSymbol] = this.shadowRoot.querySelector(
454
+ "[data-monster-role=tax]",
455
+ );
456
+ this[totalElementSymbol] = this.shadowRoot.querySelector(
457
+ "[data-monster-role=total]",
458
+ );
459
+ this[deliveryElementSymbol] = this.shadowRoot.querySelector(
460
+ "[data-monster-role=delivery]",
461
+ );
462
+ }
463
+
464
+ /**
465
+ * @private
466
+ * @return {HTMLElement|null}
467
+ */
468
+ function getCartControl() {
469
+ const selector = this.getOption("cart.controlSelector");
470
+ if (!isString(selector) || selector === "") {
471
+ return null;
472
+ }
473
+ if (this[cartControlSymbol] && this[cartControlSymbol].isConnected) {
474
+ return this[cartControlSymbol];
475
+ }
476
+ const element = getDocument().querySelector(selector);
477
+ if (element) {
478
+ this[cartControlSymbol] = element;
479
+ }
480
+ return element || null;
481
+ }
482
+
483
+ /**
484
+ * @private
485
+ */
486
+ function initDatasource() {
487
+ if (this[datasourceElementSymbol]) {
488
+ return;
489
+ }
490
+ const selector = this.getOption("datasource.selector");
491
+ if (isString(selector) && selector !== "") {
492
+ this[datasourceElementSymbol] = getDocument().querySelector(selector);
493
+ } else if (isObject(this.getOption("datasource.rest"))) {
494
+ const rest = getDocument().createElement("monster-datasource-rest");
495
+ rest.setOption("read", this.getOption("datasource.rest.read", {}));
496
+ rest.setOption("features.autoInit", true);
497
+ rest.setOption("autoInit.oneTime", true);
498
+ this.appendChild(rest);
499
+ this[datasourceElementSymbol] = rest;
500
+ }
501
+
502
+ if (this[datasourceElementSymbol]) {
503
+ this[datasourceElementSymbol].addEventListener(
504
+ "monster-datasource-fetched",
505
+ (event) => {
506
+ const data = event?.detail?.data || this[datasourceElementSymbol]?.data;
507
+ if (data) {
508
+ this[productDataSymbol] = data;
509
+ applyData.call(this);
510
+ }
511
+ },
512
+ );
513
+ }
514
+ }
515
+
516
+ /**
517
+ * @private
518
+ */
519
+ function initEventHandler() {
520
+ const variants = this.shadowRoot.querySelector("monster-variant-select");
521
+ const quantity = this.shadowRoot.querySelector("monster-quantity");
522
+ const submit = this[submitElementSymbol];
523
+
524
+ if (variants) {
525
+ variants.addEventListener("monster-variant-select-change", () => {
526
+ updateState.call(this);
527
+ checkStockIfNeeded.call(this);
528
+ checkPricingIfNeeded.call(this);
529
+ });
530
+ }
531
+
532
+ if (quantity) {
533
+ quantity.addEventListener("monster-quantity-change", () => {
534
+ updateState.call(this);
535
+ checkStockIfNeeded.call(this);
536
+ checkPricingIfNeeded.call(this);
537
+ });
538
+ }
539
+ if (this[quantityInputElementSymbol]) {
540
+ this[quantityInputElementSymbol].addEventListener("input", () => {
541
+ updateState.call(this);
542
+ checkStockIfNeeded.call(this);
543
+ checkPricingIfNeeded.call(this);
544
+ });
545
+ }
546
+
547
+ if (submit) {
548
+ submit.setOption("actions.click", () => {
549
+ handleSubmit.call(this);
550
+ });
551
+ }
552
+ }
553
+
554
+ /**
555
+ * @private
556
+ */
557
+ function applyData() {
558
+ const data = this[productDataSymbol] || this.getOption("product");
559
+ if (!data) {
560
+ return;
561
+ }
562
+
563
+ const mapped = mapProductData.call(this, data);
564
+ this[productDataSymbol] = mapped;
565
+ this[cartDataSymbol] = mapped.cart;
566
+
567
+ applyVariants.call(this, mapped);
568
+ applyQuantity.call(this);
569
+ updateState.call(this, true);
570
+ checkStockIfNeeded.call(this);
571
+ checkPricingIfNeeded.call(this);
572
+ updateSubmitLabel.call(this);
573
+ fireCustomEvent(this, "monster-buy-box-loaded", { product: mapped });
574
+ const loadAction = this.getOption("actions.onload");
575
+ if (isFunction(loadAction)) {
576
+ loadAction.call(this, mapped);
577
+ }
578
+ }
579
+
580
+ /**
581
+ * @private
582
+ * @param {Object} data
583
+ * @return {Object}
584
+ */
585
+ function mapProductData(data) {
586
+ const mapping = this.getOption("mapping", {});
587
+ let source = data;
588
+ if (
589
+ isString(mapping?.selector) &&
590
+ mapping.selector !== "*" &&
591
+ mapping.selector
592
+ ) {
593
+ try {
594
+ source = new Pathfinder(data).getVia(mapping.selector);
595
+ } catch (_e) {
596
+ source = data;
597
+ }
598
+ }
599
+
600
+ const value = (key) => mapValueFromSource(source, mapping[key]);
601
+
602
+ const product = Object.assign({}, this.getOption("product"), {
603
+ sku: value("skuTemplate") ?? this.getOption("product.sku"),
604
+ name: value("nameTemplate") ?? this.getOption("product.name"),
605
+ stock: parseNumber(value("stockTemplate"), this.getOption("product.stock")),
606
+ delivery: value("deliveryTemplate") ?? this.getOption("product.delivery"),
607
+ unitLabel:
608
+ value("unitLabelTemplate") ?? this.getOption("product.unitLabel"),
609
+ currency: value("currencyTemplate") ?? this.getOption("product.currency"),
610
+ taxIncluded: parseBoolean(
611
+ value("taxIncludedTemplate"),
612
+ this.getOption("product.taxIncluded"),
613
+ ),
614
+ taxRate: parseNumber(
615
+ value("taxRateTemplate"),
616
+ this.getOption("product.taxRate"),
617
+ ),
618
+ });
619
+
620
+ const pricing = Object.assign({}, this.getOption("pricing"), {
621
+ price: parseNumber(value("priceTemplate"), this.getOption("pricing.price")),
622
+ listPrice: parseNumber(
623
+ value("listPriceTemplate"),
624
+ this.getOption("pricing.listPrice"),
625
+ ),
626
+ basePrice: parseNumber(
627
+ value("basePriceTemplate"),
628
+ this.getOption("pricing.basePrice"),
629
+ ),
630
+ basePriceUnit:
631
+ value("basePriceUnitTemplate") ?? this.getOption("pricing.basePriceUnit"),
632
+ });
633
+
634
+ const variantsPath = mapping?.variantsPath;
635
+ const variantData = variantsPath
636
+ ? new Pathfinder(source).getVia(variantsPath)
637
+ : this.getOption("variants.data");
638
+
639
+ const cartPath = mapping?.cartPath;
640
+ const cartData = cartPath ? new Pathfinder(source).getVia(cartPath) : null;
641
+
642
+ return { product, pricing, variants: variantData, cart: cartData, source };
643
+ }
644
+
645
+ /**
646
+ * @private
647
+ * @param {Object} source
648
+ * @param {string|null} template
649
+ * @return {string|null}
650
+ */
651
+ function mapValueFromSource(source, template) {
652
+ if (!isString(template) || template === "") {
653
+ return null;
654
+ }
655
+ try {
656
+ if (template.includes("${")) {
657
+ return new Formatter(source).format(template);
658
+ }
659
+ return new Pathfinder(source).getVia(template);
660
+ } catch (_e) {
661
+ return null;
662
+ }
663
+ }
664
+
665
+ /**
666
+ * @private
667
+ * @param {Object} mapped
668
+ */
669
+ function applyVariants(mapped) {
670
+ const variants = this.shadowRoot.querySelector("monster-variant-select");
671
+ if (!variants) {
672
+ return;
673
+ }
674
+
675
+ variants.setAttribute(
676
+ "aria-label",
677
+ this.getOption("labels.variantsAria") || "Variant selection",
678
+ );
679
+ const variantOptions = Object.assign({}, this.getOption("variants"));
680
+ if (Array.isArray(mapped?.variants)) {
681
+ variantOptions.data = mapped.variants;
682
+ }
683
+
684
+ variantOptions.features = Object.assign({}, variantOptions.features, {
685
+ rowSelects: this.getOption("features.rowSelects"),
686
+ combinedSelect: this.getOption("features.combinedSelect"),
687
+ });
688
+
689
+ const hasDimensions =
690
+ Array.isArray(variantOptions.dimensions) &&
691
+ variantOptions.dimensions.length > 0;
692
+ const hasData =
693
+ Array.isArray(variantOptions.data) && variantOptions.data.length > 0;
694
+ const hasUrl = isString(variantOptions.url) && variantOptions.url !== "";
695
+ if (this[variantElementSymbol]) {
696
+ this[variantElementSymbol].hidden = !(hasDimensions && (hasData || hasUrl));
697
+ }
698
+
699
+ variants.setOption("dimensions", variantOptions.dimensions || []);
700
+ variants.setOption("data", variantOptions.data || []);
701
+ variants.setOption("url", variantOptions.url ?? null);
702
+ variants.setOption("fetch", variantOptions.fetch || {});
703
+ variants.setOption("mapping", variantOptions.mapping || {});
704
+ variants.setOption("layout", variantOptions.layout || {});
705
+ variants.setOption("features.rowSelects", variantOptions.features.rowSelects);
706
+ variants.setOption(
707
+ "features.combinedSelect",
708
+ variantOptions.features.combinedSelect,
709
+ );
710
+ if (typeof variants.refresh === "function") {
711
+ variants.refresh();
712
+ }
713
+ }
714
+
715
+ /**
716
+ * @private
717
+ */
718
+ function applyQuantity() {
719
+ const quantity = this.shadowRoot.querySelector("monster-quantity");
720
+ const quantityInput = this[quantityInputElementSymbol];
721
+ const quantityStatic = this[quantityStaticElementSymbol];
722
+ const quantityLabel = this.shadowRoot.querySelector(
723
+ "[data-monster-role=quantity-label]",
724
+ );
725
+ if (!quantity) {
726
+ return;
727
+ }
728
+ if (!quantityInput) {
729
+ return;
730
+ }
731
+ quantity.setAttribute(
732
+ "aria-label",
733
+ this.getOption("labels.quantityAria") || "Quantity",
734
+ );
735
+ quantityInput.setAttribute(
736
+ "aria-label",
737
+ this.getOption("labels.quantityAria") || "Quantity",
738
+ );
739
+ const options = this.getOption("quantity", {});
740
+ const precision =
741
+ this.getOption("features.allowDecimal") === true
742
+ ? Number(options.precision ?? 2)
743
+ : 0;
744
+ const useDecimalInput = this.getOption("features.allowDecimal") === true;
745
+ const hasFixedQuantity = isFixedQuantity(options);
746
+ quantity.setOption("min", options.min);
747
+ quantity.setOption("max", options.max);
748
+ quantity.setOption("step", options.step);
749
+ quantity.setOption("precision", precision);
750
+ quantity.setOption("inputmode", useDecimalInput ? "decimal" : "numeric");
751
+ quantity.value = options.value ?? options.min ?? 1;
752
+
753
+ quantity.hidden = useDecimalInput || hasFixedQuantity;
754
+ quantityInput.hidden = !useDecimalInput || hasFixedQuantity;
755
+ quantityInput.step = options.step ?? 1;
756
+ quantityInput.min = Number.isFinite(options.min) ? options.min : "";
757
+ quantityInput.max = Number.isFinite(options.max) ? options.max : "";
758
+ quantityInput.value = formatQuantityForLocale(
759
+ Number.isFinite(options.value) ? options.value : (options.min ?? 1),
760
+ precision,
761
+ );
762
+ if (quantityStatic) {
763
+ quantityStatic.hidden = !hasFixedQuantity;
764
+ if (hasFixedQuantity) {
765
+ const label = this.getOption("labels.quantity") || "Quantity";
766
+ setElementText(
767
+ quantityStatic,
768
+ `${label}: ${formatQuantityForLocale(options.min ?? 1, precision)}`,
769
+ );
770
+ }
771
+ }
772
+ if (quantityLabel) {
773
+ quantityLabel.hidden = hasFixedQuantity;
774
+ }
775
+
776
+ if (this[quantityUnitElementSymbol]) {
777
+ const unit = this[productDataSymbol]?.product?.unitLabel;
778
+ this[quantityUnitElementSymbol].textContent =
779
+ unit && unit !== "" ? `(${unit})` : "";
780
+ }
781
+ }
782
+
783
+ /**
784
+ * @private
785
+ * @param {boolean} initial
786
+ */
787
+ function updateState(initial = false) {
788
+ const variants = this.shadowRoot.querySelector("monster-variant-select");
789
+ const quantity = this.shadowRoot.querySelector("monster-quantity");
790
+ const selection = variants?.getOption?.("selection") || {};
791
+ const hasVariants = hasVariantOptions.call(this, variants);
792
+ const sku = hasVariants ? variants?.value : this.getOption("product.sku");
793
+ const qty = getQuantityValue.call(this, quantity);
794
+
795
+ const productBase =
796
+ this[productDataSymbol]?.product || this.getOption("product");
797
+ const pricingBase =
798
+ this[productDataSymbol]?.pricing || this.getOption("pricing");
799
+ const variantData = findSelectedVariantData.call(this, variants, selection);
800
+ const variantPricingActive = hasVariantPricing.call(this, variants);
801
+ const resolved = resolvePricing.call(
802
+ this,
803
+ pricingBase,
804
+ productBase,
805
+ variantData,
806
+ qty,
807
+ variantPricingActive,
808
+ );
809
+ const product = resolved.product;
810
+ const pricing = resolved.pricing;
811
+ const isPriceOk = resolved.priceAvailable !== false;
812
+ const stock = product?.stock;
813
+ const cartItem = findCartItem.call(this, sku);
814
+ const hasSku = isString(sku) && sku !== "";
815
+
816
+ const isValidQty = isQuantityAllowed.call(this, qty);
817
+ const isStockOk = hasSku
818
+ ? Number.isFinite(stock)
819
+ ? qty <= stock
820
+ : true
821
+ : true;
822
+ const isVariantOk = hasSku;
823
+
824
+ updatePriceDisplay.call(this, pricing, qty, product);
825
+ updateMessage.call(this, {
826
+ isValidQty,
827
+ isStockOk,
828
+ isVariantOk,
829
+ isPriceOk,
830
+ hasVariants,
831
+ });
832
+
833
+ const detail = {
834
+ sku,
835
+ selection,
836
+ qty,
837
+ product,
838
+ pricing,
839
+ cartItem,
840
+ priceAvailable: isPriceOk,
841
+ };
842
+ fireCustomEvent(this, "monster-buy-box-change", detail);
843
+ const changeAction = this.getOption("actions.onchange");
844
+ if (isFunction(changeAction)) {
845
+ changeAction.call(this, detail);
846
+ }
847
+
848
+ const valid = isValidQty && isStockOk && isVariantOk && isPriceOk;
849
+ if (!initial) {
850
+ if (valid) {
851
+ fireCustomEvent(this, "monster-buy-box-valid", detail);
852
+ const action = this.getOption("actions.onvalid");
853
+ if (isFunction(action)) {
854
+ action.call(this, detail);
855
+ }
856
+ } else {
857
+ fireCustomEvent(this, "monster-buy-box-invalid", detail);
858
+ const action = this.getOption("actions.oninvalid");
859
+ if (isFunction(action)) {
860
+ action.call(this, detail);
861
+ }
862
+ }
863
+ }
864
+
865
+ setSubmitEnabled.call(this, valid);
866
+ }
867
+
868
+ /**
869
+ * @private
870
+ * @param {Object} pricing
871
+ * @param {number} qty
872
+ * @param {Object} product
873
+ */
874
+ function updatePriceDisplay(pricing, qty, product) {
875
+ const currency = product?.currency || "EUR";
876
+ const price = pricing?.price;
877
+ const listPrice = pricing?.listPrice;
878
+ const basePrice = pricing?.basePrice;
879
+ const baseUnit = pricing?.basePriceUnit;
880
+ const taxIncluded = product?.taxIncluded;
881
+ const taxRate = product?.taxRate;
882
+ const subtotal =
883
+ price !== null && price !== undefined && Number.isFinite(qty)
884
+ ? price * qty
885
+ : null;
886
+
887
+ if (this[priceElementSymbol]) {
888
+ setElementText(
889
+ this[priceElementSymbol],
890
+ price !== null && price !== undefined ? formatMoney(price, currency) : "",
891
+ );
892
+ }
893
+ if (this[sumElementSymbol]) {
894
+ setElementText(
895
+ this[sumElementSymbol],
896
+ subtotal !== null ? formatMoney(subtotal, currency) : "",
897
+ );
898
+ }
899
+ if (this[listPriceElementSymbol]) {
900
+ setElementText(
901
+ this[listPriceElementSymbol],
902
+ listPrice !== null && listPrice !== undefined
903
+ ? formatMoney(listPrice, currency)
904
+ : "",
905
+ );
906
+ }
907
+ if (this[basePriceElementSymbol]) {
908
+ if (basePrice !== null && basePrice !== undefined) {
909
+ const base = formatMoney(basePrice, currency);
910
+ setElementText(
911
+ this[basePriceElementSymbol],
912
+ baseUnit ? `${base} / ${baseUnit}` : base,
913
+ );
914
+ } else {
915
+ setElementText(this[basePriceElementSymbol], "");
916
+ }
917
+ }
918
+ if (this[taxElementSymbol]) {
919
+ const label = taxIncluded
920
+ ? this.getOption("labels.taxIncluded")
921
+ : this.getOption("labels.taxExcluded");
922
+ if (Number.isFinite(taxRate)) {
923
+ setElementText(this[taxElementSymbol], `${label} (${taxRate}%)`);
924
+ } else {
925
+ setElementText(this[taxElementSymbol], label);
926
+ }
927
+ }
928
+ if (this[totalElementSymbol]) {
929
+ const total = subtotal;
930
+ setElementText(
931
+ this[totalElementSymbol],
932
+ total !== null ? formatMoney(total, currency) : "",
933
+ );
934
+ }
935
+ if (this[deliveryElementSymbol]) {
936
+ setElementText(
937
+ this[deliveryElementSymbol],
938
+ product?.delivery ?? this.getOption("labels.deliveryUnknown"),
939
+ );
940
+ }
941
+ }
942
+
943
+ /**
944
+ * @private
945
+ * @param {Object} state
946
+ */
947
+ function updateMessage(state) {
948
+ if (this.getOption("features.messages") === false) {
949
+ if (this[messageElementSymbol]) {
950
+ this[messageElementSymbol].textContent = "";
951
+ }
952
+ return;
953
+ }
954
+
955
+ if (!this[messageElementSymbol]) {
956
+ return;
957
+ }
958
+
959
+ let msg = "";
960
+ msg = getInvalidMessage.call(this, state);
961
+
962
+ this[messageElementSymbol].textContent = msg;
963
+ }
964
+
965
+ /**
966
+ * @private
967
+ * @param {Object} state
968
+ * @return {string}
969
+ */
970
+ function getInvalidMessage(state) {
971
+ if (!state) {
972
+ return "";
973
+ }
974
+ if (!state.isVariantOk && state.hasVariants) {
975
+ return this.getOption("labels.selectVariant");
976
+ }
977
+ if (state.isPriceOk === false) {
978
+ return this.getOption("labels.priceUnavailable");
979
+ }
980
+ if (!state.isValidQty) {
981
+ return this.getOption("labels.selectQuantity");
982
+ }
983
+ if (!state.isStockOk) {
984
+ return this.getOption("labels.outOfStock");
985
+ }
986
+ return "";
987
+ }
988
+
989
+ /**
990
+ * @private
991
+ * @param {boolean} enabled
992
+ */
993
+ function setSubmitEnabled(enabled) {
994
+ if (!this[submitElementSymbol]) {
995
+ return;
996
+ }
997
+ this[submitElementSymbol].setOption("features.disableButton", false);
998
+ }
999
+
1000
+ /**
1001
+ * @private
1002
+ */
1003
+ function handleSubmit() {
1004
+ const variants = this.shadowRoot.querySelector("monster-variant-select");
1005
+ const quantity = this.shadowRoot.querySelector("monster-quantity");
1006
+ const hasVariants = hasVariantOptions.call(this, variants);
1007
+ const sku = hasVariants ? variants?.value : this.getOption("product.sku");
1008
+ const qty = getQuantityValue.call(this, quantity);
1009
+ const selection = variants?.getOption?.("selection") || {};
1010
+ const productBase =
1011
+ this[productDataSymbol]?.product || this.getOption("product");
1012
+ const pricingBase =
1013
+ this[productDataSymbol]?.pricing || this.getOption("pricing");
1014
+ const variantData = findSelectedVariantData.call(this, variants, selection);
1015
+ const variantPricingActive = hasVariantPricing.call(this, variants);
1016
+ const resolved = resolvePricing.call(
1017
+ this,
1018
+ pricingBase,
1019
+ productBase,
1020
+ variantData,
1021
+ qty,
1022
+ variantPricingActive,
1023
+ );
1024
+ const product = resolved.product;
1025
+ const pricing = resolved.pricing;
1026
+ const isPriceOk = resolved.priceAvailable !== false;
1027
+
1028
+ const hasSku = isString(sku) && sku !== "";
1029
+ const stock = product?.stock;
1030
+ const isValidQty = isQuantityAllowed.call(this, qty);
1031
+ const isStockOk = hasSku
1032
+ ? Number.isFinite(stock)
1033
+ ? qty <= stock
1034
+ : true
1035
+ : true;
1036
+ const isVariantOk = hasSku;
1037
+ const valid = isValidQty && isStockOk && isVariantOk && isPriceOk;
1038
+ if (!valid) {
1039
+ updateState.call(this);
1040
+ const button = this[submitElementSymbol];
1041
+ const message = getInvalidMessage.call(this, {
1042
+ isValidQty,
1043
+ isStockOk,
1044
+ isVariantOk,
1045
+ isPriceOk,
1046
+ hasVariants,
1047
+ });
1048
+ button?.setState("failed", 3000);
1049
+ button?.setMessage(message);
1050
+ button?.showMessage?.(3000);
1051
+ return;
1052
+ }
1053
+
1054
+ const detail = { sku, qty, selection, product, pricing };
1055
+ fireCustomEvent(this, "monster-buy-box-submit", detail);
1056
+ const action = this.getOption("actions.onsubmit");
1057
+ if (isFunction(action)) {
1058
+ action.call(this, detail);
1059
+ }
1060
+
1061
+ const button = this[submitElementSymbol];
1062
+ const cartControl = getCartControl.call(this);
1063
+ if (cartControl && isFunction(cartControl.addOrChange)) {
1064
+ this[pendingCartPayloadSymbol] = detail;
1065
+ button?.setState("activity");
1066
+ const onVerified = (event) => {
1067
+ if (!matchesCartPayload.call(this, event?.detail)) {
1068
+ return;
1069
+ }
1070
+ this[pendingCartPayloadSymbol] = null;
1071
+ cartControl.removeEventListener(
1072
+ "monster-cart-control-verified",
1073
+ onVerified,
1074
+ );
1075
+ cartControl.removeEventListener("monster-cart-control-error", onError);
1076
+ const responseData = event?.detail?.data;
1077
+ const items = Array.isArray(responseData)
1078
+ ? responseData
1079
+ : Array.isArray(responseData?.items)
1080
+ ? responseData.items
1081
+ : null;
1082
+ if (items) {
1083
+ this[cartDataSymbol] = items;
1084
+ updateState.call(this);
1085
+ }
1086
+ button?.setState("successful", 2000);
1087
+ fireCustomEvent(this, "monster-buy-box-success", {
1088
+ sku,
1089
+ qty,
1090
+ payload: event?.detail?.data,
1091
+ });
1092
+ const okAction = this.getOption("actions.onsuccess");
1093
+ if (isFunction(okAction)) {
1094
+ okAction.call(this, { sku, qty, payload: event?.detail?.data });
1095
+ }
1096
+ };
1097
+ const onError = (event) => {
1098
+ if (!matchesCartPayload.call(this, event?.detail)) {
1099
+ return;
1100
+ }
1101
+ this[pendingCartPayloadSymbol] = null;
1102
+ cartControl.removeEventListener(
1103
+ "monster-cart-control-verified",
1104
+ onVerified,
1105
+ );
1106
+ cartControl.removeEventListener("monster-cart-control-error", onError);
1107
+ button?.setState("failed", 3000);
1108
+ button?.setMessage(this.getOption("labels.addToCartError"));
1109
+ button?.showMessage?.(3000);
1110
+ fireCustomEvent(this, "monster-buy-box-error", {
1111
+ sku,
1112
+ qty,
1113
+ error: event?.detail?.error,
1114
+ });
1115
+ const errAction = this.getOption("actions.onerror");
1116
+ if (isFunction(errAction)) {
1117
+ errAction.call(this, event?.detail?.error);
1118
+ }
1119
+ };
1120
+ cartControl.addEventListener("monster-cart-control-verified", onVerified);
1121
+ cartControl.addEventListener("monster-cart-control-error", onError);
1122
+ cartControl.addOrChange(detail);
1123
+ return;
1124
+ }
1125
+
1126
+ const url = this.getOption("cart.url");
1127
+ if (!isString(url) || url === "") {
1128
+ const notConfigured = this.getOption("labels.addToCartNotConfigured");
1129
+ button?.setState("failed", 3000);
1130
+ button?.setMessage(notConfigured);
1131
+ button?.showMessage?.(3000);
1132
+ fireCustomEvent(this, "monster-buy-box-error", {
1133
+ sku,
1134
+ qty,
1135
+ error: new Error("cart url not configured"),
1136
+ });
1137
+ const errAction = this.getOption("actions.onerror");
1138
+ if (isFunction(errAction)) {
1139
+ errAction.call(this, new Error("cart url not configured"));
1140
+ }
1141
+ return;
1142
+ }
1143
+
1144
+ button?.setState("activity");
1145
+
1146
+ fetch(url, buildCartRequest.call(this, detail))
1147
+ .then(async (response) => {
1148
+ if (!response.ok) {
1149
+ throw new Error("add to cart failed");
1150
+ }
1151
+ const payload = await response.json();
1152
+ button?.setState("successful", 2000);
1153
+ fireCustomEvent(this, "monster-buy-box-success", { sku, qty, payload });
1154
+ const okAction = this.getOption("actions.onsuccess");
1155
+ if (isFunction(okAction)) {
1156
+ okAction.call(this, { sku, qty, payload });
1157
+ }
1158
+ })
1159
+ .catch((e) => {
1160
+ button?.setState("failed", 3000);
1161
+ button?.setMessage(this.getOption("labels.addToCartError"));
1162
+ button?.showMessage?.(3000);
1163
+ fireCustomEvent(this, "monster-buy-box-error", { sku, qty, error: e });
1164
+ const errAction = this.getOption("actions.onerror");
1165
+ if (isFunction(errAction)) {
1166
+ errAction.call(this, e);
1167
+ }
1168
+ });
1169
+ }
1170
+
1171
+ /**
1172
+ * @private
1173
+ * @param {Object} detail
1174
+ * @return {boolean}
1175
+ */
1176
+ function matchesCartPayload(detail) {
1177
+ const pending = this[pendingCartPayloadSymbol];
1178
+ if (!pending) {
1179
+ return false;
1180
+ }
1181
+ const source = detail?.source || detail?.payload;
1182
+ if (!source || typeof source !== "object") {
1183
+ return true;
1184
+ }
1185
+ if (isString(source.sku) && source.sku !== pending.sku) {
1186
+ return false;
1187
+ }
1188
+ if (source.qty !== undefined && Number(source.qty) !== Number(pending.qty)) {
1189
+ return false;
1190
+ }
1191
+ return true;
1192
+ }
1193
+
1194
+ /**
1195
+ * @private
1196
+ * @param {Object} data
1197
+ * @return {Object}
1198
+ */
1199
+ function buildCartRequest(data) {
1200
+ const headers = Object.assign({}, this.getOption("cart.headers", {}));
1201
+ const method = this.getOption("cart.method") || "POST";
1202
+ const bodyTemplate = this.getOption("cart.bodyTemplate");
1203
+ let body = null;
1204
+
1205
+ if (isString(bodyTemplate) && bodyTemplate.includes("${")) {
1206
+ body = new Formatter(stringifyForTemplate(data)).format(bodyTemplate);
1207
+ } else {
1208
+ body = JSON.stringify(data);
1209
+ }
1210
+
1211
+ return { method, headers, body };
1212
+ }
1213
+
1214
+ /**
1215
+ * @private
1216
+ * @param {Object|Array|string|number|boolean|null} value
1217
+ * @return {Object|Array|string}
1218
+ */
1219
+ function stringifyForTemplate(value) {
1220
+ if (value === null || value === undefined) {
1221
+ return "";
1222
+ }
1223
+ if (Array.isArray(value)) {
1224
+ return value.map((entry) => stringifyForTemplate(entry));
1225
+ }
1226
+ if (typeof value === "object") {
1227
+ const result = {};
1228
+ for (const [key, entry] of Object.entries(value)) {
1229
+ result[key] = stringifyForTemplate(entry);
1230
+ }
1231
+ return result;
1232
+ }
1233
+ return `${value}`;
1234
+ }
1235
+
1236
+ /**
1237
+ * @private
1238
+ * @param {number} value
1239
+ * @return {boolean}
1240
+ */
1241
+ function isQuantityAllowed(value) {
1242
+ if (!Number.isFinite(value)) {
1243
+ return false;
1244
+ }
1245
+
1246
+ const options = this.getOption("quantity", {});
1247
+ if (this.getOption("features.allowDecimal") !== true) {
1248
+ if (!Number.isInteger(value)) {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ const min = Number(options.min);
1253
+ const max = Number(options.max);
1254
+ if (Number.isFinite(min) && value < min) {
1255
+ return false;
1256
+ }
1257
+ if (Number.isFinite(max) && value > max) {
1258
+ return false;
1259
+ }
1260
+
1261
+ const allowed = options.allowed;
1262
+ if (!allowed) {
1263
+ return true;
1264
+ }
1265
+
1266
+ const { list, ranges } = parseAllowed(allowed);
1267
+ if (list.length === 0 && ranges.length === 0) {
1268
+ return true;
1269
+ }
1270
+
1271
+ const allowDecimal = this.getOption("features.allowDecimal") === true;
1272
+ const precision = Number.isFinite(options.precision)
1273
+ ? options.precision
1274
+ : allowDecimal
1275
+ ? 2
1276
+ : 0;
1277
+ const rounded = roundQuantity(value, precision, options.rounding);
1278
+ if (list.includes(rounded)) {
1279
+ return true;
1280
+ }
1281
+ for (const range of ranges) {
1282
+ if (rounded >= range.min && rounded <= range.max) {
1283
+ return true;
1284
+ }
1285
+ }
1286
+ return false;
1287
+ }
1288
+
1289
+ /**
1290
+ * @private
1291
+ * @param {string|Array} allowed
1292
+ * @return {Object}
1293
+ */
1294
+ function parseAllowed(allowed) {
1295
+ if (Array.isArray(allowed)) {
1296
+ return {
1297
+ list: allowed.filter((v) => Number.isFinite(v)),
1298
+ ranges: [],
1299
+ };
1300
+ }
1301
+ if (!isString(allowed)) {
1302
+ return { list: [], ranges: [] };
1303
+ }
1304
+ const list = [];
1305
+ const ranges = [];
1306
+ const parts = allowed.split(",");
1307
+ for (const part of parts) {
1308
+ const trimmed = part.trim();
1309
+ if (trimmed === "") {
1310
+ continue;
1311
+ }
1312
+ if (trimmed.includes("-")) {
1313
+ const [minRaw, maxRaw] = trimmed.split("-");
1314
+ const min = minRaw === "" ? Number.NEGATIVE_INFINITY : Number(minRaw);
1315
+ const max = maxRaw === "" ? Number.POSITIVE_INFINITY : Number(maxRaw);
1316
+ if (Number.isFinite(min) || Number.isFinite(max)) {
1317
+ ranges.push({ min, max });
1318
+ }
1319
+ continue;
1320
+ }
1321
+ const value = Number(trimmed);
1322
+ if (Number.isFinite(value)) {
1323
+ list.push(value);
1324
+ }
1325
+ }
1326
+ return { list, ranges };
1327
+ }
1328
+
1329
+ /**
1330
+ * @private
1331
+ * @param {HTMLElement|null} variants
1332
+ * @param {Object} selection
1333
+ * @return {Object|null}
1334
+ */
1335
+ function findSelectedVariantData(variants, selection) {
1336
+ if (!(variants instanceof HTMLElement)) {
1337
+ return null;
1338
+ }
1339
+ const data = variants.getOption?.("data") || [];
1340
+ const dimensions = variants.getOption?.("dimensions") || [];
1341
+ if (!Array.isArray(data) || data.length === 0) {
1342
+ return null;
1343
+ }
1344
+ if (!selection || Object.keys(selection).length === 0) {
1345
+ return null;
1346
+ }
1347
+ for (const item of data) {
1348
+ let matches = true;
1349
+ for (const def of dimensions) {
1350
+ const key = def?.key;
1351
+ if (!isString(key) || key === "") {
1352
+ continue;
1353
+ }
1354
+ const expected = selection?.[key];
1355
+ if (!isString(expected) || expected === "") {
1356
+ matches = false;
1357
+ break;
1358
+ }
1359
+ const value = getDimensionValueFromItem(item, def);
1360
+ if (String(value) !== String(expected)) {
1361
+ matches = false;
1362
+ break;
1363
+ }
1364
+ }
1365
+ if (matches) {
1366
+ return item;
1367
+ }
1368
+ }
1369
+ return null;
1370
+ }
1371
+
1372
+ /**
1373
+ * @private
1374
+ * @param {HTMLElement|null} variants
1375
+ * @return {boolean}
1376
+ */
1377
+ function hasVariantPricing(variants) {
1378
+ if (!(variants instanceof HTMLElement)) {
1379
+ return false;
1380
+ }
1381
+ const data = variants.getOption?.("data") || [];
1382
+ if (!Array.isArray(data) || data.length === 0) {
1383
+ return false;
1384
+ }
1385
+ const pricingKeys = getVariantPricingKeys.call(this);
1386
+ const priceKey = pricingKeys.priceKey;
1387
+ const tiersKey = pricingKeys.tiersKey;
1388
+ for (const item of data) {
1389
+ if (isString(priceKey) && priceKey !== "") {
1390
+ const value = item?.[priceKey];
1391
+ if (value !== null && value !== undefined && value !== "") {
1392
+ return true;
1393
+ }
1394
+ }
1395
+ if (isString(tiersKey) && tiersKey !== "") {
1396
+ const tiers = item?.[tiersKey];
1397
+ if (Array.isArray(tiers) && tiers.length > 0) {
1398
+ return true;
1399
+ }
1400
+ }
1401
+ }
1402
+ return false;
1403
+ }
1404
+
1405
+ /**
1406
+ * @private
1407
+ * @param {Object} item
1408
+ * @param {Object} def
1409
+ * @return {string|null}
1410
+ */
1411
+ function getDimensionValueFromItem(item, def) {
1412
+ const template = def?.valueTemplate;
1413
+ if (isString(template) && template !== "") {
1414
+ return new Formatter(item).format(template);
1415
+ }
1416
+ const key = def?.key;
1417
+ if (!isString(key) || key === "") {
1418
+ return null;
1419
+ }
1420
+ try {
1421
+ return new Pathfinder(item).getVia(key);
1422
+ } catch (_e) {
1423
+ return item?.[key];
1424
+ }
1425
+ }
1426
+
1427
+ /**
1428
+ * @private
1429
+ * @param {Object} pricing
1430
+ * @param {Object} product
1431
+ * @param {Object|null} variantData
1432
+ * @param {number} qty
1433
+ * @return {{pricing: Object, product: Object}}
1434
+ */
1435
+ function resolvePricing(
1436
+ pricing,
1437
+ product,
1438
+ variantData,
1439
+ qty,
1440
+ variantPricingActive = false,
1441
+ ) {
1442
+ let nextPricing = Object.assign({}, pricing);
1443
+ let nextProduct = Object.assign({}, product);
1444
+ let priceAvailable = true;
1445
+
1446
+ if (variantPricingActive) {
1447
+ nextPricing = Object.assign({}, nextPricing, {
1448
+ price: null,
1449
+ listPrice: null,
1450
+ basePrice: null,
1451
+ basePriceUnit: null,
1452
+ tiers: [],
1453
+ });
1454
+ if (!variantData) {
1455
+ priceAvailable = false;
1456
+ }
1457
+ }
1458
+
1459
+ if (variantData) {
1460
+ const pricingKeys = getVariantPricingKeys.call(this);
1461
+ const priceKey = pricingKeys.priceKey;
1462
+ const listPriceKey = pricingKeys.listPriceKey;
1463
+ const basePriceKey = pricingKeys.basePriceKey;
1464
+ const basePriceUnitKey = pricingKeys.basePriceUnitKey;
1465
+ const currencyKey = pricingKeys.currencyKey;
1466
+ const taxRateKey = pricingKeys.taxRateKey;
1467
+ const taxIncludedKey = pricingKeys.taxIncludedKey;
1468
+ const tiersKey = pricingKeys.tiersKey;
1469
+
1470
+ const variantPrice =
1471
+ isString(priceKey) && priceKey !== "" ? variantData?.[priceKey] : null;
1472
+ const variantListPrice =
1473
+ isString(listPriceKey) && listPriceKey !== ""
1474
+ ? variantData?.[listPriceKey]
1475
+ : null;
1476
+ const variantBasePrice =
1477
+ isString(basePriceKey) && basePriceKey !== ""
1478
+ ? variantData?.[basePriceKey]
1479
+ : null;
1480
+ const variantBaseUnit =
1481
+ isString(basePriceUnitKey) && basePriceUnitKey !== ""
1482
+ ? variantData?.[basePriceUnitKey]
1483
+ : null;
1484
+ const variantCurrency =
1485
+ isString(currencyKey) && currencyKey !== ""
1486
+ ? variantData?.[currencyKey]
1487
+ : null;
1488
+ const variantTaxRate =
1489
+ isString(taxRateKey) && taxRateKey !== ""
1490
+ ? variantData?.[taxRateKey]
1491
+ : null;
1492
+ const variantTaxIncluded =
1493
+ isString(taxIncludedKey) && taxIncludedKey !== ""
1494
+ ? variantData?.[taxIncludedKey]
1495
+ : null;
1496
+ const variantTiers =
1497
+ isString(tiersKey) && tiersKey !== "" ? variantData?.[tiersKey] : null;
1498
+
1499
+ if (variantPrice !== null && variantPrice !== undefined) {
1500
+ nextPricing.price = parseNumber(variantPrice, nextPricing.price);
1501
+ }
1502
+ if (variantListPrice !== null && variantListPrice !== undefined) {
1503
+ nextPricing.listPrice = parseNumber(
1504
+ variantListPrice,
1505
+ nextPricing.listPrice,
1506
+ );
1507
+ }
1508
+ if (variantBasePrice !== null && variantBasePrice !== undefined) {
1509
+ nextPricing.basePrice = parseNumber(
1510
+ variantBasePrice,
1511
+ nextPricing.basePrice,
1512
+ );
1513
+ }
1514
+ if (variantBaseUnit !== null && variantBaseUnit !== undefined) {
1515
+ nextPricing.basePriceUnit = variantBaseUnit;
1516
+ }
1517
+ if (variantCurrency !== null && variantCurrency !== undefined) {
1518
+ nextProduct.currency = variantCurrency;
1519
+ }
1520
+ if (variantTaxRate !== null && variantTaxRate !== undefined) {
1521
+ nextProduct.taxRate = parseNumber(variantTaxRate, nextProduct.taxRate);
1522
+ }
1523
+ if (variantTaxIncluded !== null && variantTaxIncluded !== undefined) {
1524
+ nextProduct.taxIncluded = parseBoolean(
1525
+ variantTaxIncluded,
1526
+ nextProduct.taxIncluded,
1527
+ );
1528
+ }
1529
+ if (Array.isArray(variantTiers)) {
1530
+ nextPricing.tiers = variantTiers;
1531
+ }
1532
+ }
1533
+
1534
+ const tiers = Array.isArray(nextPricing.tiers)
1535
+ ? nextPricing.tiers
1536
+ : Array.isArray(this.getOption("pricing.tiers"))
1537
+ ? this.getOption("pricing.tiers")
1538
+ : [];
1539
+ if (tiers.length > 0 && Number.isFinite(qty)) {
1540
+ const tier = findTierForQuantity(tiers, qty);
1541
+ if (tier) {
1542
+ const tierResult = applyTier(tier, nextPricing);
1543
+ nextPricing = Object.assign({}, nextPricing, tierResult.pricing);
1544
+ if (tierResult.currency) {
1545
+ nextProduct.currency = tierResult.currency;
1546
+ }
1547
+ }
1548
+ }
1549
+
1550
+ if (variantPricingActive) {
1551
+ priceAvailable = Number.isFinite(nextPricing.price);
1552
+ }
1553
+
1554
+ return { pricing: nextPricing, product: nextProduct, priceAvailable };
1555
+ }
1556
+
1557
+ /**
1558
+ * @private
1559
+ * @return {Object}
1560
+ */
1561
+ function getVariantPricingKeys() {
1562
+ const defaults = {
1563
+ priceKey: "price",
1564
+ listPriceKey: "listPrice",
1565
+ basePriceKey: "basePrice",
1566
+ basePriceUnitKey: "basePriceUnit",
1567
+ currencyKey: "currency",
1568
+ taxRateKey: "taxRate",
1569
+ taxIncludedKey: "taxIncluded",
1570
+ tiersKey: "tiers",
1571
+ };
1572
+ const configured = this.getOption("variants.pricing");
1573
+ if (!isObject(configured)) {
1574
+ return defaults;
1575
+ }
1576
+ return Object.assign({}, defaults, configured);
1577
+ }
1578
+
1579
+ /**
1580
+ * @private
1581
+ * @param {Array} tiers
1582
+ * @param {number} qty
1583
+ * @return {Object|null}
1584
+ */
1585
+ function findTierForQuantity(tiers, qty) {
1586
+ let match = null;
1587
+ for (const tier of tiers) {
1588
+ const minRaw = tier?.min ?? tier?.from ?? tier?.qty ?? null;
1589
+ const maxRaw = tier?.max ?? tier?.to ?? null;
1590
+ const min = minRaw === null ? 1 : Number(minRaw);
1591
+ const max = maxRaw === null || maxRaw === undefined ? null : Number(maxRaw);
1592
+ if (!Number.isFinite(min)) {
1593
+ continue;
1594
+ }
1595
+ if (qty < min) {
1596
+ continue;
1597
+ }
1598
+ if (max !== null && Number.isFinite(max) && qty > max) {
1599
+ continue;
1600
+ }
1601
+ if (!match || min > Number(match.min ?? 0)) {
1602
+ match = Object.assign({}, tier, { min, max });
1603
+ }
1604
+ }
1605
+ return match;
1606
+ }
1607
+
1608
+ /**
1609
+ * @private
1610
+ * @param {Object} tier
1611
+ * @param {Object} fallback
1612
+ * @return {Object}
1613
+ */
1614
+ function applyTier(tier, fallback) {
1615
+ const next = Object.assign({}, fallback);
1616
+ let currency = null;
1617
+ if (tier.price !== undefined) {
1618
+ next.price = parseNumber(tier.price, next.price);
1619
+ }
1620
+ if (tier.listPrice !== undefined) {
1621
+ next.listPrice = parseNumber(tier.listPrice, next.listPrice);
1622
+ }
1623
+ if (tier.basePrice !== undefined) {
1624
+ next.basePrice = parseNumber(tier.basePrice, next.basePrice);
1625
+ }
1626
+ if (tier.basePriceUnit !== undefined) {
1627
+ next.basePriceUnit = tier.basePriceUnit;
1628
+ }
1629
+ if (tier.currency !== undefined) {
1630
+ currency = tier.currency;
1631
+ }
1632
+ return { pricing: next, currency };
1633
+ }
1634
+
1635
+ /**
1636
+ * @private
1637
+ * @param {string|null} sku
1638
+ * @return {string}
1639
+ */
1640
+ function getPricingCacheKey(sku) {
1641
+ if (isString(sku) && sku !== "") {
1642
+ return sku;
1643
+ }
1644
+ return "__base__";
1645
+ }
1646
+
1647
+ /**
1648
+ * @private
1649
+ * @param {{product: Object, pricing: Object}|null} cached
1650
+ */
1651
+ function applyPricingCache(cached) {
1652
+ if (!cached) {
1653
+ return;
1654
+ }
1655
+ this[productDataSymbol] = Object.assign({}, this[productDataSymbol], {
1656
+ product: cached.product,
1657
+ pricing: cached.pricing,
1658
+ });
1659
+ }
1660
+
1661
+ /**
1662
+ * @private
1663
+ * @param {Object} options
1664
+ * @return {boolean}
1665
+ */
1666
+ function isFixedQuantity(options) {
1667
+ const min = Number(options.min);
1668
+ const max = Number(options.max);
1669
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
1670
+ return false;
1671
+ }
1672
+ return min === 1 && max === 1;
1673
+ }
1674
+
1675
+ /**
1676
+ * @private
1677
+ * @param {HTMLElement|null} variants
1678
+ * @return {boolean}
1679
+ */
1680
+ function hasVariantOptions(variants) {
1681
+ if (!(variants instanceof HTMLElement)) {
1682
+ return false;
1683
+ }
1684
+ const data = variants.getOption?.("data") || [];
1685
+ const url = variants.getOption?.("url");
1686
+ return (
1687
+ (Array.isArray(data) && data.length > 0) || (isString(url) && url !== "")
1688
+ );
1689
+ }
1690
+
1691
+ /**
1692
+ * @private
1693
+ * @param {HTMLElement|null} quantityControl
1694
+ * @return {number|null}
1695
+ */
1696
+ function getQuantityValue(quantityControl) {
1697
+ const useDecimalInput = this.getOption("features.allowDecimal") === true;
1698
+ if (useDecimalInput) {
1699
+ const raw = this[quantityInputElementSymbol]?.value ?? "";
1700
+ const parsed = parseLocaleNumber(raw);
1701
+ if (Number.isFinite(parsed)) {
1702
+ return parsed;
1703
+ }
1704
+ }
1705
+ const fallback =
1706
+ this.getOption("quantity.value") ?? this.getOption("quantity.min") ?? 1;
1707
+ if (useDecimalInput) {
1708
+ return Number(fallback);
1709
+ }
1710
+ const value = quantityControl?.value;
1711
+ if (Number.isFinite(value)) {
1712
+ return value;
1713
+ }
1714
+ return Number(fallback);
1715
+ }
1716
+
1717
+ /**
1718
+ * @private
1719
+ * @param {number} value
1720
+ * @param {number|null} precision
1721
+ * @param {string} rounding
1722
+ * @return {number}
1723
+ */
1724
+ function roundQuantity(value, precision, rounding) {
1725
+ if (!Number.isFinite(value)) {
1726
+ return value;
1727
+ }
1728
+ if (!Number.isFinite(precision)) {
1729
+ return value;
1730
+ }
1731
+ const factor = 10 ** precision;
1732
+ const scaled = value * factor;
1733
+ if (rounding === "half-up") {
1734
+ return Math.round(scaled) / factor;
1735
+ }
1736
+ if (rounding === "floor") {
1737
+ return Math.floor(scaled) / factor;
1738
+ }
1739
+ if (rounding === "ceil") {
1740
+ return Math.ceil(scaled) / factor;
1741
+ }
1742
+ return Math.round(scaled) / factor;
1743
+ }
1744
+
1745
+ /**
1746
+ * @private
1747
+ * @param {string} value
1748
+ * @param {string} currency
1749
+ * @return {string}
1750
+ */
1751
+ function formatMoney(value, currency) {
1752
+ try {
1753
+ const locale = getLocaleOfDocument();
1754
+ return new Intl.NumberFormat(locale.locale, {
1755
+ style: "currency",
1756
+ currency,
1757
+ }).format(value);
1758
+ } catch (_e) {
1759
+ return `${value} ${currency}`;
1760
+ }
1761
+ }
1762
+
1763
+ /**
1764
+ * @private
1765
+ * @param {number} value
1766
+ * @param {number} precision
1767
+ * @return {string}
1768
+ */
1769
+ function formatQuantityForLocale(value, precision) {
1770
+ try {
1771
+ const locale = getLocaleOfDocument();
1772
+ return new Intl.NumberFormat(locale.locale, {
1773
+ minimumFractionDigits: precision,
1774
+ maximumFractionDigits: precision,
1775
+ }).format(value);
1776
+ } catch (_e) {
1777
+ return `${value}`;
1778
+ }
1779
+ }
1780
+
1781
+ /**
1782
+ * @private
1783
+ * @param {string} value
1784
+ * @return {number}
1785
+ */
1786
+ function parseLocaleNumber(value) {
1787
+ if (!isString(value)) {
1788
+ return Number.NaN;
1789
+ }
1790
+ const trimmed = value.trim();
1791
+ if (trimmed === "") {
1792
+ return Number.NaN;
1793
+ }
1794
+ const locale = getLocaleOfDocument();
1795
+ const decimal = getDecimalSeparator(locale.locale);
1796
+ const group = decimal === "," ? "." : ",";
1797
+ const normalized = trimmed
1798
+ .replace(new RegExp(`\\${group}`, "g"), "")
1799
+ .replace(new RegExp(`\\${decimal}`), ".");
1800
+ return Number(normalized);
1801
+ }
1802
+
1803
+ /**
1804
+ * @private
1805
+ * @param {string} locale
1806
+ * @return {string}
1807
+ */
1808
+ function getDecimalSeparator(locale) {
1809
+ try {
1810
+ const parts = new Intl.NumberFormat(locale).formatToParts(1.1);
1811
+ const separator = parts.find((part) => part.type === "decimal");
1812
+ return separator?.value || ".";
1813
+ } catch (_e) {
1814
+ return ".";
1815
+ }
1816
+ }
1817
+
1818
+ /**
1819
+ * @private
1820
+ */
1821
+ function updateSubmitLabel() {
1822
+ if (!this[submitLabelElementSymbol]) {
1823
+ return;
1824
+ }
1825
+ const label = this.getOption("labels.addToCart") || "Add to cart";
1826
+ this[submitLabelElementSymbol].textContent = label;
1827
+ }
1828
+
1829
+ /**
1830
+ * @private
1831
+ * @param {HTMLElement} element
1832
+ * @param {string} text
1833
+ */
1834
+ function setElementText(element, text) {
1835
+ element.textContent = text;
1836
+ element.hidden = text === "";
1837
+ }
1838
+
1839
+ /**
1840
+ * @private
1841
+ * @param {string|null} sku
1842
+ * @return {Object|null}
1843
+ */
1844
+ function findCartItem(sku) {
1845
+ if (!isString(sku) || sku === "") {
1846
+ return null;
1847
+ }
1848
+ const cart = this[cartDataSymbol];
1849
+ if (!Array.isArray(cart)) {
1850
+ return null;
1851
+ }
1852
+
1853
+ const skuTemplate = this.getOption("cart.mapping.skuTemplate");
1854
+ const qtyTemplate = this.getOption("cart.mapping.qtyTemplate");
1855
+
1856
+ for (const item of cart) {
1857
+ const itemSku =
1858
+ mapValueFromSource(item, skuTemplate) ?? item?.sku ?? item?.id ?? null;
1859
+ if (itemSku === sku) {
1860
+ const qty = parseNumber(
1861
+ mapValueFromSource(item, qtyTemplate),
1862
+ item?.qty ?? item?.quantity ?? null,
1863
+ );
1864
+ return { sku: itemSku, qty, source: item };
1865
+ }
1866
+ }
1867
+ return null;
1868
+ }
1869
+
1870
+ /**
1871
+ * @private
1872
+ */
1873
+ function checkStockIfNeeded() {
1874
+ const options = this.getOption("stock", {});
1875
+ const url = options?.url;
1876
+ if (!isString(url) || url === "") {
1877
+ return;
1878
+ }
1879
+ if (options?.features?.autoCheck === false) {
1880
+ return;
1881
+ }
1882
+
1883
+ if (this[stockRequestSymbol]) {
1884
+ this[stockRequestSymbol].abort?.();
1885
+ this[stockRequestSymbol] = null;
1886
+ }
1887
+
1888
+ const controller = new AbortController();
1889
+ this[stockRequestSymbol] = controller;
1890
+ const sku = this.shadowRoot.querySelector("monster-variant-select")?.value;
1891
+ if (!isString(sku) || sku === "") {
1892
+ return;
1893
+ }
1894
+ const query = sku ? `?sku=${encodeURIComponent(sku)}` : "";
1895
+
1896
+ fetch(url + query, {
1897
+ ...options.fetch,
1898
+ signal: controller.signal,
1899
+ })
1900
+ .then((response) => {
1901
+ if (!response.ok) {
1902
+ throw new Error("stock check failed");
1903
+ }
1904
+ return response.json();
1905
+ })
1906
+ .then((json) => {
1907
+ const selector = options?.mapping?.selector || "*";
1908
+ let data = json;
1909
+ if (selector !== "*" && selector !== "") {
1910
+ data = new Pathfinder(json).getVia(selector);
1911
+ }
1912
+ const stock = mapValueFromSource(data, options?.mapping?.stockTemplate);
1913
+ const product = this[productDataSymbol]?.product || {};
1914
+ this[productDataSymbol] = Object.assign({}, this[productDataSymbol], {
1915
+ product: Object.assign({}, product, {
1916
+ stock: parseNumber(stock, product.stock),
1917
+ }),
1918
+ });
1919
+ updateState.call(this);
1920
+ })
1921
+ .catch((e) => {
1922
+ if (e.name === "AbortError") {
1923
+ return;
1924
+ }
1925
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
1926
+ });
1927
+ }
1928
+
1929
+ /**
1930
+ * @private
1931
+ */
1932
+ function checkPricingIfNeeded() {
1933
+ const api = this.getOption("pricing.api", {});
1934
+ const url = api?.url;
1935
+ if (!isString(url) || url === "") {
1936
+ return;
1937
+ }
1938
+ if (this[pricingRequestSymbol]) {
1939
+ this[pricingRequestSymbol].abort?.();
1940
+ this[pricingRequestSymbol] = null;
1941
+ }
1942
+ const variants = this.shadowRoot.querySelector("monster-variant-select");
1943
+ const quantity = this.shadowRoot.querySelector("monster-quantity");
1944
+ const hasVariants = hasVariantOptions.call(this, variants);
1945
+ const sku = hasVariants ? variants?.value : this.getOption("product.sku");
1946
+ const qty = getQuantityValue.call(this, quantity);
1947
+ const selection = variants?.getOption?.("selection") || {};
1948
+ if (hasVariants && (!isString(sku) || sku === "")) {
1949
+ return;
1950
+ }
1951
+ const alwaysFetch = api?.features?.alwaysFetch === true;
1952
+ const cacheKey = getPricingCacheKey.call(this, sku);
1953
+ if (!alwaysFetch && cacheKey && this[pricingCacheSymbol]?.has(cacheKey)) {
1954
+ const cached = this[pricingCacheSymbol].get(cacheKey);
1955
+ applyPricingCache.call(this, cached);
1956
+ updateState.call(this);
1957
+ return;
1958
+ }
1959
+ const requestData = stringifyForTemplate({
1960
+ sku,
1961
+ qty: Number.isFinite(qty) ? qty : "",
1962
+ selection,
1963
+ });
1964
+ const requestUrl = url.includes("${")
1965
+ ? new Formatter(requestData).format(url)
1966
+ : url;
1967
+ const controller = new AbortController();
1968
+ this[pricingRequestSymbol] = controller;
1969
+
1970
+ fetch(requestUrl, { ...(api?.fetch || {}), signal: controller.signal })
1971
+ .then((response) => {
1972
+ if (!response.ok) {
1973
+ throw new Error("pricing fetch failed");
1974
+ }
1975
+ return response.json();
1976
+ })
1977
+ .then((json) => {
1978
+ const selector = api?.mapping?.selector || "*";
1979
+ let data = json;
1980
+ if (selector !== "*" && selector !== "") {
1981
+ data = new Pathfinder(json).getVia(selector);
1982
+ }
1983
+ const price = mapValueFromSource(data, api?.mapping?.priceTemplate);
1984
+ const listPrice = mapValueFromSource(
1985
+ data,
1986
+ api?.mapping?.listPriceTemplate,
1987
+ );
1988
+ const basePrice = mapValueFromSource(
1989
+ data,
1990
+ api?.mapping?.basePriceTemplate,
1991
+ );
1992
+ const baseUnit = mapValueFromSource(
1993
+ data,
1994
+ api?.mapping?.basePriceUnitTemplate,
1995
+ );
1996
+ const tiersPath = api?.mapping?.tiersPath;
1997
+ let tiers = null;
1998
+ if (tiersPath) {
1999
+ try {
2000
+ tiers = new Pathfinder(data).getVia(tiersPath);
2001
+ } catch (_e) {
2002
+ tiers = null;
2003
+ }
2004
+ }
2005
+ const taxRate = mapValueFromSource(data, api?.mapping?.taxRateTemplate);
2006
+ const taxIncluded = mapValueFromSource(
2007
+ data,
2008
+ api?.mapping?.taxIncludedTemplate,
2009
+ );
2010
+
2011
+ const current = this[productDataSymbol]?.product || {};
2012
+ const pricing = Object.assign({}, this[productDataSymbol]?.pricing, {
2013
+ price: parseNumber(price, this.getOption("pricing.price")),
2014
+ listPrice: parseNumber(listPrice, this.getOption("pricing.listPrice")),
2015
+ basePrice: parseNumber(basePrice, this.getOption("pricing.basePrice")),
2016
+ basePriceUnit: baseUnit ?? this.getOption("pricing.basePriceUnit"),
2017
+ tiers: Array.isArray(tiers) ? tiers : this.getOption("pricing.tiers"),
2018
+ });
2019
+
2020
+ const product = Object.assign({}, current, {
2021
+ taxRate: parseNumber(taxRate, current.taxRate),
2022
+ taxIncluded: parseBoolean(taxIncluded, current.taxIncluded),
2023
+ });
2024
+
2025
+ const cacheEntry = { product, pricing };
2026
+ if (cacheKey) {
2027
+ this[pricingCacheSymbol]?.set(cacheKey, cacheEntry);
2028
+ }
2029
+ applyPricingCache.call(this, cacheEntry);
2030
+ updateState.call(this);
2031
+ })
2032
+ .catch((e) => {
2033
+ if (e.name === "AbortError") {
2034
+ return;
2035
+ }
2036
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
2037
+ });
2038
+ }
2039
+
2040
+ /**
2041
+ * @private
2042
+ * @param {string|null} value
2043
+ * @param {boolean} fallback
2044
+ * @return {boolean}
2045
+ */
2046
+ function parseBoolean(value, fallback) {
2047
+ if (value === null || value === undefined) {
2048
+ return fallback;
2049
+ }
2050
+ if (typeof value === "boolean") {
2051
+ return value;
2052
+ }
2053
+ if (isString(value)) {
2054
+ return value.toLowerCase() === "true";
2055
+ }
2056
+ return fallback;
2057
+ }
2058
+
2059
+ /**
2060
+ * @private
2061
+ * @param {string|null} value
2062
+ * @param {number|null} fallback
2063
+ * @return {number|null}
2064
+ */
2065
+ function parseNumber(value, fallback) {
2066
+ if (value === null || value === undefined) {
2067
+ return fallback;
2068
+ }
2069
+ const num = Number(value);
2070
+ return Number.isFinite(num) ? num : fallback;
2071
+ }
2072
+
2073
+ /**
2074
+ * @private
2075
+ * @return {string}
2076
+ */
2077
+ function getTemplate() {
2078
+ // language=HTML
2079
+ return `
2080
+ <div data-monster-role="control" part="control">
2081
+ <div data-monster-role="header" part="header">
2082
+ <slot name="title"></slot>
2083
+ </div>
2084
+ <div data-monster-role="body" part="body">
2085
+ <div data-monster-role="variants" part="variants">
2086
+ <monster-variant-select part="variants-control"></monster-variant-select>
2087
+ </div>
2088
+ <div data-monster-role="quantity" part="quantity">
2089
+ <div data-monster-role="quantity-label" part="quantity-label">
2090
+ <span data-monster-role="quantity-label-text" part="quantity-label-text"
2091
+ data-monster-replace="path:labels.quantity"></span>
2092
+ <span data-monster-role="quantity-unit" part="quantity-unit"></span>
2093
+ </div>
2094
+ <monster-quantity part="quantity-control"></monster-quantity>
2095
+ <input data-monster-role="quantity-input" part="quantity-input"
2096
+ type="text" inputmode="decimal" />
2097
+ <div data-monster-role="quantity-static" part="quantity-static"></div>
2098
+ </div>
2099
+ <div data-monster-role="prices" part="prices">
2100
+ <div data-monster-role="list-price" part="list-price"></div>
2101
+ <div data-monster-role="price" part="price"></div>
2102
+ <div data-monster-role="sum" part="sum"></div>
2103
+ <div data-monster-role="base-price" part="base-price"></div>
2104
+ <div data-monster-role="tax" part="tax"></div>
2105
+ <div data-monster-role="total-label" part="total-label"
2106
+ data-monster-replace="path:labels.total"></div>
2107
+ <div data-monster-role="total" part="total"></div>
2108
+ </div>
2109
+ <div data-monster-role="delivery" part="delivery"></div>
2110
+ <div data-monster-role="message" part="message"></div>
2111
+ <div data-monster-role="actions" part="actions">
2112
+ <monster-message-state-button data-monster-role="submit" part="submit">
2113
+ <span data-monster-role="submit-label" part="submit-label"></span>
2114
+ </monster-message-state-button>
2115
+ </div>
2116
+ </div>
2117
+ <div data-monster-role="slots" part="slots">
2118
+ <slot name="info"></slot>
2119
+ <slot name="shipping"></slot>
2120
+ <slot name="footer"></slot>
2121
+ </div>
2122
+ </div>
2123
+ `;
2124
+ }
2125
+
2126
+ /**
2127
+ * @private
2128
+ * @returns {object}
2129
+ */
2130
+ function getTranslations() {
2131
+ const locale = getLocaleOfDocument();
2132
+ switch (locale.language) {
2133
+ case "de":
2134
+ return {
2135
+ addToCart: "In den Warenkorb",
2136
+ addToCartAria: "In den Warenkorb",
2137
+ quantity: "Menge",
2138
+ total: "Summe",
2139
+ variantsAria: "Varianten",
2140
+ quantityAria: "Menge",
2141
+ selectVariant: "Bitte eine Variante waehlen.",
2142
+ selectQuantity: "Bitte Menge waehlen.",
2143
+ priceUnavailable: "Preis nicht verfuegbar.",
2144
+ invalidQuantity: "Menge ist nicht erlaubt.",
2145
+ outOfStock: "Nicht genuegend Lagerbestand.",
2146
+ addToCartError: "Hinzufuegen fehlgeschlagen.",
2147
+ addToCartNotConfigured: "Warenkorb ist nicht konfiguriert.",
2148
+ taxIncluded: "inkl. MwSt.",
2149
+ taxExcluded: "zzgl. MwSt.",
2150
+ deliveryUnknown: "Lieferzeit unbekannt",
2151
+ };
2152
+ case "es":
2153
+ return {
2154
+ addToCart: "Anadir al carrito",
2155
+ addToCartAria: "Anadir al carrito",
2156
+ quantity: "Cantidad",
2157
+ total: "Total",
2158
+ variantsAria: "Variantes",
2159
+ quantityAria: "Cantidad",
2160
+ selectVariant: "Seleccione una variante.",
2161
+ selectQuantity: "Seleccione una cantidad.",
2162
+ priceUnavailable: "Precio no disponible.",
2163
+ invalidQuantity: "Cantidad no permitida.",
2164
+ outOfStock: "Stock insuficiente.",
2165
+ addToCartError: "Error al anadir.",
2166
+ addToCartNotConfigured: "Carrito no configurado.",
2167
+ taxIncluded: "incl. impuestos",
2168
+ taxExcluded: "excl. impuestos",
2169
+ deliveryUnknown: "Plazo desconocido",
2170
+ };
2171
+ case "zh":
2172
+ return {
2173
+ addToCart: "加入购物车",
2174
+ addToCartAria: "加入购物车",
2175
+ quantity: "数量",
2176
+ total: "总计",
2177
+ variantsAria: "变体选择",
2178
+ quantityAria: "数量",
2179
+ selectVariant: "请选择一个变体。",
2180
+ selectQuantity: "请选择数量。",
2181
+ priceUnavailable: "价格不可用。",
2182
+ invalidQuantity: "数量无效。",
2183
+ outOfStock: "库存不足。",
2184
+ addToCartError: "添加失败。",
2185
+ addToCartNotConfigured: "购物车未配置。",
2186
+ taxIncluded: "含税",
2187
+ taxExcluded: "不含税",
2188
+ deliveryUnknown: "交付时间未知",
2189
+ };
2190
+ case "hi":
2191
+ return {
2192
+ addToCart: "कार्ट में जोड़ें",
2193
+ addToCartAria: "कार्ट में जोड़ें",
2194
+ quantity: "मात्रा",
2195
+ total: "कुल",
2196
+ variantsAria: "विकल्प",
2197
+ quantityAria: "मात्रा",
2198
+ selectVariant: "कृपया एक विकल्प चुनें।",
2199
+ selectQuantity: "कृपया मात्रा चुनें।",
2200
+ priceUnavailable: "मूल्य उपलब्ध नहीं।",
2201
+ invalidQuantity: "मात्रा अनुमत नहीं है।",
2202
+ outOfStock: "स्टॉक पर्याप्त नहीं है।",
2203
+ addToCartError: "जोड़ने में त्रुटि।",
2204
+ addToCartNotConfigured: "कार्ट कॉन्फ़िगर नहीं है।",
2205
+ taxIncluded: "कर सहित",
2206
+ taxExcluded: "कर रहित",
2207
+ deliveryUnknown: "डिलीवरी समय अज्ञात",
2208
+ };
2209
+ case "bn":
2210
+ return {
2211
+ addToCart: "কার্টে যোগ করুন",
2212
+ addToCartAria: "কার্টে যোগ করুন",
2213
+ quantity: "পরিমাণ",
2214
+ total: "মোট",
2215
+ variantsAria: "ভ্যারিয়েন্ট",
2216
+ quantityAria: "পরিমাণ",
2217
+ selectVariant: "দয়া করে একটি ভ্যারিয়েন্ট বেছে নিন।",
2218
+ selectQuantity: "দয়া করে পরিমাণ বেছে নিন।",
2219
+ priceUnavailable: "মূল্য পাওয়া যাচ্ছে না।",
2220
+ invalidQuantity: "পরিমাণ অনুমোদিত নয়।",
2221
+ outOfStock: "স্টক পর্যাপ্ত নয়।",
2222
+ addToCartError: "যোগ করতে ব্যর্থ।",
2223
+ addToCartNotConfigured: "কার্ট কনফিগার করা হয়নি।",
2224
+ taxIncluded: "করসহ",
2225
+ taxExcluded: "কর ছাড়া",
2226
+ deliveryUnknown: "ডেলিভারি সময় অজানা",
2227
+ };
2228
+ case "pt":
2229
+ return {
2230
+ addToCart: "Adicionar ao carrinho",
2231
+ addToCartAria: "Adicionar ao carrinho",
2232
+ quantity: "Quantidade",
2233
+ total: "Total",
2234
+ variantsAria: "Variantes",
2235
+ quantityAria: "Quantidade",
2236
+ selectVariant: "Escolha uma variante.",
2237
+ selectQuantity: "Escolha uma quantidade.",
2238
+ priceUnavailable: "Preco indisponivel.",
2239
+ invalidQuantity: "Quantidade nao permitida.",
2240
+ outOfStock: "Estoque insuficiente.",
2241
+ addToCartError: "Falha ao adicionar.",
2242
+ addToCartNotConfigured: "Carrinho nao configurado.",
2243
+ taxIncluded: "incl. imposto",
2244
+ taxExcluded: "excl. imposto",
2245
+ deliveryUnknown: "Prazo desconhecido",
2246
+ };
2247
+ case "ru":
2248
+ return {
2249
+ addToCart: "V korzinu",
2250
+ addToCartAria: "V korzinu",
2251
+ quantity: "Kolichestvo",
2252
+ total: "Itogo",
2253
+ variantsAria: "Varianty",
2254
+ quantityAria: "Kolichestvo",
2255
+ selectVariant: "Vyberite variant.",
2256
+ selectQuantity: "Vyberite kolichestvo.",
2257
+ priceUnavailable: "Tsena nedostupna.",
2258
+ invalidQuantity: "Nedopustimoe kol-vo.",
2259
+ outOfStock: "Nedostatochno na sklade.",
2260
+ addToCartError: "Ne udalos dobavit.",
2261
+ addToCartNotConfigured: "Korzina ne nastroena.",
2262
+ taxIncluded: "s NDS",
2263
+ taxExcluded: "bez NDS",
2264
+ deliveryUnknown: "Srok neizvesten",
2265
+ };
2266
+ case "ja":
2267
+ return {
2268
+ addToCart: "カートに追加",
2269
+ addToCartAria: "カートに追加",
2270
+ quantity: "数量",
2271
+ total: "合計",
2272
+ variantsAria: "バリエーション",
2273
+ quantityAria: "数量",
2274
+ selectVariant: "バリエーションを選択してください。",
2275
+ selectQuantity: "数量を選択してください。",
2276
+ priceUnavailable: "価格が利用できません。",
2277
+ invalidQuantity: "数量が無効です。",
2278
+ outOfStock: "在庫不足。",
2279
+ addToCartError: "追加に失敗。",
2280
+ addToCartNotConfigured: "カートが未設定です。",
2281
+ taxIncluded: "税込",
2282
+ taxExcluded: "税別",
2283
+ deliveryUnknown: "配送情報なし",
2284
+ };
2285
+ case "pa":
2286
+ return {
2287
+ addToCart: "ਕਾਰਟ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ",
2288
+ addToCartAria: "ਕਾਰਟ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ",
2289
+ quantity: "ਮਾਤਰਾ",
2290
+ total: "ਕੁੱਲ",
2291
+ variantsAria: "ਵੈਰੀਐਂਟ",
2292
+ quantityAria: "ਮਾਤਰਾ",
2293
+ selectVariant: "ਕਿਰਪਾ ਕਰਕੇ ਇਕ ਵੈਰੀਐਂਟ ਚੁਣੋ।",
2294
+ selectQuantity: "ਕਿਰਪਾ ਕਰਕੇ ਮਾਤਰਾ ਚੁਣੋ।",
2295
+ priceUnavailable: "ਕੀਮਤ ਉਪਲਬਧ ਨਹੀਂ।",
2296
+ invalidQuantity: "ਮਾਤਰਾ ਮਨਜ਼ੂਰ ਨਹੀਂ।",
2297
+ outOfStock: "ਸਟਾਕ ਘੱਟ ਹੈ।",
2298
+ addToCartError: "ਜੋੜਨਾ ਅਸਫਲ।",
2299
+ addToCartNotConfigured: "ਕਾਰਟ ਕਨਫਿਗਰ ਨਹੀਂ ਹੈ।",
2300
+ taxIncluded: "ਟੈਕਸ ਸਮੇਤ",
2301
+ taxExcluded: "ਟੈਕਸ ਤੋਂ ਬਿਨਾਂ",
2302
+ deliveryUnknown: "ਡਿਲਿਵਰੀ ਸਮਾਂ ਅਣਜਾਣ",
2303
+ };
2304
+ case "mr":
2305
+ return {
2306
+ addToCart: "कार्टमध्ये जोडा",
2307
+ addToCartAria: "कार्टमध्ये जोडा",
2308
+ quantity: "प्रमाण",
2309
+ total: "एकूण",
2310
+ variantsAria: "पर्याय",
2311
+ quantityAria: "प्रमाण",
2312
+ selectVariant: "कृपया एक पर्याय निवडा.",
2313
+ selectQuantity: "कृपया प्रमाण निवडा.",
2314
+ priceUnavailable: "किंमत उपलब्ध नाही.",
2315
+ invalidQuantity: "प्रमाण अनुमती नाही.",
2316
+ outOfStock: "साठा अपुरा आहे.",
2317
+ addToCartError: "जोडण्यात त्रुटी.",
2318
+ addToCartNotConfigured: "कार्ट संरचित नाही.",
2319
+ taxIncluded: "करासह",
2320
+ taxExcluded: "कराशिवाय",
2321
+ deliveryUnknown: "डिलिव्हरी वेळ अज्ञात",
2322
+ };
2323
+ case "fr":
2324
+ return {
2325
+ addToCart: "Ajouter au panier",
2326
+ addToCartAria: "Ajouter au panier",
2327
+ quantity: "Quantite",
2328
+ total: "Total",
2329
+ variantsAria: "Variantes",
2330
+ quantityAria: "Quantite",
2331
+ selectVariant: "Veuillez choisir une variante.",
2332
+ selectQuantity: "Veuillez choisir une quantite.",
2333
+ priceUnavailable: "Prix indisponible.",
2334
+ invalidQuantity: "Quantite non autorisee.",
2335
+ outOfStock: "Stock insuffisant.",
2336
+ addToCartError: "Ajout echoue.",
2337
+ addToCartNotConfigured: "Panier non configure.",
2338
+ taxIncluded: "TTC",
2339
+ taxExcluded: "HT",
2340
+ deliveryUnknown: "Delai inconnu",
2341
+ };
2342
+ case "it":
2343
+ return {
2344
+ addToCart: "Aggiungi al carrello",
2345
+ addToCartAria: "Aggiungi al carrello",
2346
+ quantity: "Quantita",
2347
+ total: "Totale",
2348
+ variantsAria: "Varianti",
2349
+ quantityAria: "Quantita",
2350
+ selectVariant: "Seleziona una variante.",
2351
+ selectQuantity: "Seleziona una quantita.",
2352
+ priceUnavailable: "Prezzo non disponibile.",
2353
+ invalidQuantity: "Quantita non consentita.",
2354
+ outOfStock: "Scorte insufficienti.",
2355
+ addToCartError: "Aggiunta fallita.",
2356
+ addToCartNotConfigured: "Carrello non configurato.",
2357
+ taxIncluded: "IVA inclusa",
2358
+ taxExcluded: "IVA esclusa",
2359
+ deliveryUnknown: "Tempi sconosciuti",
2360
+ };
2361
+ case "nl":
2362
+ return {
2363
+ addToCart: "In winkelwagen",
2364
+ addToCartAria: "In winkelwagen",
2365
+ quantity: "Aantal",
2366
+ total: "Totaal",
2367
+ variantsAria: "Varianten",
2368
+ quantityAria: "Aantal",
2369
+ selectVariant: "Kies een variant.",
2370
+ selectQuantity: "Kies een hoeveelheid.",
2371
+ priceUnavailable: "Prijs niet beschikbaar.",
2372
+ invalidQuantity: "Hoeveelheid niet toegestaan.",
2373
+ outOfStock: "Onvoldoende voorraad.",
2374
+ addToCartError: "Toevoegen mislukt.",
2375
+ addToCartNotConfigured: "Winkelwagen niet geconfigureerd.",
2376
+ taxIncluded: "incl. btw",
2377
+ taxExcluded: "excl. btw",
2378
+ deliveryUnknown: "Levertijd onbekend",
2379
+ };
2380
+ case "sv":
2381
+ return {
2382
+ addToCart: "Lag i kundvagn",
2383
+ addToCartAria: "Lag i kundvagn",
2384
+ quantity: "Antal",
2385
+ total: "Summa",
2386
+ variantsAria: "Varianter",
2387
+ quantityAria: "Antal",
2388
+ selectVariant: "Valj en variant.",
2389
+ selectQuantity: "Valj antal.",
2390
+ priceUnavailable: "Pris ej tillgangligt.",
2391
+ invalidQuantity: "Antal ej tillatet.",
2392
+ outOfStock: "Otillrackligt lager.",
2393
+ addToCartError: "Kunde inte lagga till.",
2394
+ addToCartNotConfigured: "Kundvagn ej konfigurerad.",
2395
+ taxIncluded: "inkl. moms",
2396
+ taxExcluded: "exkl. moms",
2397
+ deliveryUnknown: "Leverans okand",
2398
+ };
2399
+ case "pl":
2400
+ return {
2401
+ addToCart: "Dodaj do koszyka",
2402
+ addToCartAria: "Dodaj do koszyka",
2403
+ quantity: "Ilosc",
2404
+ total: "Suma",
2405
+ variantsAria: "Warianty",
2406
+ quantityAria: "Ilosc",
2407
+ selectVariant: "Wybierz wariant.",
2408
+ selectQuantity: "Wybierz ilosc.",
2409
+ priceUnavailable: "Cena niedostepna.",
2410
+ invalidQuantity: "Niepoprawna ilosc.",
2411
+ outOfStock: "Brak na stanie.",
2412
+ addToCartError: "Nie udalo sie dodac.",
2413
+ addToCartNotConfigured: "Koszyk nie skonfigurowany.",
2414
+ taxIncluded: "z VAT",
2415
+ taxExcluded: "bez VAT",
2416
+ deliveryUnknown: "Czas dostawy nieznany",
2417
+ };
2418
+ case "da":
2419
+ return {
2420
+ addToCart: "Laeg i kurv",
2421
+ addToCartAria: "Laeg i kurv",
2422
+ quantity: "Maengde",
2423
+ total: "Total",
2424
+ variantsAria: "Varianter",
2425
+ quantityAria: "Maengde",
2426
+ selectVariant: "Vaelg en variant.",
2427
+ selectQuantity: "Vaelg maengde.",
2428
+ priceUnavailable: "Pris ikke tilgaengelig.",
2429
+ invalidQuantity: "Maengde ikke tilladt.",
2430
+ outOfStock: "Ikke nok lager.",
2431
+ addToCartError: "Kunne ikke tilfoeje.",
2432
+ addToCartNotConfigured: "Kurv ikke konfigureret.",
2433
+ taxIncluded: "inkl. moms",
2434
+ taxExcluded: "ekskl. moms",
2435
+ deliveryUnknown: "Leveringstid ukendt",
2436
+ };
2437
+ case "fi":
2438
+ return {
2439
+ addToCart: "Lisaa koriin",
2440
+ addToCartAria: "Lisaa koriin",
2441
+ quantity: "Maara",
2442
+ total: "Yhteensa",
2443
+ variantsAria: "Vaihtoehdot",
2444
+ quantityAria: "Maara",
2445
+ selectVariant: "Valitse vaihtoehto.",
2446
+ selectQuantity: "Valitse maara.",
2447
+ priceUnavailable: "Hinta ei saatavilla.",
2448
+ invalidQuantity: "Maara ei sallittu.",
2449
+ outOfStock: "Varasto ei riita.",
2450
+ addToCartError: "Lisaaminen epaonnistui.",
2451
+ addToCartNotConfigured: "Ostoskori ei ole konfiguroitu.",
2452
+ taxIncluded: "sis. alv",
2453
+ taxExcluded: "ei sis. alv",
2454
+ deliveryUnknown: "Toimitusaika tuntematon",
2455
+ };
2456
+ case "no":
2457
+ return {
2458
+ addToCart: "Legg i handlekurv",
2459
+ addToCartAria: "Legg i handlekurv",
2460
+ quantity: "Antall",
2461
+ total: "Sum",
2462
+ variantsAria: "Varianter",
2463
+ quantityAria: "Antall",
2464
+ selectVariant: "Velg en variant.",
2465
+ selectQuantity: "Velg antall.",
2466
+ priceUnavailable: "Pris ikke tilgjengelig.",
2467
+ invalidQuantity: "Mengde ikke tillatt.",
2468
+ outOfStock: "Ikke nok lager.",
2469
+ addToCartError: "Kunne ikke legge til.",
2470
+ addToCartNotConfigured: "Handlekurv ikke konfigurert.",
2471
+ taxIncluded: "inkl. mva",
2472
+ taxExcluded: "ekskl. mva",
2473
+ deliveryUnknown: "Leveringstid ukjent",
2474
+ };
2475
+ case "cs":
2476
+ return {
2477
+ addToCart: "Pridat do kosiku",
2478
+ addToCartAria: "Pridat do kosiku",
2479
+ quantity: "Mnozstvi",
2480
+ total: "Celkem",
2481
+ variantsAria: "Varianty",
2482
+ quantityAria: "Mnozstvi",
2483
+ selectVariant: "Vyberte variantu.",
2484
+ selectQuantity: "Vyberte mnozstvi.",
2485
+ priceUnavailable: "Cena neni dostupna.",
2486
+ invalidQuantity: "Neplatne mnozstvi.",
2487
+ outOfStock: "Nedostatek na sklade.",
2488
+ addToCartError: "Nelze pridat.",
2489
+ addToCartNotConfigured: "Kosik neni nakonfigurovan.",
2490
+ taxIncluded: "vc. DPH",
2491
+ taxExcluded: "bez DPH",
2492
+ deliveryUnknown: "Dodaci lhuta neznama",
2493
+ };
2494
+ default:
2495
+ return {
2496
+ addToCart: "Add to cart",
2497
+ addToCartAria: "Add to cart",
2498
+ quantity: "Quantity",
2499
+ total: "Total",
2500
+ variantsAria: "Variants",
2501
+ quantityAria: "Quantity",
2502
+ selectVariant: "Please select a variant.",
2503
+ selectQuantity: "Please choose a quantity.",
2504
+ priceUnavailable: "Price unavailable.",
2505
+ invalidQuantity: "Quantity is not allowed.",
2506
+ outOfStock: "Not enough stock.",
2507
+ addToCartError: "Failed to add to cart.",
2508
+ addToCartNotConfigured: "Cart is not configured.",
2509
+ taxIncluded: "incl. tax",
2510
+ taxExcluded: "excl. tax",
2511
+ deliveryUnknown: "Delivery unknown",
2512
+ };
2513
+ }
2514
+ }
2515
+
2516
+ registerCustomElement(BuyBox);