@vio-live/web-sdk 0.28.0 → 0.32.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 +243 -2
- package/README.md +175 -13
- package/dist/{chunk-Q2NALS7P.js → chunk-C5Q6W4B2.js} +1453 -199
- package/dist/chunk-C5Q6W4B2.js.map +1 -0
- package/dist/{chunk-AFJ5M53F.js → chunk-IR4XYTZL.js} +707 -92
- package/dist/chunk-IR4XYTZL.js.map +1 -0
- package/dist/chunk-QGJUBVH3.js +3 -0
- package/dist/{chunk-67QESEV2.js.map → chunk-QGJUBVH3.js.map} +1 -1
- package/dist/core/index.d.ts +226 -6
- package/dist/core/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +3 -3
- package/dist/{tokens-y0KzN7bP.d.ts → tokens-D1DCVzOf.d.ts} +331 -13
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-67QESEV2.js +0 -3
- package/dist/chunk-AFJ5M53F.js.map +0 -1
- package/dist/chunk-Q2NALS7P.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-IR4XYTZL.js';
|
|
2
2
|
import { LitElement, css, html, nothing } from 'lit';
|
|
3
3
|
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
@@ -164,6 +164,13 @@ var THEME_KEY_TO_CSS_VAR = {
|
|
|
164
164
|
detailImageZoom: "--vio-detail-image-zoom",
|
|
165
165
|
detailImageInset: "--vio-detail-image-inset",
|
|
166
166
|
detailImageBg: "--vio-detail-image-bg",
|
|
167
|
+
detailGallery: "--vio-detail-gallery",
|
|
168
|
+
carouselLayout: "--vio-carousel-layout",
|
|
169
|
+
detailCarouselPeek: "--vio-detail-carousel-peek",
|
|
170
|
+
detailDescriptionMode: "--vio-detail-description-mode",
|
|
171
|
+
detailDescriptionPreviewLines: "--vio-detail-description-preview-lines",
|
|
172
|
+
detailHighlightsDisplay: "--vio-detail-highlights-display",
|
|
173
|
+
detailShippingReturnsDisplay: "--vio-detail-shipping-returns-display",
|
|
167
174
|
detailThumbSize: "--vio-detail-thumb-size",
|
|
168
175
|
detailThumbGap: "--vio-detail-thumb-gap",
|
|
169
176
|
detailThumbRadius: "--vio-detail-thumb-radius",
|
|
@@ -176,6 +183,18 @@ var THEME_KEY_TO_CSS_VAR = {
|
|
|
176
183
|
detailButtonRadius: "--vio-detail-button-radius",
|
|
177
184
|
detailBuyPosition: "--vio-detail-buy-position",
|
|
178
185
|
detailBuyShadow: "--vio-detail-buy-shadow",
|
|
186
|
+
cardFont: "--vio-card-font",
|
|
187
|
+
cardBorder: "--vio-card-border",
|
|
188
|
+
cardImageAspect: "--vio-card-image-aspect",
|
|
189
|
+
cardImageFit: "--vio-card-image-fit",
|
|
190
|
+
cardImageBg: "--vio-card-image-bg",
|
|
191
|
+
cardImagePadding: "--vio-card-image-padding",
|
|
192
|
+
cardMetaPadding: "--vio-card-meta-padding",
|
|
193
|
+
cardPriceFont: "--vio-card-price-font",
|
|
194
|
+
cardAccent: "--vio-card-accent",
|
|
195
|
+
cardBarRadius: "--vio-card-bar-radius",
|
|
196
|
+
cardDescriptionLines: "--vio-card-description-lines",
|
|
197
|
+
cardRetailerOrder: "--vio-card-retailer-order",
|
|
179
198
|
cartWidth: "--vio-cart-width",
|
|
180
199
|
checkoutWidth: "--vio-checkout-width",
|
|
181
200
|
confirmLogoHeight: "--vio-confirm-logo-height",
|
|
@@ -359,6 +378,10 @@ var VioProduct = class extends LitElement {
|
|
|
359
378
|
this.brand = "";
|
|
360
379
|
this.name = "";
|
|
361
380
|
this.price = "";
|
|
381
|
+
this.description = "";
|
|
382
|
+
this.showDescription = false;
|
|
383
|
+
this.buttonStyle = "icon";
|
|
384
|
+
this.buttonLabel = "Kj\xF8p";
|
|
362
385
|
this.retailer = "";
|
|
363
386
|
this.imageUrl = "";
|
|
364
387
|
this.productId = "";
|
|
@@ -427,20 +450,29 @@ var VioProduct = class extends LitElement {
|
|
|
427
450
|
box-sizing: border-box;
|
|
428
451
|
width: 100%;
|
|
429
452
|
cursor: pointer;
|
|
430
|
-
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
453
|
+
font-family: var(--vio-card-font, var(--vio-font-sans, -apple-system, sans-serif));
|
|
431
454
|
color: var(--vio-color-text, #0a0a0a);
|
|
432
455
|
-webkit-tap-highlight-color: transparent;
|
|
433
456
|
}
|
|
457
|
+
/* Every --vio-card-* below defaults to the look the card always had; a
|
|
458
|
+
host restyles the card with them (they cross the carousel's shadow
|
|
459
|
+
root, ::part would not). E.g. a framed card with a buy button:
|
|
460
|
+
--vio-card-border, --vio-card-meta-padding, --vio-card-image-fit: contain. */
|
|
434
461
|
.card {
|
|
435
462
|
display: flex;
|
|
436
463
|
flex-direction: column;
|
|
437
464
|
height: 100%;
|
|
465
|
+
box-sizing: border-box;
|
|
466
|
+
border: var(--vio-card-border, none);
|
|
467
|
+
background: var(--vio-card-bg, transparent);
|
|
468
|
+
transition: border-color 0.2s ease;
|
|
438
469
|
}
|
|
470
|
+
:host(:hover) .card { border-color: var(--vio-card-border-hover-color, var(--vio-color-border, #e5e5e5)); }
|
|
439
471
|
.image {
|
|
440
472
|
position: relative;
|
|
441
|
-
aspect-ratio: 1 / 1;
|
|
473
|
+
aspect-ratio: var(--vio-card-image-aspect, 1 / 1);
|
|
442
474
|
width: 100%;
|
|
443
|
-
background-color: var(--vio-color-surface-muted, #f2f2f2);
|
|
475
|
+
background-color: var(--vio-card-image-bg, var(--vio-color-surface-muted, #f2f2f2));
|
|
444
476
|
display: flex;
|
|
445
477
|
align-items: center;
|
|
446
478
|
justify-content: center;
|
|
@@ -455,8 +487,8 @@ var VioProduct = class extends LitElement {
|
|
|
455
487
|
position: absolute;
|
|
456
488
|
right: 10px;
|
|
457
489
|
bottom: 10px;
|
|
458
|
-
width: 40px;
|
|
459
|
-
height: 40px;
|
|
490
|
+
width: var(--vio-card-add-size, 40px);
|
|
491
|
+
height: var(--vio-card-add-size, 40px);
|
|
460
492
|
border-radius: 50%;
|
|
461
493
|
background: rgba(255, 255, 255, 0.96);
|
|
462
494
|
border: none;
|
|
@@ -476,7 +508,7 @@ var VioProduct = class extends LitElement {
|
|
|
476
508
|
.add-btn.in-cart { background: #0a0a0a; color: #fff; }
|
|
477
509
|
.add-btn.in-cart:hover { background: #222; }
|
|
478
510
|
.add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
479
|
-
.add-btn svg { width: 18px; height: 18px; }
|
|
511
|
+
.add-btn svg { width: var(--vio-card-add-icon-size, 18px); height: var(--vio-card-add-icon-size, 18px); }
|
|
480
512
|
.add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
|
|
481
513
|
@keyframes vio-add-pop {
|
|
482
514
|
0% { transform: scale(1); }
|
|
@@ -487,47 +519,87 @@ var VioProduct = class extends LitElement {
|
|
|
487
519
|
.image img {
|
|
488
520
|
width: 100%;
|
|
489
521
|
height: 100%;
|
|
490
|
-
|
|
522
|
+
box-sizing: border-box;
|
|
523
|
+
padding: var(--vio-card-image-padding, 0);
|
|
524
|
+
object-fit: var(--vio-card-image-fit, cover);
|
|
491
525
|
display: block;
|
|
492
526
|
}
|
|
493
527
|
.meta {
|
|
494
|
-
padding
|
|
528
|
+
padding: var(--vio-card-meta-padding, 14px 0 0);
|
|
495
529
|
display: flex;
|
|
496
530
|
flex-direction: column;
|
|
497
531
|
flex: 1;
|
|
498
532
|
}
|
|
499
533
|
.brand {
|
|
500
|
-
font-size: var(--vio-size-xs, 12px);
|
|
501
|
-
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
534
|
+
font-size: var(--vio-card-brand-size, var(--vio-size-xs, 12px));
|
|
535
|
+
letter-spacing: var(--vio-card-brand-tracking, var(--vio-tracking-label, 0.1em));
|
|
502
536
|
text-transform: uppercase;
|
|
503
|
-
color: var(--vio-color-text-secondary, #666);
|
|
504
|
-
font-weight: 500;
|
|
505
|
-
margin-bottom: 6px;
|
|
537
|
+
color: var(--vio-card-brand-color, var(--vio-color-text-secondary, #666));
|
|
538
|
+
font-weight: var(--vio-card-brand-weight, 500);
|
|
539
|
+
margin-bottom: var(--vio-card-brand-gap, 6px);
|
|
506
540
|
}
|
|
507
541
|
.name {
|
|
508
|
-
font-
|
|
509
|
-
font-
|
|
542
|
+
font-family: var(--vio-card-name-font, inherit);
|
|
543
|
+
font-size: var(--vio-card-name-size, var(--vio-size-md, 16px));
|
|
544
|
+
font-weight: var(--vio-card-name-weight, 500);
|
|
510
545
|
line-height: 1.3;
|
|
511
|
-
margin: 0 0 10px;
|
|
546
|
+
margin: 0 0 var(--vio-card-name-gap, 10px);
|
|
512
547
|
display: -webkit-box;
|
|
513
548
|
-webkit-line-clamp: 2;
|
|
514
549
|
line-clamp: 2;
|
|
515
550
|
-webkit-box-orient: vertical;
|
|
516
551
|
overflow: hidden;
|
|
517
552
|
}
|
|
553
|
+
/* show-description: cut after --vio-card-description-lines (4). */
|
|
554
|
+
.description {
|
|
555
|
+
display: -webkit-box;
|
|
556
|
+
-webkit-line-clamp: var(--vio-card-description-lines, 4);
|
|
557
|
+
line-clamp: var(--vio-card-description-lines, 4);
|
|
558
|
+
-webkit-box-orient: vertical;
|
|
559
|
+
overflow: hidden;
|
|
560
|
+
font-size: var(--vio-card-description-size, 14px);
|
|
561
|
+
line-height: 1.45;
|
|
562
|
+
color: var(--vio-card-description-color, var(--vio-color-text-secondary, #737373));
|
|
563
|
+
margin: 0 0 var(--vio-card-description-gap, 12px);
|
|
564
|
+
}
|
|
518
565
|
.price {
|
|
519
|
-
font-
|
|
520
|
-
font-
|
|
521
|
-
|
|
566
|
+
font-family: var(--vio-card-price-font, inherit);
|
|
567
|
+
font-size: var(--vio-card-price-size, var(--vio-size-md, 16px));
|
|
568
|
+
font-weight: var(--vio-card-price-weight, 700);
|
|
569
|
+
margin-bottom: var(--vio-card-price-gap, 16px);
|
|
522
570
|
}
|
|
571
|
+
/* Who sells it. --vio-card-retailer-order: -1 lifts it above the name
|
|
572
|
+
(where a brand line goes), for catalogues without a brand of their own. */
|
|
523
573
|
.retailer {
|
|
524
|
-
|
|
525
|
-
|
|
574
|
+
display: var(--vio-card-retailer-display, block);
|
|
575
|
+
order: var(--vio-card-retailer-order, 0);
|
|
576
|
+
font-size: var(--vio-card-retailer-size, var(--vio-size-xs, 12px));
|
|
577
|
+
letter-spacing: var(--vio-card-retailer-tracking, var(--vio-tracking-label, 0.1em));
|
|
526
578
|
text-transform: uppercase;
|
|
527
|
-
color: var(--vio-color-text-tertiary, #999);
|
|
528
|
-
font-weight: 500;
|
|
579
|
+
color: var(--vio-card-retailer-color, var(--vio-color-text-tertiary, #999));
|
|
580
|
+
font-weight: var(--vio-card-retailer-weight, 500);
|
|
581
|
+
margin: var(--vio-card-retailer-margin, auto 0 0);
|
|
582
|
+
}
|
|
583
|
+
/* button-style="bar": the Vev card's bar — full width under the price,
|
|
584
|
+
uppercase, the accent colour, 6px corners (--vio-card-bar-radius). */
|
|
585
|
+
.bar-btn {
|
|
586
|
+
width: 100%;
|
|
529
587
|
margin-top: auto;
|
|
588
|
+
padding: 14px;
|
|
589
|
+
border: none;
|
|
590
|
+
border-radius: var(--vio-card-bar-radius, 6px);
|
|
591
|
+
background: var(--vio-card-accent, var(--vio-color-text, #0a0a0a));
|
|
592
|
+
color: var(--vio-card-accent-text, #fff);
|
|
593
|
+
font-family: inherit;
|
|
594
|
+
font-size: 13px;
|
|
595
|
+
font-weight: 600;
|
|
596
|
+
letter-spacing: 0.08em;
|
|
597
|
+
text-transform: uppercase;
|
|
598
|
+
cursor: pointer;
|
|
599
|
+
transition: opacity 0.15s ease;
|
|
530
600
|
}
|
|
601
|
+
.bar-btn:hover { opacity: 0.88; }
|
|
602
|
+
.bar-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
531
603
|
`;
|
|
532
604
|
}
|
|
533
605
|
connectedCallback() {
|
|
@@ -644,7 +716,7 @@ var VioProduct = class extends LitElement {
|
|
|
644
716
|
>
|
|
645
717
|
<div class="image">
|
|
646
718
|
${this.imageUrl ? html`<img src=${this.imageUrl} alt=${this.name || this.brand} loading="lazy" />` : ""}
|
|
647
|
-
|
|
719
|
+
${this.buttonStyle === "icon" ? html`<button
|
|
648
720
|
class="add-btn ${this.inCart ? "in-cart" : ""} ${this.justAdded ? "pop" : ""}"
|
|
649
721
|
@click=${this.onAdd}
|
|
650
722
|
aria-label=${this.inCart ? "I handlekurven \u2013 legg til en til" : "Legg i kurv"}
|
|
@@ -657,14 +729,18 @@ var VioProduct = class extends LitElement {
|
|
|
657
729
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
658
730
|
<path d="M16 10a4 4 0 0 1-8 0"></path>
|
|
659
731
|
</svg>`}
|
|
660
|
-
</button
|
|
732
|
+
</button>` : ""}
|
|
661
733
|
</div>
|
|
662
734
|
${this.hideMeta ? "" : html`
|
|
663
735
|
<div class="meta">
|
|
664
736
|
${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
|
|
665
737
|
${this.name ? html`<div class="name">${this.name}</div>` : ""}
|
|
738
|
+
${this.showDescription && this.description ? html`<p class="description">${this.description}</p>` : ""}
|
|
666
739
|
${this.price ? html`<div class="price">${this.price}</div>` : ""}
|
|
667
740
|
${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
|
|
741
|
+
${this.buttonStyle === "bar" ? html`<button class="bar-btn ${this.justAdded ? "pop" : ""}" @click=${this.onAdd}>
|
|
742
|
+
${this.inCart ? `${this.buttonLabel} \xB7 ${this.cartQty}` : this.buttonLabel}
|
|
743
|
+
</button>` : ""}
|
|
668
744
|
</div>`}
|
|
669
745
|
</div>
|
|
670
746
|
`;
|
|
@@ -679,6 +755,18 @@ __decorateClass([
|
|
|
679
755
|
__decorateClass([
|
|
680
756
|
property({ type: String })
|
|
681
757
|
], VioProduct.prototype, "price", 2);
|
|
758
|
+
__decorateClass([
|
|
759
|
+
property({ type: String })
|
|
760
|
+
], VioProduct.prototype, "description", 2);
|
|
761
|
+
__decorateClass([
|
|
762
|
+
property({ type: Boolean, attribute: "show-description" })
|
|
763
|
+
], VioProduct.prototype, "showDescription", 2);
|
|
764
|
+
__decorateClass([
|
|
765
|
+
property({ type: String, attribute: "button-style" })
|
|
766
|
+
], VioProduct.prototype, "buttonStyle", 2);
|
|
767
|
+
__decorateClass([
|
|
768
|
+
property({ type: String, attribute: "button-label" })
|
|
769
|
+
], VioProduct.prototype, "buttonLabel", 2);
|
|
682
770
|
__decorateClass([
|
|
683
771
|
property({ type: String })
|
|
684
772
|
], VioProduct.prototype, "retailer", 2);
|
|
@@ -797,6 +885,12 @@ var VioProductCarousel = class extends LitElement {
|
|
|
797
885
|
this.sponsorId = 0;
|
|
798
886
|
this.productRefs = "";
|
|
799
887
|
this.currency = "NOK";
|
|
888
|
+
this.layout = "";
|
|
889
|
+
this.showDescription = false;
|
|
890
|
+
this.descriptions = {};
|
|
891
|
+
this.buttonStyle = "icon";
|
|
892
|
+
this.buttonLabel = "Kj\xF8p";
|
|
893
|
+
this.resolvedLayout = "carousel";
|
|
800
894
|
this.vioSlot = "";
|
|
801
895
|
this.vioSlotName = "";
|
|
802
896
|
this.vioScope = "";
|
|
@@ -810,7 +904,11 @@ var VioProductCarousel = class extends LitElement {
|
|
|
810
904
|
this.fetchError = "";
|
|
811
905
|
// ── Analytics (auto) ──────────────────────────────────────────────
|
|
812
906
|
// component_impression on the contract rule (≥50% visible ≥1s, once per
|
|
813
|
-
// session)
|
|
907
|
+
// session) with the products shown in `commerce.items` (ADR-0041: the
|
|
908
|
+
// collector counts per-product impressions from them) + view_item_list with
|
|
909
|
+
// the rendered products. While products load the impression is not
|
|
910
|
+
// observed: it starts once they are here (or the fetch failed — slotted
|
|
911
|
+
// fallback is still an impression, without items). No-ops unless
|
|
814
912
|
// Vio.analytics.start() ran.
|
|
815
913
|
this.unobserveImpression = null;
|
|
816
914
|
this.impressed = false;
|
|
@@ -919,6 +1017,21 @@ var VioProductCarousel = class extends LitElement {
|
|
|
919
1017
|
.scroller { padding: 0 var(--vio-space-lg, 24px) 8px; }
|
|
920
1018
|
}
|
|
921
1019
|
.scroller::-webkit-scrollbar { display: none; }
|
|
1020
|
+
/* layout="list": one card under the other, as wide as the column. */
|
|
1021
|
+
.viewport.list .scroller {
|
|
1022
|
+
flex-direction: column;
|
|
1023
|
+
align-items: stretch;
|
|
1024
|
+
gap: var(--vio-carousel-list-gap, 32px);
|
|
1025
|
+
max-width: var(--vio-carousel-list-max-width, 480px);
|
|
1026
|
+
margin: 0 auto;
|
|
1027
|
+
padding: 0 var(--vio-space-lg, 24px);
|
|
1028
|
+
overflow: visible;
|
|
1029
|
+
scroll-snap-type: none;
|
|
1030
|
+
}
|
|
1031
|
+
.viewport.list .scroller vio-product,
|
|
1032
|
+
.viewport.list ::slotted(*),
|
|
1033
|
+
.viewport.list .skeleton { flex: 0 0 auto; width: 100%; }
|
|
1034
|
+
.viewport.list .nav-btn { display: none; }
|
|
922
1035
|
::slotted(*),
|
|
923
1036
|
vio-product {
|
|
924
1037
|
flex: 0 0 240px;
|
|
@@ -991,13 +1104,32 @@ var VioProductCarousel = class extends LitElement {
|
|
|
991
1104
|
}
|
|
992
1105
|
`;
|
|
993
1106
|
}
|
|
1107
|
+
overrideFor(p) {
|
|
1108
|
+
const own = this.descriptions?.[String(p.id)];
|
|
1109
|
+
return typeof own === "string" ? own : void 0;
|
|
1110
|
+
}
|
|
1111
|
+
descriptionFor(p) {
|
|
1112
|
+
return this.overrideFor(p) ?? cardDescription(p.description);
|
|
1113
|
+
}
|
|
1114
|
+
/** The editor's text always shows (unless empty); the catalogue's only with show-description. */
|
|
1115
|
+
showsDescription(p) {
|
|
1116
|
+
const own = this.overrideFor(p);
|
|
1117
|
+
return own !== void 0 ? own.trim() !== "" : this.showDescription;
|
|
1118
|
+
}
|
|
1119
|
+
willUpdate(changed) {
|
|
1120
|
+
if (changed.has("layout") || !this.hasUpdated) {
|
|
1121
|
+
const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue("--vio-carousel-layout") : "";
|
|
1122
|
+
const value = (this.layout || fromTheme).trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
1123
|
+
this.resolvedLayout = value === "list" ? "list" : "carousel";
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
994
1126
|
connectedCallback() {
|
|
995
1127
|
super.connectedCallback();
|
|
996
1128
|
this.slotRegistration.connect(this.slotAttributes());
|
|
997
|
-
this.observeForAnalytics();
|
|
998
1129
|
if (this.productRefs.trim() || this.productIds.trim()) {
|
|
999
1130
|
void this.autoFetch();
|
|
1000
1131
|
}
|
|
1132
|
+
this.observeForAnalytics();
|
|
1001
1133
|
}
|
|
1002
1134
|
disconnectedCallback() {
|
|
1003
1135
|
this.unobserveImpression?.();
|
|
@@ -1031,6 +1163,10 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1031
1163
|
}
|
|
1032
1164
|
this.isLoading = true;
|
|
1033
1165
|
this.fetchError = "";
|
|
1166
|
+
if (!this.impressed) {
|
|
1167
|
+
this.unobserveImpression?.();
|
|
1168
|
+
this.unobserveImpression = null;
|
|
1169
|
+
}
|
|
1034
1170
|
try {
|
|
1035
1171
|
if (this.productRefs.trim() === "" && !this.commerceKey && this.sponsorId <= 0 && !Vio.bootstrapCache) {
|
|
1036
1172
|
try {
|
|
@@ -1074,10 +1210,11 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1074
1210
|
this.fetchError = err instanceof Error ? err.message : String(err);
|
|
1075
1211
|
} finally {
|
|
1076
1212
|
this.isLoading = false;
|
|
1213
|
+
this.observeForAnalytics();
|
|
1077
1214
|
}
|
|
1078
1215
|
}
|
|
1079
1216
|
observeForAnalytics() {
|
|
1080
|
-
if (this.unobserveImpression) return;
|
|
1217
|
+
if (this.unobserveImpression || this.isLoading || !this.isConnected) return;
|
|
1081
1218
|
try {
|
|
1082
1219
|
this.unobserveImpression = Vio.analytics.observeImpression(
|
|
1083
1220
|
this,
|
|
@@ -1092,7 +1229,8 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1092
1229
|
onImpression: () => {
|
|
1093
1230
|
this.impressed = true;
|
|
1094
1231
|
this.maybeTrackList();
|
|
1095
|
-
}
|
|
1232
|
+
},
|
|
1233
|
+
commerce: () => this.listCommerce()
|
|
1096
1234
|
}
|
|
1097
1235
|
);
|
|
1098
1236
|
} catch {
|
|
@@ -1113,19 +1251,25 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1113
1251
|
locationId: this.effectiveSlot || this.label || void 0,
|
|
1114
1252
|
...this.attribution()
|
|
1115
1253
|
},
|
|
1116
|
-
commerce:
|
|
1117
|
-
items: entries.map(({ product, sponsorId: sponsorId2 }) => ({
|
|
1118
|
-
productId: product.id,
|
|
1119
|
-
name: product.title,
|
|
1120
|
-
price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
|
|
1121
|
-
})),
|
|
1122
|
-
currency: entries[0]?.product.price?.currency_code
|
|
1123
|
-
},
|
|
1254
|
+
commerce: this.listCommerce() ?? void 0,
|
|
1124
1255
|
props: this.label ? { list_name: this.label } : void 0
|
|
1125
1256
|
});
|
|
1126
1257
|
} catch {
|
|
1127
1258
|
}
|
|
1128
1259
|
}
|
|
1260
|
+
/** The products on show as analytics lines (impression and view_item_list); null before they load. */
|
|
1261
|
+
listCommerce() {
|
|
1262
|
+
const entries = this.fetched ?? [];
|
|
1263
|
+
if (entries.length === 0) return null;
|
|
1264
|
+
return {
|
|
1265
|
+
items: entries.map(({ product }) => ({
|
|
1266
|
+
productId: product.id,
|
|
1267
|
+
name: product.title,
|
|
1268
|
+
price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
|
|
1269
|
+
})),
|
|
1270
|
+
currency: entries[0]?.product.price?.currency_code
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1129
1273
|
parseRefs(raw) {
|
|
1130
1274
|
return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
|
|
1131
1275
|
const [sidStr, pidStr] = pair.split(":");
|
|
@@ -1165,6 +1309,10 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1165
1309
|
name=${p.title}
|
|
1166
1310
|
price=${displayPrice(p)}
|
|
1167
1311
|
retailer=${retailer}
|
|
1312
|
+
description=${this.descriptionFor(p)}
|
|
1313
|
+
?show-description=${this.showsDescription(p)}
|
|
1314
|
+
button-style=${this.buttonStyle}
|
|
1315
|
+
button-label=${this.buttonLabel}
|
|
1168
1316
|
image-url=${img}
|
|
1169
1317
|
?has-variants=${(p.variants?.length ?? 0) > 1}
|
|
1170
1318
|
.vioSlot=${this.effectiveSlot}
|
|
@@ -1190,7 +1338,7 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1190
1338
|
</div>
|
|
1191
1339
|
${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
|
|
1192
1340
|
</div>
|
|
1193
|
-
<div class="viewport">
|
|
1341
|
+
<div class="viewport ${this.resolvedLayout}">
|
|
1194
1342
|
<button
|
|
1195
1343
|
class="nav-btn prev"
|
|
1196
1344
|
@click=${() => this.scrollByDirection(-1)}
|
|
@@ -1241,6 +1389,24 @@ __decorateClass([
|
|
|
1241
1389
|
__decorateClass([
|
|
1242
1390
|
property({ type: String })
|
|
1243
1391
|
], VioProductCarousel.prototype, "currency", 2);
|
|
1392
|
+
__decorateClass([
|
|
1393
|
+
property({ type: String, reflect: true })
|
|
1394
|
+
], VioProductCarousel.prototype, "layout", 2);
|
|
1395
|
+
__decorateClass([
|
|
1396
|
+
property({ type: Boolean, attribute: "show-description" })
|
|
1397
|
+
], VioProductCarousel.prototype, "showDescription", 2);
|
|
1398
|
+
__decorateClass([
|
|
1399
|
+
property({ type: Object })
|
|
1400
|
+
], VioProductCarousel.prototype, "descriptions", 2);
|
|
1401
|
+
__decorateClass([
|
|
1402
|
+
property({ type: String, attribute: "button-style" })
|
|
1403
|
+
], VioProductCarousel.prototype, "buttonStyle", 2);
|
|
1404
|
+
__decorateClass([
|
|
1405
|
+
property({ type: String, attribute: "button-label" })
|
|
1406
|
+
], VioProductCarousel.prototype, "buttonLabel", 2);
|
|
1407
|
+
__decorateClass([
|
|
1408
|
+
state()
|
|
1409
|
+
], VioProductCarousel.prototype, "resolvedLayout", 2);
|
|
1244
1410
|
__decorateClass([
|
|
1245
1411
|
property({ type: String, attribute: "vio-slot" })
|
|
1246
1412
|
], VioProductCarousel.prototype, "vioSlot", 2);
|
|
@@ -1274,6 +1440,10 @@ __decorateClass([
|
|
|
1274
1440
|
__decorateClass([
|
|
1275
1441
|
state()
|
|
1276
1442
|
], VioProductCarousel.prototype, "fetchError", 2);
|
|
1443
|
+
function cardDescription(description) {
|
|
1444
|
+
if (!description) return "";
|
|
1445
|
+
return description.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\s+/g, " ").trim();
|
|
1446
|
+
}
|
|
1277
1447
|
|
|
1278
1448
|
// src/ui/payment-redirect.ts
|
|
1279
1449
|
var OVERLAY_CLASS = "vio-redirect-overlay";
|
|
@@ -1381,7 +1551,7 @@ var NO_EXPRESS_BUTTON_METHODS = [
|
|
|
1381
1551
|
...EMBEDDED_METHODS,
|
|
1382
1552
|
...FORM_FIRST_WIDGET_METHODS
|
|
1383
1553
|
];
|
|
1384
|
-
var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS
|
|
1554
|
+
var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS];
|
|
1385
1555
|
var isEmbeddedMethod = (name) => EMBEDDED_METHODS.includes(normalizeMethodName(name));
|
|
1386
1556
|
var isFormFirstWidgetMethod = (name) => FORM_FIRST_WIDGET_METHODS.includes(normalizeMethodName(name));
|
|
1387
1557
|
var collectsOwnAddress = (name) => COLLECTS_OWN_ADDRESS.includes(normalizeMethodName(name));
|
|
@@ -1595,6 +1765,8 @@ var BottomSheet = class {
|
|
|
1595
1765
|
this.locked = false;
|
|
1596
1766
|
this.drag = null;
|
|
1597
1767
|
this.dragEndedAt = -Infinity;
|
|
1768
|
+
/** A finger down in the content, not yet known to be a pull (down) or a scroll. */
|
|
1769
|
+
this.pull = null;
|
|
1598
1770
|
this.onPointerDown = (e) => {
|
|
1599
1771
|
if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
|
|
1600
1772
|
if (this.drag || !isPhoneSheet()) return;
|
|
@@ -1620,45 +1792,54 @@ var BottomSheet = class {
|
|
|
1620
1792
|
if (!drag.moving) {
|
|
1621
1793
|
if (dy < -SLOP) return this.abandon();
|
|
1622
1794
|
if (dy <= SLOP) return;
|
|
1623
|
-
drag
|
|
1624
|
-
const sheet2 = this.opts.sheet();
|
|
1625
|
-
if (sheet2) sheet2.style.transition = "none";
|
|
1626
|
-
const backdrop2 = this.opts.backdrop?.();
|
|
1627
|
-
if (backdrop2) backdrop2.style.transition = "none";
|
|
1628
|
-
}
|
|
1629
|
-
const dt = e.timeStamp - drag.lastT;
|
|
1630
|
-
if (dt > 0) drag.velocity = 0.8 * ((e.clientY - drag.lastY) / dt) + 0.2 * drag.velocity;
|
|
1631
|
-
drag.lastY = e.clientY;
|
|
1632
|
-
drag.lastT = e.timeStamp;
|
|
1633
|
-
drag.offset = Math.max(0, dy);
|
|
1634
|
-
const sheet = this.opts.sheet();
|
|
1635
|
-
if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
|
|
1636
|
-
const backdrop = this.opts.backdrop?.();
|
|
1637
|
-
if (backdrop && sheet) {
|
|
1638
|
-
const height = sheet.getBoundingClientRect().height || window.innerHeight;
|
|
1639
|
-
backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
|
|
1795
|
+
this.startMoving(drag);
|
|
1640
1796
|
}
|
|
1797
|
+
this.follow(drag, e.clientY, e.timeStamp);
|
|
1641
1798
|
if (e.cancelable) e.preventDefault();
|
|
1642
1799
|
};
|
|
1643
1800
|
this.onPointerUp = (e) => {
|
|
1644
1801
|
const drag = this.drag;
|
|
1645
1802
|
if (!drag || e.pointerId !== drag.pointerId) return;
|
|
1646
1803
|
this.stopTracking();
|
|
1647
|
-
this.drag
|
|
1648
|
-
|
|
1649
|
-
|
|
1804
|
+
this.finish(drag);
|
|
1805
|
+
};
|
|
1806
|
+
// ── Pull from the content (touch) ─────────────────────────────────────────
|
|
1807
|
+
this.onTouchStart = (e) => {
|
|
1808
|
+
this.pull = null;
|
|
1809
|
+
if (this.drag || e.touches.length !== 1 || !isPhoneSheet()) return;
|
|
1650
1810
|
const sheet = this.opts.sheet();
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1811
|
+
if (!sheet || !e.composedPath().includes(sheet)) return;
|
|
1812
|
+
if (inZone(e, sheet, this.opts.dragZones)) return;
|
|
1813
|
+
const t = e.touches[0];
|
|
1814
|
+
if (t) this.pull = { startX: t.clientX, startY: t.clientY };
|
|
1815
|
+
};
|
|
1816
|
+
this.onTouchMove = (e) => {
|
|
1817
|
+
const t = e.touches[0];
|
|
1818
|
+
if (!t) return;
|
|
1819
|
+
if (this.pull) {
|
|
1820
|
+
const dx = t.clientX - this.pull.startX;
|
|
1821
|
+
const dy = t.clientY - this.pull.startY;
|
|
1822
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) <= SLOP) return;
|
|
1823
|
+
const startY = this.pull.startY;
|
|
1824
|
+
this.pull = null;
|
|
1825
|
+
if (Math.abs(dx) >= Math.abs(dy) || dy < 0 || this.scrolledDown(e)) return;
|
|
1826
|
+
this.drag = { pointerId: -1, startY, lastY: startY, lastT: e.timeStamp, velocity: 0, moving: false, offset: 0 };
|
|
1827
|
+
this.startMoving(this.drag);
|
|
1661
1828
|
}
|
|
1829
|
+
const drag = this.drag;
|
|
1830
|
+
if (!drag || drag.pointerId !== -1) return;
|
|
1831
|
+
this.follow(drag, t.clientY, e.timeStamp);
|
|
1832
|
+
if (e.cancelable) e.preventDefault();
|
|
1833
|
+
};
|
|
1834
|
+
this.onTouchEnd = (e) => {
|
|
1835
|
+
this.pull = null;
|
|
1836
|
+
const drag = this.drag;
|
|
1837
|
+
if (!drag || drag.pointerId !== -1 || e.touches.length > 0) return;
|
|
1838
|
+
this.finish(drag);
|
|
1839
|
+
};
|
|
1840
|
+
this.onTouchCancel = () => {
|
|
1841
|
+
this.pull = null;
|
|
1842
|
+
if (this.drag?.pointerId === -1) this.abandon();
|
|
1662
1843
|
};
|
|
1663
1844
|
this.onPointerCancel = (e) => {
|
|
1664
1845
|
if (!this.drag || e.pointerId !== this.drag.pointerId) return;
|
|
@@ -1693,12 +1874,21 @@ var BottomSheet = class {
|
|
|
1693
1874
|
}
|
|
1694
1875
|
this.opts.host.addEventListener("pointerdown", this.onPointerDown);
|
|
1695
1876
|
this.opts.host.addEventListener("click", this.onClickCapture, true);
|
|
1877
|
+
this.opts.host.addEventListener("touchstart", this.onTouchStart, { passive: true });
|
|
1878
|
+
this.opts.host.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
1879
|
+
this.opts.host.addEventListener("touchend", this.onTouchEnd);
|
|
1880
|
+
this.opts.host.addEventListener("touchcancel", this.onTouchCancel);
|
|
1696
1881
|
}
|
|
1697
1882
|
closed() {
|
|
1698
1883
|
this.opts.host.removeEventListener("pointerdown", this.onPointerDown);
|
|
1699
1884
|
this.opts.host.removeEventListener("click", this.onClickCapture, true);
|
|
1885
|
+
this.opts.host.removeEventListener("touchstart", this.onTouchStart);
|
|
1886
|
+
this.opts.host.removeEventListener("touchmove", this.onTouchMove);
|
|
1887
|
+
this.opts.host.removeEventListener("touchend", this.onTouchEnd);
|
|
1888
|
+
this.opts.host.removeEventListener("touchcancel", this.onTouchCancel);
|
|
1700
1889
|
this.stopTracking();
|
|
1701
1890
|
this.drag = null;
|
|
1891
|
+
this.pull = null;
|
|
1702
1892
|
const sheet = this.opts.sheet();
|
|
1703
1893
|
if (sheet) {
|
|
1704
1894
|
sheet.style.transform = "";
|
|
@@ -1714,6 +1904,58 @@ var BottomSheet = class {
|
|
|
1714
1904
|
unlockPageScroll();
|
|
1715
1905
|
}
|
|
1716
1906
|
}
|
|
1907
|
+
/** Is the content under the finger scrolled away from its top (the sheet's scroller, or one inside it)? */
|
|
1908
|
+
scrolledDown(e) {
|
|
1909
|
+
const sheet = this.opts.sheet();
|
|
1910
|
+
const scroller = this.opts.scroller?.() ?? sheet;
|
|
1911
|
+
if (scroller && scroller.scrollTop > 0) return true;
|
|
1912
|
+
for (const node of e.composedPath()) {
|
|
1913
|
+
if (node === sheet) break;
|
|
1914
|
+
if (node instanceof HTMLElement && node.scrollTop > 0 && node.scrollHeight > node.clientHeight) return true;
|
|
1915
|
+
}
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
// ── Shared by both ────────────────────────────────────────────────────────
|
|
1919
|
+
/** The press is a drag now: the sheet and backdrop follow the finger without easing. */
|
|
1920
|
+
startMoving(drag) {
|
|
1921
|
+
drag.moving = true;
|
|
1922
|
+
const sheet = this.opts.sheet();
|
|
1923
|
+
if (sheet) sheet.style.transition = "none";
|
|
1924
|
+
const backdrop = this.opts.backdrop?.();
|
|
1925
|
+
if (backdrop) backdrop.style.transition = "none";
|
|
1926
|
+
}
|
|
1927
|
+
follow(drag, y, timeStamp) {
|
|
1928
|
+
const dt = timeStamp - drag.lastT;
|
|
1929
|
+
if (dt > 0) drag.velocity = 0.8 * ((y - drag.lastY) / dt) + 0.2 * drag.velocity;
|
|
1930
|
+
drag.lastY = y;
|
|
1931
|
+
drag.lastT = timeStamp;
|
|
1932
|
+
drag.offset = Math.max(0, y - drag.startY);
|
|
1933
|
+
const sheet = this.opts.sheet();
|
|
1934
|
+
if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
|
|
1935
|
+
const backdrop = this.opts.backdrop?.();
|
|
1936
|
+
if (backdrop && sheet) {
|
|
1937
|
+
const height = sheet.getBoundingClientRect().height || window.innerHeight;
|
|
1938
|
+
backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/** Let go: close if far or fast enough (and allowed), else spring back. */
|
|
1942
|
+
finish(drag) {
|
|
1943
|
+
this.drag = null;
|
|
1944
|
+
if (!drag.moving) return;
|
|
1945
|
+
this.dragEndedAt = performance.now();
|
|
1946
|
+
const sheet = this.opts.sheet();
|
|
1947
|
+
const height = sheet?.getBoundingClientRect().height || window.innerHeight;
|
|
1948
|
+
const far = drag.offset >= Math.min(CLOSE_DISTANCE, height / 4);
|
|
1949
|
+
const flick = drag.velocity >= FLICK_VELOCITY && drag.offset >= FLICK_MIN;
|
|
1950
|
+
const allowed = !this.opts.canClose || this.opts.canClose();
|
|
1951
|
+
this.release();
|
|
1952
|
+
if ((far || flick) && allowed) {
|
|
1953
|
+
if (sheet) sheet.style.transform = "translateY(100%)";
|
|
1954
|
+
this.opts.close();
|
|
1955
|
+
} else if (sheet) {
|
|
1956
|
+
sheet.style.transform = "";
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1717
1959
|
/** Give up on this press and put the sheet back where it was. */
|
|
1718
1960
|
abandon() {
|
|
1719
1961
|
const moving = this.drag?.moving;
|
|
@@ -1780,9 +2022,40 @@ function unlockPageScroll() {
|
|
|
1780
2022
|
}
|
|
1781
2023
|
|
|
1782
2024
|
// src/ui/components/vio-product-detail.ts
|
|
2025
|
+
var ADDED_TOAST_MS = 8e3;
|
|
1783
2026
|
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1784
2027
|
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
1785
2028
|
);
|
|
2029
|
+
var HIGHLIGHT_ICONS = {
|
|
2030
|
+
plane: "M21.5 2.5 2.5 10l7 3m12-10.5-12 10.5m12-10.5L17 21l-7.5-8m0 0V20l3-4M5 17l-2.5 2.5M7.5 18.5 6 20",
|
|
2031
|
+
heart: "M12 20.5s-8.5-5-8.5-11A4.75 4.75 0 0 1 12 6.75 4.75 4.75 0 0 1 20.5 9.5c0 6-8.5 11-8.5 11Z",
|
|
2032
|
+
shirt: "M8.5 3.5 3 6l1.75 4.25L7 9.5v11h10v-11l2.25.75L21 6l-5.5-2.5a3.5 3.5 0 0 1-7 0Z",
|
|
2033
|
+
truck: "M2.5 5.5h11v10h-11zM13.5 9.5h4l3 3.5v2.5h-7M6.5 18.5a2 2 0 1 0 0-.01M17 18.5a2 2 0 1 0 0-.01",
|
|
2034
|
+
return: "M4 9h11.5a4.5 4.5 0 0 1 0 9H8M4 9l4-4M4 9l4 4",
|
|
2035
|
+
leaf: "M5 19C5 10 10 4.5 20 4.5c0 10-5.5 15-14 15M5 19l7-7",
|
|
2036
|
+
star: "m12 3.5 2.6 5.4 5.9.8-4.3 4.1 1 5.9L12 17l-5.2 2.7 1-5.9-4.3-4.1 5.9-.8Z",
|
|
2037
|
+
shield: "M12 3 4.5 6v5.5c0 4.5 3.2 8.2 7.5 9.5 4.3-1.3 7.5-5 7.5-9.5V6Z",
|
|
2038
|
+
mailbox: "M3 19v-8a4 4 0 0 1 8 0v8m-8 0h18v-7a4 4 0 0 0-4-4H7m8 0V4h3M6 12h2",
|
|
2039
|
+
home: "M3 11 12 4l9 7M5.5 9.5V20h13V9.5M10 20v-5h4v5",
|
|
2040
|
+
pin: "M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
|
|
2041
|
+
clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13.5V12l3 2"
|
|
2042
|
+
};
|
|
2043
|
+
function rateIcon(method) {
|
|
2044
|
+
const m = method.toLowerCase();
|
|
2045
|
+
if (/postkasse|mailbox|brevkasse/.test(m)) return "mailbox";
|
|
2046
|
+
if (/hente|pakkeboks|automat|postkontor|pickup|utlever/.test(m)) return "pin";
|
|
2047
|
+
if (/dør|hjem|home|door/.test(m)) return "home";
|
|
2048
|
+
return "truck";
|
|
2049
|
+
}
|
|
2050
|
+
function carrierLogo(logos, carrier) {
|
|
2051
|
+
if (!logos || !carrier) return "";
|
|
2052
|
+
const want = carrier.trim().toLowerCase();
|
|
2053
|
+
for (const [name, url] of Object.entries(logos)) if (name.trim().toLowerCase() === want && url) return url;
|
|
2054
|
+
return "";
|
|
2055
|
+
}
|
|
2056
|
+
function lineIcon(name, cls = "line-icon") {
|
|
2057
|
+
return html`<svg class=${cls} viewBox="0 0 24 24" aria-hidden="true"><path d=${HIGHLIGHT_ICONS[name] ?? ""}></path></svg>`;
|
|
2058
|
+
}
|
|
1786
2059
|
var VioProductDetail = class extends LitElement {
|
|
1787
2060
|
constructor() {
|
|
1788
2061
|
super(...arguments);
|
|
@@ -1798,6 +2071,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
1798
2071
|
this.vioCampaign = "";
|
|
1799
2072
|
this.vioContentId = "";
|
|
1800
2073
|
this.vioContentTitle = "";
|
|
2074
|
+
this.noAddedToast = false;
|
|
2075
|
+
this.gallery = "";
|
|
2076
|
+
this.descriptionMode = "";
|
|
2077
|
+
this.highlights = [];
|
|
2078
|
+
this.deliveryTime = "";
|
|
2079
|
+
this.carrierLogos = {};
|
|
1801
2080
|
this.slotRegistration = new SlotRegistration(this, "detail");
|
|
1802
2081
|
/** What the click that opened the detail carried ('' when nothing), and for which product. */
|
|
1803
2082
|
this.eventSlot = "";
|
|
@@ -1808,18 +2087,31 @@ var VioProductDetail = class extends LitElement {
|
|
|
1808
2087
|
this.product = null;
|
|
1809
2088
|
this.sellerName = null;
|
|
1810
2089
|
this.sellerSettled = false;
|
|
2090
|
+
this.shippingFrom = null;
|
|
2091
|
+
this.shippingRates = [];
|
|
1811
2092
|
this.isLoading = false;
|
|
1812
2093
|
this.fetchError = "";
|
|
1813
2094
|
this.selectedOptions = {};
|
|
2095
|
+
this.selectionPrompt = "";
|
|
1814
2096
|
this.quantity = 1;
|
|
1815
2097
|
this.activeImageIndex = 0;
|
|
1816
2098
|
this.thumbsOverflow = false;
|
|
1817
2099
|
this.thumbsAtStart = true;
|
|
1818
2100
|
this.thumbsAtEnd = false;
|
|
2101
|
+
this.descriptionOpen = false;
|
|
2102
|
+
this.descriptionClipped = false;
|
|
2103
|
+
this.galleryMode = "thumbs";
|
|
2104
|
+
this.resolvedDescriptionMode = "preview";
|
|
1819
2105
|
this.logoSquare = false;
|
|
1820
2106
|
this.adding = false;
|
|
2107
|
+
this.addedToast = null;
|
|
2108
|
+
this.addedToastTimer = null;
|
|
1821
2109
|
this.applePayOk = false;
|
|
1822
2110
|
this.availableMethods = null;
|
|
2111
|
+
this.methodsLoading = false;
|
|
2112
|
+
this.vippsExpress = false;
|
|
2113
|
+
/** Whose methods `availableMethods` are — another brand's are not shown while its own load. */
|
|
2114
|
+
this.methodsSponsorId = 0;
|
|
1823
2115
|
this.vippsOfficial = false;
|
|
1824
2116
|
/** Race guard: only the latest fetch may write state (two quick taps on
|
|
1825
2117
|
* different cards would otherwise let the slow response win). */
|
|
@@ -1828,6 +2120,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
1828
2120
|
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
1829
2121
|
/** Pre-prepared Apple Pay handle — show() fires synchronously on tap. */
|
|
1830
2122
|
this.applePayHandle = null;
|
|
2123
|
+
/** The amount `applePayHandle`'s sheet shows (product × quantity when it was prepared). */
|
|
2124
|
+
this.applePayAmount = null;
|
|
2125
|
+
/** The amount the latest preparation was asked for — a newer one supersedes it. */
|
|
2126
|
+
this.applePayAskedFor = null;
|
|
1831
2127
|
/** Keyboard and focus while open: Escape closes, Tab stays inside, focus returns to the opener. */
|
|
1832
2128
|
this.dialogFocus = new DialogFocus({
|
|
1833
2129
|
host: this,
|
|
@@ -1869,6 +2165,15 @@ var VioProductDetail = class extends LitElement {
|
|
|
1869
2165
|
this.show();
|
|
1870
2166
|
void this.fetchProduct();
|
|
1871
2167
|
};
|
|
2168
|
+
/** The photo in view is the active one (dots, Apple Pay sheet, events). */
|
|
2169
|
+
this.onCarouselScroll = () => {
|
|
2170
|
+
const track = this.carouselTrack();
|
|
2171
|
+
const step = this.carouselStep(track);
|
|
2172
|
+
if (!track || !step) return;
|
|
2173
|
+
const last = track.children.length - 1;
|
|
2174
|
+
const idx = Math.min(last, Math.max(0, Math.round(track.scrollLeft / step)));
|
|
2175
|
+
if (idx !== this.activeImageIndex) this.activeImageIndex = idx;
|
|
2176
|
+
};
|
|
1872
2177
|
this.onThumbsScroll = () => {
|
|
1873
2178
|
const row = this.thumbRow();
|
|
1874
2179
|
if (!row) return;
|
|
@@ -2183,6 +2488,103 @@ var VioProductDetail = class extends LitElement {
|
|
|
2183
2488
|
.thumbs.overflows .thumbs-arrow { display: flex; }
|
|
2184
2489
|
}
|
|
2185
2490
|
|
|
2491
|
+
/* Carousel (gallery="carousel" / theme detailGallery): the same box as the
|
|
2492
|
+
main image — proportion, fit, background, inset — one per photo, in a
|
|
2493
|
+
row that snaps. --vio-detail-carousel-peek shows the edge of the next. */
|
|
2494
|
+
.carousel {
|
|
2495
|
+
position: relative;
|
|
2496
|
+
margin-inline: calc(
|
|
2497
|
+
var(--vio-detail-image-inset, var(--vio-detail-padding-x, var(--vio-space-lg, 24px))) -
|
|
2498
|
+
var(--vio-detail-padding-x, var(--vio-space-lg, 24px))
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
.carousel-track {
|
|
2502
|
+
display: flex;
|
|
2503
|
+
gap: var(--vio-detail-carousel-gap, 0px);
|
|
2504
|
+
overflow-x: auto;
|
|
2505
|
+
scroll-snap-type: x mandatory;
|
|
2506
|
+
scroll-behavior: smooth;
|
|
2507
|
+
scrollbar-width: none;
|
|
2508
|
+
-webkit-overflow-scrolling: touch;
|
|
2509
|
+
overscroll-behavior-x: contain;
|
|
2510
|
+
/* Sideways is the carousel's, up and down the panel's; no double-tap
|
|
2511
|
+
zoom delay. The sheet only drags from its top bar, never from here. */
|
|
2512
|
+
touch-action: pan-x pan-y;
|
|
2513
|
+
border-radius: var(--vio-detail-image-radius, 0);
|
|
2514
|
+
}
|
|
2515
|
+
.carousel-track::-webkit-scrollbar { display: none; }
|
|
2516
|
+
.carousel-slide {
|
|
2517
|
+
flex: 0 0 calc(100% - var(--vio-detail-carousel-peek, 0px));
|
|
2518
|
+
aspect-ratio: var(--vio-detail-image-aspect, 4 / 5);
|
|
2519
|
+
max-height: var(--vio-detail-image-max-height, 85vh);
|
|
2520
|
+
background: var(--vio-detail-image-bg, var(--vio-color-surface-muted, #f2f2f2));
|
|
2521
|
+
scroll-snap-align: start;
|
|
2522
|
+
scroll-snap-stop: always;
|
|
2523
|
+
overflow: hidden;
|
|
2524
|
+
}
|
|
2525
|
+
.carousel-slide img {
|
|
2526
|
+
display: block;
|
|
2527
|
+
/* A press on a photo is a swipe: no image drag, no long-press menu, no selection. */
|
|
2528
|
+
pointer-events: none;
|
|
2529
|
+
user-select: none;
|
|
2530
|
+
-webkit-user-select: none;
|
|
2531
|
+
-webkit-user-drag: none;
|
|
2532
|
+
-webkit-touch-callout: none;
|
|
2533
|
+
width: 100%;
|
|
2534
|
+
height: 100%;
|
|
2535
|
+
object-fit: var(--vio-detail-image-fit, cover);
|
|
2536
|
+
object-position: var(--vio-detail-image-position, center);
|
|
2537
|
+
transform: scale(var(--vio-detail-image-zoom, 1));
|
|
2538
|
+
}
|
|
2539
|
+
/* One dot per photo over the bottom of the image: there is more than one,
|
|
2540
|
+
and which one this is. White with a shadow reads on light and dark photos. */
|
|
2541
|
+
.carousel-dots {
|
|
2542
|
+
position: absolute;
|
|
2543
|
+
left: 0;
|
|
2544
|
+
right: 0;
|
|
2545
|
+
bottom: 12px;
|
|
2546
|
+
display: flex;
|
|
2547
|
+
justify-content: center;
|
|
2548
|
+
gap: 6px;
|
|
2549
|
+
pointer-events: none;
|
|
2550
|
+
}
|
|
2551
|
+
.carousel-dot {
|
|
2552
|
+
width: 6px;
|
|
2553
|
+
height: 6px;
|
|
2554
|
+
border-radius: 50%;
|
|
2555
|
+
background: var(--vio-detail-dot-color, rgba(255, 255, 255, 0.6));
|
|
2556
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.35);
|
|
2557
|
+
transition: background 0.2s ease, transform 0.2s ease;
|
|
2558
|
+
}
|
|
2559
|
+
.carousel-dot.active {
|
|
2560
|
+
background: var(--vio-detail-dot-active-color, #fff);
|
|
2561
|
+
transform: scale(1.35);
|
|
2562
|
+
}
|
|
2563
|
+
.carousel-arrow {
|
|
2564
|
+
display: none;
|
|
2565
|
+
position: absolute;
|
|
2566
|
+
top: 50%;
|
|
2567
|
+
transform: translateY(-50%);
|
|
2568
|
+
width: 36px;
|
|
2569
|
+
height: 36px;
|
|
2570
|
+
border-radius: 50%;
|
|
2571
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2572
|
+
background: var(--vio-color-surface, #fff);
|
|
2573
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
2574
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
2575
|
+
cursor: pointer;
|
|
2576
|
+
align-items: center;
|
|
2577
|
+
justify-content: center;
|
|
2578
|
+
padding: 0;
|
|
2579
|
+
font: 18px/1 sans-serif;
|
|
2580
|
+
}
|
|
2581
|
+
.carousel-arrow.prev { left: 12px; }
|
|
2582
|
+
.carousel-arrow.next { right: 12px; }
|
|
2583
|
+
.carousel-arrow:disabled { visibility: hidden; }
|
|
2584
|
+
@media (hover: hover) and (pointer: fine) {
|
|
2585
|
+
.carousel-arrow { display: flex; }
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2186
2588
|
/* Info column */
|
|
2187
2589
|
.info { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); min-width: 0; }
|
|
2188
2590
|
.brand {
|
|
@@ -2200,11 +2602,88 @@ var VioProductDetail = class extends LitElement {
|
|
|
2200
2602
|
margin: 0;
|
|
2201
2603
|
letter-spacing: -0.01em;
|
|
2202
2604
|
}
|
|
2605
|
+
/* The brand's promises: one row, an icon over a title and a line of text,
|
|
2606
|
+
centred like a shop's own trust strip. */
|
|
2607
|
+
.highlights {
|
|
2608
|
+
list-style: none;
|
|
2609
|
+
/* --vio-detail-highlights-display: none hides the strip (theme detailHighlightsDisplay). */
|
|
2610
|
+
margin: 0;
|
|
2611
|
+
padding: var(--vio-detail-highlights-padding, 16px 0 2px);
|
|
2612
|
+
display: var(--vio-detail-highlights-display, grid);
|
|
2613
|
+
grid-auto-flow: column;
|
|
2614
|
+
grid-auto-columns: 1fr;
|
|
2615
|
+
gap: 10px;
|
|
2616
|
+
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2617
|
+
}
|
|
2618
|
+
/* Like a shop's trust strip: a thin icon, the promise in regular weight,
|
|
2619
|
+
a smaller line under it — nothing bold. */
|
|
2620
|
+
.hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
|
|
2621
|
+
.hl-icon {
|
|
2622
|
+
width: var(--vio-detail-highlight-icon-size, 22px);
|
|
2623
|
+
height: var(--vio-detail-highlight-icon-size, 22px);
|
|
2624
|
+
margin-bottom: 8px;
|
|
2625
|
+
fill: none;
|
|
2626
|
+
stroke: currentColor;
|
|
2627
|
+
stroke-width: 1.2;
|
|
2628
|
+
stroke-linecap: round;
|
|
2629
|
+
stroke-linejoin: round;
|
|
2630
|
+
object-fit: contain;
|
|
2631
|
+
}
|
|
2632
|
+
.hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
|
|
2633
|
+
.hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
|
|
2634
|
+
/* «Frakt & retur»: same fold as «Beskrivelse», with a small list of rates.
|
|
2635
|
+
--vio-detail-shipping-returns-display: none hides it (theme detailShippingReturnsDisplay). */
|
|
2636
|
+
.shipping-returns { display: var(--vio-detail-shipping-returns-display, block); }
|
|
2637
|
+
.description.shipping-returns-body { white-space: normal; }
|
|
2638
|
+
.shipping-returns-body p { margin: 0 0 10px; }
|
|
2639
|
+
.sr-title {
|
|
2640
|
+
font-size: 11px;
|
|
2641
|
+
text-transform: uppercase;
|
|
2642
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
2643
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2644
|
+
font-weight: 600;
|
|
2645
|
+
margin: 4px 0 6px;
|
|
2646
|
+
}
|
|
2647
|
+
.sr-rates { list-style: none; margin: 0 0 10px; padding: 0; }
|
|
2648
|
+
/* One row per carrier: «Helthjem» over «Til postkasse/dørmatte», price right. */
|
|
2649
|
+
.sr-rates li {
|
|
2650
|
+
display: flex;
|
|
2651
|
+
align-items: center;
|
|
2652
|
+
gap: 12px;
|
|
2653
|
+
padding: 8px 0;
|
|
2654
|
+
border-bottom: 1px solid var(--vio-color-border, #eee);
|
|
2655
|
+
}
|
|
2656
|
+
.sr-rates li:last-child { border-bottom: none; }
|
|
2657
|
+
.sr-rate { flex: 1; display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
|
|
2658
|
+
.line-icon { fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
|
|
2659
|
+
.sr-icon { flex: 0 0 auto; width: 20px; height: 20px; color: var(--vio-color-text, #0a0a0a); }
|
|
2660
|
+
.sr-line { display: flex; align-items: center; gap: 12px; }
|
|
2661
|
+
/* A carrier's logo in a fixed box, so rows line up whatever its shape. */
|
|
2662
|
+
.sr-logo { flex: 0 0 auto; width: var(--vio-detail-carrier-logo-width, 56px); height: 24px; object-fit: contain; object-position: left center; }
|
|
2663
|
+
.sr-carrier { font-weight: 600; color: var(--vio-color-text, #0a0a0a); }
|
|
2664
|
+
.sr-method { font-size: 13px; }
|
|
2665
|
+
.sr-time { margin: 0 0 14px; }
|
|
2666
|
+
.sr-price { font-weight: 600; color: var(--vio-color-text, #0a0a0a); white-space: nowrap; }
|
|
2667
|
+
.sr-links { display: flex; gap: 16px; padding-left: 32px; }
|
|
2668
|
+
.sr-links a { color: inherit; }
|
|
2669
|
+
/* Name and price read as one block (stock on the name's line, right);
|
|
2670
|
+
the sizes come after a wider gap, then shipping/returns by the buttons. */
|
|
2671
|
+
.headline { display: flex; flex-direction: column; gap: var(--vio-detail-headline-gap, 8px); }
|
|
2672
|
+
.title-row {
|
|
2673
|
+
display: flex;
|
|
2674
|
+
align-items: baseline;
|
|
2675
|
+
justify-content: space-between;
|
|
2676
|
+
gap: 16px;
|
|
2677
|
+
}
|
|
2678
|
+
.title-row .name { min-width: 0; }
|
|
2679
|
+
.title-row .stock { flex: 0 0 auto; white-space: nowrap; }
|
|
2203
2680
|
.price-row {
|
|
2204
2681
|
display: flex;
|
|
2205
2682
|
align-items: baseline;
|
|
2206
2683
|
gap: 16px;
|
|
2207
2684
|
}
|
|
2685
|
+
/* The sizes follow the price closely: they are the next thing to do. */
|
|
2686
|
+
.headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
|
|
2208
2687
|
.price {
|
|
2209
2688
|
font-size: var(--vio-detail-price-size, 24px);
|
|
2210
2689
|
font-weight: var(--vio-detail-price-weight, 700);
|
|
@@ -2214,6 +2693,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
2214
2693
|
color: var(--vio-color-text-tertiary, #999);
|
|
2215
2694
|
text-decoration: line-through;
|
|
2216
2695
|
}
|
|
2696
|
+
.delivery-note {
|
|
2697
|
+
margin-top: calc(var(--vio-detail-info-gap, 20px) * -0.6);
|
|
2698
|
+
font-size: 13px;
|
|
2699
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2700
|
+
}
|
|
2217
2701
|
.stock {
|
|
2218
2702
|
font-size: 12px;
|
|
2219
2703
|
text-transform: uppercase;
|
|
@@ -2225,8 +2709,15 @@ var VioProductDetail = class extends LitElement {
|
|
|
2225
2709
|
.stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
|
|
2226
2710
|
|
|
2227
2711
|
/* Option pickers */
|
|
2228
|
-
.option-group { display: flex; flex-direction: column; gap:
|
|
2712
|
+
.option-group { display: flex; flex-direction: column; gap: 8px; }
|
|
2713
|
+
/* The prompt sits on the label's own line: tapping a buy button before
|
|
2714
|
+
choosing must not push the pills down under the shopper's finger. */
|
|
2229
2715
|
.option-label {
|
|
2716
|
+
display: flex;
|
|
2717
|
+
align-items: baseline;
|
|
2718
|
+
justify-content: space-between;
|
|
2719
|
+
gap: 12px;
|
|
2720
|
+
min-height: 1.4em;
|
|
2230
2721
|
font-size: 11px;
|
|
2231
2722
|
text-transform: uppercase;
|
|
2232
2723
|
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
@@ -2272,6 +2763,19 @@ var VioProductDetail = class extends LitElement {
|
|
|
2272
2763
|
background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
|
|
2273
2764
|
}
|
|
2274
2765
|
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
2766
|
+
/* The value chosen, beside the option's name: «Størrelse: M». */
|
|
2767
|
+
.option-chosen { color: var(--vio-color-text, #0a0a0a); font-weight: 700; }
|
|
2768
|
+
/* A buy button was tapped before choosing: say what is missing. */
|
|
2769
|
+
.option-prompt {
|
|
2770
|
+
text-transform: none;
|
|
2771
|
+
letter-spacing: normal;
|
|
2772
|
+
font-size: 13px;
|
|
2773
|
+
font-weight: 600;
|
|
2774
|
+
color: var(--vio-color-accent, #c14a3b);
|
|
2775
|
+
}
|
|
2776
|
+
.option-group.needs-choice .option-pill:not(.unavailable) {
|
|
2777
|
+
border-color: var(--vio-color-accent, #c14a3b);
|
|
2778
|
+
}
|
|
2275
2779
|
|
|
2276
2780
|
/* The buy zone: Vipps first, then quantity + add to cart, then the other
|
|
2277
2781
|
ways to pay. --vio-detail-buy-position: sticky pins it to the bottom of
|
|
@@ -2288,6 +2792,37 @@ var VioProductDetail = class extends LitElement {
|
|
|
2288
2792
|
box-shadow: var(--vio-detail-buy-shadow, none);
|
|
2289
2793
|
}
|
|
2290
2794
|
.buy > * { margin-top: 0 !important; }
|
|
2795
|
+
/* The buy zone and the description, apart from the name, price and stock:
|
|
2796
|
+
a pinned buy zone can slide up over what is in this box (the
|
|
2797
|
+
description) but never above it — never over the name, the price or
|
|
2798
|
+
the stock line (Mote & Livsstil, 2026-10-28). */
|
|
2799
|
+
.purchase { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); }
|
|
2800
|
+
/* The pickers live in the buy zone: when it is pinned to the bottom of the
|
|
2801
|
+
panel (desktop, or a host's sticky), it can never cover them — they
|
|
2802
|
+
travel with the buttons they gate (Mote & Livsstil, 2026-10-28). */
|
|
2803
|
+
.buy-options {
|
|
2804
|
+
display: flex;
|
|
2805
|
+
flex-direction: column;
|
|
2806
|
+
gap: var(--vio-detail-info-gap, 20px);
|
|
2807
|
+
padding-bottom: 4px;
|
|
2808
|
+
}
|
|
2809
|
+
/* Where the express buttons go while the brand's methods load: neutral,
|
|
2810
|
+
the height of one button, no method named. */
|
|
2811
|
+
.buy-pending {
|
|
2812
|
+
height: var(--vio-detail-button-height, 52px);
|
|
2813
|
+
border-radius: var(--vio-detail-button-radius, 0);
|
|
2814
|
+
background: linear-gradient(
|
|
2815
|
+
100deg,
|
|
2816
|
+
var(--vio-color-surface-muted, #f1f1f1) 30%,
|
|
2817
|
+
rgba(255, 255, 255, 0.65) 50%,
|
|
2818
|
+
var(--vio-color-surface-muted, #f1f1f1) 70%
|
|
2819
|
+
);
|
|
2820
|
+
background-size: 200% 100%;
|
|
2821
|
+
animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
|
|
2822
|
+
}
|
|
2823
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2824
|
+
.buy-pending { animation: none; background: var(--vio-color-surface-muted, #f1f1f1); }
|
|
2825
|
+
}
|
|
2291
2826
|
/* Under the buttons: who the shopper is buying from. */
|
|
2292
2827
|
.seller-note {
|
|
2293
2828
|
margin: 2px 0 0;
|
|
@@ -2420,6 +2955,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
2420
2955
|
}
|
|
2421
2956
|
.buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
|
|
2422
2957
|
.buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2958
|
+
/* Under the Express button: what happens in the app. */
|
|
2959
|
+
.vipps-note {
|
|
2960
|
+
margin: 0;
|
|
2961
|
+
font-size: 12px;
|
|
2962
|
+
line-height: 1.4;
|
|
2963
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2964
|
+
text-align: center;
|
|
2965
|
+
}
|
|
2423
2966
|
/* Vipps' own button (web component) — same slot, their drawing. */
|
|
2424
2967
|
.buy-vipps-official { display: block; width: 100%; margin-top: 10px; cursor: pointer; }
|
|
2425
2968
|
/* Apple Pay express button — black, per Apple's button guidelines. Only
|
|
@@ -2471,6 +3014,61 @@ var VioProductDetail = class extends LitElement {
|
|
|
2471
3014
|
transition: transform 0.15s ease;
|
|
2472
3015
|
}
|
|
2473
3016
|
.description-block[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
|
|
3017
|
+
/* «Beskrivelse» with a preview: the same heading as the folds, then the
|
|
3018
|
+
first lines fading out, then «Les mer». */
|
|
3019
|
+
.desc-head {
|
|
3020
|
+
all: unset;
|
|
3021
|
+
box-sizing: border-box;
|
|
3022
|
+
width: 100%;
|
|
3023
|
+
cursor: pointer;
|
|
3024
|
+
display: flex;
|
|
3025
|
+
align-items: center;
|
|
3026
|
+
justify-content: space-between;
|
|
3027
|
+
padding: 14px 0 6px;
|
|
3028
|
+
font-size: 11px;
|
|
3029
|
+
text-transform: uppercase;
|
|
3030
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
3031
|
+
color: var(--vio-color-text-secondary, #666);
|
|
3032
|
+
font-weight: 600;
|
|
3033
|
+
font-family: inherit;
|
|
3034
|
+
}
|
|
3035
|
+
.description-preview.clipped .desc-head::after {
|
|
3036
|
+
content: '';
|
|
3037
|
+
width: 8px;
|
|
3038
|
+
height: 8px;
|
|
3039
|
+
border-right: 1.5px solid currentColor;
|
|
3040
|
+
border-bottom: 1.5px solid currentColor;
|
|
3041
|
+
transform: rotate(45deg) translateY(-2px);
|
|
3042
|
+
transition: transform 0.15s ease;
|
|
3043
|
+
}
|
|
3044
|
+
.description-preview.open .desc-head::after { transform: rotate(-135deg) translateY(-2px); }
|
|
3045
|
+
.description-preview .description { padding-bottom: 6px; }
|
|
3046
|
+
.description-preview.folded .desc-head, .description-preview.open:not(.clipped) .desc-head { padding-bottom: 14px; }
|
|
3047
|
+
.description-preview.folded.open .desc-head { padding-bottom: 6px; }
|
|
3048
|
+
.description-preview.folded .description { padding-bottom: 14px; }
|
|
3049
|
+
.description-preview:not(.clipped) .description { padding-bottom: 14px; }
|
|
3050
|
+
.description.collapsed {
|
|
3051
|
+
display: -webkit-box;
|
|
3052
|
+
-webkit-box-orient: vertical;
|
|
3053
|
+
-webkit-line-clamp: var(--vio-detail-description-preview-lines, 3);
|
|
3054
|
+
line-clamp: var(--vio-detail-description-preview-lines, 3);
|
|
3055
|
+
overflow: hidden;
|
|
3056
|
+
cursor: pointer;
|
|
3057
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent);
|
|
3058
|
+
mask-image: linear-gradient(to bottom, #000 45%, transparent);
|
|
3059
|
+
}
|
|
3060
|
+
.desc-more {
|
|
3061
|
+
all: unset;
|
|
3062
|
+
cursor: pointer;
|
|
3063
|
+
display: inline-block;
|
|
3064
|
+
margin: 0 0 14px;
|
|
3065
|
+
font-size: 13px;
|
|
3066
|
+
font-weight: 600;
|
|
3067
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3068
|
+
text-decoration: underline;
|
|
3069
|
+
text-underline-offset: 3px;
|
|
3070
|
+
font-family: inherit;
|
|
3071
|
+
}
|
|
2474
3072
|
.description {
|
|
2475
3073
|
font-size: 14px;
|
|
2476
3074
|
line-height: 1.6;
|
|
@@ -2478,15 +3076,13 @@ var VioProductDetail = class extends LitElement {
|
|
|
2478
3076
|
padding-bottom: 14px;
|
|
2479
3077
|
white-space: pre-line;
|
|
2480
3078
|
}
|
|
2481
|
-
/*
|
|
2482
|
-
(
|
|
3079
|
+
/* Same order on every screen: buy, then the description at the very
|
|
3080
|
+
bottom (folded). Hosts that want the buy zone pinned can still set
|
|
3081
|
+
--vio-detail-buy-position: sticky. */
|
|
2483
3082
|
@media (min-width: 601px) {
|
|
2484
|
-
.description-block { order: 1; }
|
|
2485
3083
|
.buy {
|
|
2486
|
-
|
|
2487
|
-
position: var(--vio-detail-buy-position, sticky);
|
|
3084
|
+
position: var(--vio-detail-buy-position, static);
|
|
2488
3085
|
padding: var(--vio-detail-buy-padding, 12px 0 16px);
|
|
2489
|
-
box-shadow: var(--vio-detail-buy-shadow, 0 -1px 0 var(--vio-color-border, #e5e5e5));
|
|
2490
3086
|
}
|
|
2491
3087
|
}
|
|
2492
3088
|
|
|
@@ -2526,6 +3122,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
2526
3122
|
background: var(--vio-color-surface-muted, #f1f1f1);
|
|
2527
3123
|
}
|
|
2528
3124
|
}
|
|
3125
|
+
/* «Lagt i handlekurven»: above the page, below where a thumb reaches. */
|
|
3126
|
+
.added-toast {
|
|
3127
|
+
position: fixed;
|
|
3128
|
+
left: 50%;
|
|
3129
|
+
bottom: max(16px, env(safe-area-inset-bottom));
|
|
3130
|
+
transform: translateX(-50%);
|
|
3131
|
+
z-index: 1102;
|
|
3132
|
+
width: min(420px, calc(100vw - 32px));
|
|
3133
|
+
box-sizing: border-box;
|
|
3134
|
+
padding: 14px 16px;
|
|
3135
|
+
background: var(--vio-color-surface, #fff);
|
|
3136
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3137
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
3138
|
+
border-radius: var(--vio-radius-lg, 8px);
|
|
3139
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
|
3140
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
3141
|
+
display: flex;
|
|
3142
|
+
flex-direction: column;
|
|
3143
|
+
gap: 12px;
|
|
3144
|
+
}
|
|
3145
|
+
.added-toast-text { font-size: 14px; font-weight: 600; }
|
|
3146
|
+
.added-toast-text::before { content: '✓ '; color: #2e7d32; }
|
|
3147
|
+
.added-toast-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
3148
|
+
.added-toast-actions button {
|
|
3149
|
+
min-height: 44px;
|
|
3150
|
+
padding: 0 12px;
|
|
3151
|
+
font-family: inherit;
|
|
3152
|
+
font-size: 13px;
|
|
3153
|
+
font-weight: 600;
|
|
3154
|
+
cursor: pointer;
|
|
3155
|
+
border-radius: var(--vio-detail-button-radius, 0);
|
|
3156
|
+
}
|
|
3157
|
+
.added-toast-checkout {
|
|
3158
|
+
background: var(--vio-color-text, #0a0a0a);
|
|
3159
|
+
color: var(--vio-color-text-on-primary, #fff);
|
|
3160
|
+
border: 1px solid var(--vio-color-text, #0a0a0a);
|
|
3161
|
+
}
|
|
3162
|
+
.added-toast-continue {
|
|
3163
|
+
background: transparent;
|
|
3164
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3165
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
2529
3168
|
.error {
|
|
2530
3169
|
padding: 80px 24px;
|
|
2531
3170
|
text-align: center;
|
|
@@ -2541,6 +3180,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
2541
3180
|
document.addEventListener("vio:product-click", this.boundProductClick);
|
|
2542
3181
|
}
|
|
2543
3182
|
disconnectedCallback() {
|
|
3183
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3184
|
+
this.addedToastTimer = null;
|
|
2544
3185
|
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
2545
3186
|
this.slotRegistration.clear();
|
|
2546
3187
|
this.bottomSheet.disconnect();
|
|
@@ -2551,13 +3192,63 @@ var VioProductDetail = class extends LitElement {
|
|
|
2551
3192
|
this.dialogFocus.update(this.open);
|
|
2552
3193
|
this.bottomSheet.update(this.open);
|
|
2553
3194
|
}
|
|
2554
|
-
if (changed.has("product") || changed.has("open"))
|
|
2555
|
-
|
|
3195
|
+
if (changed.has("product") || changed.has("open")) {
|
|
3196
|
+
this.measureThumbs();
|
|
3197
|
+
this.measureDescription();
|
|
3198
|
+
}
|
|
3199
|
+
if ((changed.has("quantity") || changed.has("selectedOptions")) && this.product && this.applePayAskedFor !== null) {
|
|
3200
|
+
if (this.unitPrice * this.quantity !== this.applePayAskedFor) this.prepareApplePaySheet();
|
|
3201
|
+
}
|
|
3202
|
+
if (changed.has("activeImageIndex")) {
|
|
3203
|
+
this.revealActiveThumb();
|
|
3204
|
+
this.revealActiveSlide();
|
|
3205
|
+
}
|
|
3206
|
+
if (changed.has("selectedOptions") && this.galleryMode === "carousel") {
|
|
3207
|
+
if (this.activeImageIndex !== 0) this.activeImageIndex = 0;
|
|
3208
|
+
else this.revealActiveSlide();
|
|
3209
|
+
}
|
|
2556
3210
|
this.measureLogo();
|
|
2557
3211
|
if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
|
|
2558
3212
|
this.slotRegistration.sync(this.slotAttributes());
|
|
2559
3213
|
}
|
|
2560
3214
|
}
|
|
3215
|
+
willUpdate(changed) {
|
|
3216
|
+
if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
|
|
3217
|
+
if (changed.has("descriptionMode") || changed.has("open") && this.open) {
|
|
3218
|
+
const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
|
|
3219
|
+
this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
/** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
|
|
3223
|
+
themeChoice(own, cssVar) {
|
|
3224
|
+
const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
|
|
3225
|
+
return (own || fromTheme).trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
3226
|
+
}
|
|
3227
|
+
/** The attribute wins; else the theme's `--vio-detail-gallery`; else thumbnails. */
|
|
3228
|
+
resolveGalleryMode() {
|
|
3229
|
+
return this.themeChoice(this.gallery, "--vio-detail-gallery") === "carousel" ? "carousel" : "thumbs";
|
|
3230
|
+
}
|
|
3231
|
+
carouselTrack() {
|
|
3232
|
+
return this.renderRoot.querySelector(".carousel-track");
|
|
3233
|
+
}
|
|
3234
|
+
/** One photo's width plus the gap (with a peek a photo is narrower than the row). */
|
|
3235
|
+
carouselStep(track) {
|
|
3236
|
+
const slide = track?.firstElementChild;
|
|
3237
|
+
if (!track || !slide || slide.offsetWidth === 0) return 0;
|
|
3238
|
+
return slide.offsetWidth + (parseFloat(getComputedStyle(track).columnGap) || 0);
|
|
3239
|
+
}
|
|
3240
|
+
/** Bring the active photo into view — only when it is not already the one a swipe is on. */
|
|
3241
|
+
revealActiveSlide() {
|
|
3242
|
+
const track = this.carouselTrack();
|
|
3243
|
+
const step = this.carouselStep(track);
|
|
3244
|
+
if (!track || !step) return;
|
|
3245
|
+
const target = Math.min(this.activeImageIndex * step, track.scrollWidth - track.clientWidth);
|
|
3246
|
+
if (Math.abs(track.scrollLeft - target) > step / 2) track.scrollTo({ left: target });
|
|
3247
|
+
}
|
|
3248
|
+
stepCarousel(direction) {
|
|
3249
|
+
const last = this.allImages().length - 1;
|
|
3250
|
+
this.activeImageIndex = Math.min(last, Math.max(0, this.activeImageIndex + direction));
|
|
3251
|
+
}
|
|
2561
3252
|
thumbRow() {
|
|
2562
3253
|
return this.renderRoot.querySelector(".gallery-thumbs");
|
|
2563
3254
|
}
|
|
@@ -2586,6 +3277,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2586
3277
|
}
|
|
2587
3278
|
show() {
|
|
2588
3279
|
this.open = true;
|
|
3280
|
+
this.hideAddedToast();
|
|
2589
3281
|
}
|
|
2590
3282
|
close() {
|
|
2591
3283
|
this.open = false;
|
|
@@ -2599,9 +3291,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
2599
3291
|
this.product = null;
|
|
2600
3292
|
this.sellerName = null;
|
|
2601
3293
|
this.sellerSettled = false;
|
|
3294
|
+
this.shippingFrom = null;
|
|
2602
3295
|
this.selectedOptions = {};
|
|
3296
|
+
this.selectionPrompt = "";
|
|
2603
3297
|
this.quantity = 1;
|
|
2604
3298
|
this.activeImageIndex = 0;
|
|
3299
|
+
this.descriptionOpen = false;
|
|
2605
3300
|
this.fetchError = "";
|
|
2606
3301
|
}
|
|
2607
3302
|
}, 400);
|
|
@@ -2617,6 +3312,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2617
3312
|
this.fetchError = "";
|
|
2618
3313
|
this.sellerName = null;
|
|
2619
3314
|
this.sellerSettled = false;
|
|
3315
|
+
this.shippingFrom = null;
|
|
2620
3316
|
let products = null;
|
|
2621
3317
|
let lastError = null;
|
|
2622
3318
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
@@ -2646,6 +3342,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2646
3342
|
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
2647
3343
|
this.product = products?.[0] ?? null;
|
|
2648
3344
|
if (this.product) void this.loadSeller(productIdNum, seq);
|
|
3345
|
+
if (this.product) void this.loadShippingFrom(productIdNum, seq);
|
|
2649
3346
|
if (this.product) {
|
|
2650
3347
|
try {
|
|
2651
3348
|
const price = Number(
|
|
@@ -2678,16 +3375,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
2678
3375
|
const initial = {};
|
|
2679
3376
|
for (const opt of this.product.options ?? []) {
|
|
2680
3377
|
const values = normalizeValues(opt.values);
|
|
2681
|
-
if (values.length
|
|
3378
|
+
if (values.length === 1) initial[opt.name] = values[0];
|
|
2682
3379
|
}
|
|
2683
3380
|
this.selectedOptions = initial;
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
this.applePayOk = handle.available;
|
|
2687
|
-
}).catch(() => {
|
|
2688
|
-
this.applePayHandle = null;
|
|
2689
|
-
this.applePayOk = false;
|
|
2690
|
-
});
|
|
3381
|
+
this.selectionPrompt = "";
|
|
3382
|
+
this.prepareApplePaySheet();
|
|
2691
3383
|
void this.loadAvailablePaymentMethods();
|
|
2692
3384
|
}
|
|
2693
3385
|
this.isLoading = false;
|
|
@@ -2696,22 +3388,41 @@ var VioProductDetail = class extends LitElement {
|
|
|
2696
3388
|
async loadAvailablePaymentMethods() {
|
|
2697
3389
|
const spId = this.sponsorId || (typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.activeSlot || null, { campaignId: this.activeCampaignHint })?.primarySponsor?.id) : 0) || globalThis.__VIO_SPONSOR_ID__;
|
|
2698
3390
|
if (!spId) return;
|
|
3391
|
+
if (spId !== this.methodsSponsorId) {
|
|
3392
|
+
this.availableMethods = null;
|
|
3393
|
+
this.vippsExpress = false;
|
|
3394
|
+
}
|
|
3395
|
+
this.methodsSponsorId = spId;
|
|
3396
|
+
this.methodsLoading = true;
|
|
2699
3397
|
try {
|
|
2700
3398
|
const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
|
|
2701
3399
|
if (Array.isArray(methods)) {
|
|
2702
3400
|
this.availableMethods = methods.map((m) => m.name);
|
|
3401
|
+
const vipps = methods.find(
|
|
3402
|
+
(m) => String(m?.name ?? "").toLowerCase() === "vipps"
|
|
3403
|
+
);
|
|
3404
|
+
this.vippsExpress = !!vipps && vipps.config?.find((c) => c?.name === "express")?.value !== "false";
|
|
2703
3405
|
} else {
|
|
2704
3406
|
this.availableMethods = null;
|
|
3407
|
+
this.vippsExpress = false;
|
|
2705
3408
|
}
|
|
2706
3409
|
} catch (err) {
|
|
2707
3410
|
if (typeof console !== "undefined") {
|
|
2708
3411
|
console.warn("[VioProductDetail] Failed to load payment methods:", err);
|
|
2709
3412
|
}
|
|
2710
3413
|
this.availableMethods = null;
|
|
3414
|
+
this.vippsExpress = false;
|
|
3415
|
+
} finally {
|
|
3416
|
+
this.methodsLoading = false;
|
|
2711
3417
|
}
|
|
2712
3418
|
}
|
|
3419
|
+
/** The methods are still on their way: a placeholder stands where the buttons go. */
|
|
3420
|
+
get methodsPending() {
|
|
3421
|
+
return this.methodsLoading && this.availableMethods === null;
|
|
3422
|
+
}
|
|
2713
3423
|
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2714
3424
|
methodEnabled(...names) {
|
|
3425
|
+
if (this.methodsPending) return false;
|
|
2715
3426
|
return isMethodEnabled(this.availableMethods, ...names);
|
|
2716
3427
|
}
|
|
2717
3428
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -2900,26 +3611,92 @@ var VioProductDetail = class extends LitElement {
|
|
|
2900
3611
|
const price = v?.price ?? this.product?.price;
|
|
2901
3612
|
return price?.amount_incl_taxes ?? price?.amount ?? 0;
|
|
2902
3613
|
}
|
|
3614
|
+
/**
|
|
3615
|
+
* There is something to pay for — whether the express buttons show. With
|
|
3616
|
+
* every option chosen, the chosen variant's price; before that, the
|
|
3617
|
+
* product's or any variant's: the first variant (what `unitPrice` falls
|
|
3618
|
+
* back to) at 0 must not hide Vipps for a size that costs 699.
|
|
3619
|
+
*/
|
|
3620
|
+
get hasPrice() {
|
|
3621
|
+
if (!this.missingOption()) return this.unitPrice > 0;
|
|
3622
|
+
const priceOf = (p) => Number(p?.amount_incl_taxes ?? p?.amount ?? 0);
|
|
3623
|
+
return priceOf(this.product?.price) > 0 || (this.product?.variants ?? []).some((v) => priceOf(v.price) > 0);
|
|
3624
|
+
}
|
|
2903
3625
|
get compareAt() {
|
|
2904
3626
|
const v = this.selectedVariant();
|
|
2905
3627
|
const price = v?.price ?? this.product?.price;
|
|
2906
3628
|
return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
|
|
2907
3629
|
}
|
|
3630
|
+
/** The options the shopper has to choose (those with values), in order. */
|
|
3631
|
+
choosableOptions() {
|
|
3632
|
+
return (this.product?.options ?? []).filter((o) => normalizeValues(o.values).length > 0);
|
|
3633
|
+
}
|
|
3634
|
+
/** The first option still without a value, or null when every one is chosen. */
|
|
3635
|
+
missingOption() {
|
|
3636
|
+
return this.choosableOptions().find((o) => !String(this.selectedOptions[o.name] ?? "").trim()) ?? null;
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Units that can be sold. With every option chosen, the chosen variant's;
|
|
3640
|
+
* before that, the most any variant still matching the partial choice has
|
|
3641
|
+
* — so the stepper and «Utsolgt» do not speak for a variant (the first
|
|
3642
|
+
* one) the shopper never picked.
|
|
3643
|
+
*/
|
|
2908
3644
|
get availableQuantity() {
|
|
3645
|
+
if (this.missingOption() && Array.isArray(this.product?.variants) && this.product.variants.length > 0) {
|
|
3646
|
+
const sel = this.selectedOptions;
|
|
3647
|
+
const qtys = this.product.variants.filter((v2) => this.variantMatchesSelection(v2, sel)).map((v2) => this.variantSellableQty(v2));
|
|
3648
|
+
return qtys.length > 0 ? Math.max(0, ...qtys) : 0;
|
|
3649
|
+
}
|
|
2909
3650
|
const v = this.selectedVariant();
|
|
2910
3651
|
if (v) return this.variantSellableQty(v);
|
|
2911
3652
|
return this.product?.quantity ?? 0;
|
|
2912
3653
|
}
|
|
3654
|
+
/**
|
|
3655
|
+
* «Få igjen — N stk» only for the variant actually chosen: before a size is
|
|
3656
|
+
* picked the count belonged to the first variant, which said «Få igjen» of
|
|
3657
|
+
* a product with plenty of other sizes.
|
|
3658
|
+
*/
|
|
2913
3659
|
stockLabel() {
|
|
2914
3660
|
const qty = this.availableQuantity;
|
|
2915
3661
|
if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
|
|
2916
|
-
if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
3662
|
+
if (qty <= 3 && !this.missingOption()) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
2917
3663
|
return { text: "P\xE5 lager", cls: "in-stock" };
|
|
2918
3664
|
}
|
|
3665
|
+
/** «Velg størrelse» for a size, «Velg farge» for a colour… */
|
|
3666
|
+
promptFor(option) {
|
|
3667
|
+
const name = String(option.name ?? "").trim();
|
|
3668
|
+
if (!name || /^(st(ø|o)rrelse|str\.?|size)$/i.test(name)) return "Velg st\xF8rrelse";
|
|
3669
|
+
return `Velg ${name.toLowerCase()}`;
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Every buy button goes through here first: with an option not chosen yet,
|
|
3673
|
+
* nothing is added or paid — the picker says what is missing and comes
|
|
3674
|
+
* into view (it sits right above the buttons, inside the buy zone).
|
|
3675
|
+
*/
|
|
3676
|
+
requireSelection() {
|
|
3677
|
+
const missing = this.missingOption();
|
|
3678
|
+
if (!missing) {
|
|
3679
|
+
this.selectionPrompt = "";
|
|
3680
|
+
return true;
|
|
3681
|
+
}
|
|
3682
|
+
this.selectionPrompt = missing.name;
|
|
3683
|
+
void this.updateComplete.then(() => {
|
|
3684
|
+
const group = this.renderRoot.querySelector(".option-group.needs-choice");
|
|
3685
|
+
group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
|
|
3686
|
+
});
|
|
3687
|
+
return false;
|
|
3688
|
+
}
|
|
2919
3689
|
/** Pick an option value. If the exact combination doesn't exist, jump to an
|
|
2920
3690
|
* in-stock variant that carries the value and adopt its full option map
|
|
2921
3691
|
* (keeps the selection always resolvable to a real variant). */
|
|
2922
3692
|
selectOption(optionName, value) {
|
|
3693
|
+
this.applyOption(optionName, value);
|
|
3694
|
+
if (this.selectionPrompt && !this.missingOption()) this.selectionPrompt = "";
|
|
3695
|
+
else if (this.selectionPrompt) this.selectionPrompt = this.missingOption()?.name ?? "";
|
|
3696
|
+
const max = this.availableQuantity;
|
|
3697
|
+
if (max > 0 && this.quantity > max) this.quantity = max;
|
|
3698
|
+
}
|
|
3699
|
+
applyOption(optionName, value) {
|
|
2923
3700
|
const newSelection = { ...this.selectedOptions, [optionName]: value };
|
|
2924
3701
|
const exactInStock = this.product?.variants?.find(
|
|
2925
3702
|
(v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
|
|
@@ -2987,6 +3764,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2987
3764
|
}
|
|
2988
3765
|
async addToCart() {
|
|
2989
3766
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3767
|
+
if (!this.requireSelection()) return;
|
|
2990
3768
|
this.adding = true;
|
|
2991
3769
|
try {
|
|
2992
3770
|
const variantId = this.addCurrentToCart();
|
|
@@ -3003,11 +3781,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
3003
3781
|
}
|
|
3004
3782
|
})
|
|
3005
3783
|
);
|
|
3784
|
+
this.showAddedToast();
|
|
3006
3785
|
this.close();
|
|
3007
3786
|
} finally {
|
|
3008
3787
|
this.adding = false;
|
|
3009
3788
|
}
|
|
3010
3789
|
}
|
|
3790
|
+
/** The toast after an add; it goes by itself after a while. */
|
|
3791
|
+
showAddedToast() {
|
|
3792
|
+
if (this.noAddedToast) return;
|
|
3793
|
+
this.addedToast = {
|
|
3794
|
+
sponsorId: this.sponsorId,
|
|
3795
|
+
detail: { sponsorId: this.sponsorId, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
|
|
3796
|
+
};
|
|
3797
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3798
|
+
this.addedToastTimer = setTimeout(() => this.hideAddedToast(), ADDED_TOAST_MS);
|
|
3799
|
+
}
|
|
3800
|
+
hideAddedToast() {
|
|
3801
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3802
|
+
this.addedToastTimer = null;
|
|
3803
|
+
this.addedToast = null;
|
|
3804
|
+
}
|
|
3805
|
+
/** «Til kassen»: the checkout of the brand just added to — the host opens it on `vio:checkout-open`, as from the cart. */
|
|
3806
|
+
toastToCheckout() {
|
|
3807
|
+
const toast = this.addedToast;
|
|
3808
|
+
this.hideAddedToast();
|
|
3809
|
+
if (!toast) return;
|
|
3810
|
+
const cart = Vio.cart.getCart(toast.sponsorId);
|
|
3811
|
+
const total = (cart?.items ?? []).reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
3812
|
+
if (!cart || cart.items.length === 0 || !(total > 0)) return;
|
|
3813
|
+
this.dispatchEvent(new CustomEvent("vio:checkout-open", { bubbles: true, composed: true, detail: toast.detail }));
|
|
3814
|
+
}
|
|
3815
|
+
renderAddedToast() {
|
|
3816
|
+
if (!this.addedToast) return nothing;
|
|
3817
|
+
return html`
|
|
3818
|
+
<div class="added-toast" part="added-toast" role="status" aria-live="polite">
|
|
3819
|
+
<div class="added-toast-text">Lagt i handlekurven</div>
|
|
3820
|
+
<div class="added-toast-actions">
|
|
3821
|
+
<button class="added-toast-checkout" part="added-toast-checkout" @click=${this.toastToCheckout}>Til kassen</button>
|
|
3822
|
+
<button class="added-toast-continue" part="added-toast-continue" @click=${this.hideAddedToast}>Fortsett å handle</button>
|
|
3823
|
+
</div>
|
|
3824
|
+
</div>
|
|
3825
|
+
`;
|
|
3826
|
+
}
|
|
3011
3827
|
/**
|
|
3012
3828
|
* Express buy: add to cart, then open the checkout overlay with Klarna
|
|
3013
3829
|
* preselected. The host app handles `vio:checkout-open` (same event the cart
|
|
@@ -3016,6 +3832,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3016
3832
|
*/
|
|
3017
3833
|
async buyWithKlarna() {
|
|
3018
3834
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3835
|
+
if (!this.requireSelection()) return;
|
|
3019
3836
|
this.adding = true;
|
|
3020
3837
|
try {
|
|
3021
3838
|
this.addCurrentToCart();
|
|
@@ -3048,6 +3865,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3048
3865
|
*/
|
|
3049
3866
|
buyNow() {
|
|
3050
3867
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3868
|
+
if (!this.requireSelection()) return;
|
|
3051
3869
|
this.addCurrentToCart();
|
|
3052
3870
|
this.close();
|
|
3053
3871
|
this.dispatchEvent(
|
|
@@ -3070,11 +3888,27 @@ var VioProductDetail = class extends LitElement {
|
|
|
3070
3888
|
}
|
|
3071
3889
|
buyWithVipps() {
|
|
3072
3890
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3891
|
+
if (!this.requireSelection()) return;
|
|
3892
|
+
if (!(this.unitPrice > 0)) return;
|
|
3893
|
+
if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
|
|
3894
|
+
this.addCurrentToCart();
|
|
3895
|
+
this.close();
|
|
3896
|
+
this.openCheckoutWithVipps();
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3073
3899
|
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.sponsorId, source: "detail" });
|
|
3074
3900
|
this.addCurrentToCart();
|
|
3075
3901
|
this.close();
|
|
3076
3902
|
void this.startVippsExpressOrCheckout(redirect);
|
|
3077
3903
|
}
|
|
3904
|
+
/**
|
|
3905
|
+
* The Vipps button will open the app with Express (see buyWithVipps): the
|
|
3906
|
+
* brand has Express on, and this brand's cart is empty — with lines in it
|
|
3907
|
+
* the checkout opens first.
|
|
3908
|
+
*/
|
|
3909
|
+
vippsGoesStraightToApp() {
|
|
3910
|
+
return this.vippsExpress && (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) === 0;
|
|
3911
|
+
}
|
|
3078
3912
|
async startVippsExpressOrCheckout(redirect) {
|
|
3079
3913
|
let fallback = true;
|
|
3080
3914
|
try {
|
|
@@ -3092,17 +3926,26 @@ var VioProductDetail = class extends LitElement {
|
|
|
3092
3926
|
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
3093
3927
|
}
|
|
3094
3928
|
if (!fallback) return;
|
|
3929
|
+
this.openCheckoutWithVipps();
|
|
3930
|
+
}
|
|
3931
|
+
/** The ordinary way in, Vipps preselected: the host opens the checkout on `vio:checkout-open`. */
|
|
3932
|
+
openCheckoutWithVipps() {
|
|
3933
|
+
this.openCheckoutWith("vipps");
|
|
3934
|
+
}
|
|
3935
|
+
/** The ordinary way in with a method preselected: the host opens the checkout on `vio:checkout-open`. */
|
|
3936
|
+
openCheckoutWith(paymentMethod) {
|
|
3095
3937
|
this.dispatchEvent(
|
|
3096
3938
|
new CustomEvent("vio:checkout-open", {
|
|
3097
3939
|
bubbles: true,
|
|
3098
3940
|
composed: true,
|
|
3099
|
-
detail: { sponsorId: this.sponsorId, paymentMethod
|
|
3941
|
+
detail: { sponsorId: this.sponsorId, paymentMethod, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
|
|
3100
3942
|
})
|
|
3101
3943
|
);
|
|
3102
3944
|
}
|
|
3103
3945
|
/** Buy with Stripe — opens the checkout with Stripe preselected. */
|
|
3104
3946
|
buyWithStripe() {
|
|
3105
3947
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3948
|
+
if (!this.requireSelection()) return;
|
|
3106
3949
|
this.addCurrentToCart();
|
|
3107
3950
|
this.close();
|
|
3108
3951
|
this.dispatchEvent(
|
|
@@ -3113,14 +3956,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
3113
3956
|
})
|
|
3114
3957
|
);
|
|
3115
3958
|
}
|
|
3959
|
+
/** Prepare the Apple Pay sheet for this product × quantity; the newest preparation wins. */
|
|
3960
|
+
prepareApplePaySheet() {
|
|
3961
|
+
const amount = this.unitPrice * this.quantity;
|
|
3962
|
+
this.applePayAskedFor = amount;
|
|
3963
|
+
void Vio.checkout.prepareApplePayFor(amount, this.currency).then((handle) => {
|
|
3964
|
+
if (this.applePayAskedFor !== amount) return;
|
|
3965
|
+
this.applePayHandle = handle;
|
|
3966
|
+
this.applePayAmount = amount;
|
|
3967
|
+
this.applePayOk = handle.available;
|
|
3968
|
+
}).catch(() => {
|
|
3969
|
+
if (this.applePayAskedFor !== amount) return;
|
|
3970
|
+
this.applePayHandle = null;
|
|
3971
|
+
this.applePayAmount = null;
|
|
3972
|
+
this.applePayOk = false;
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3116
3975
|
/**
|
|
3117
3976
|
* Express buy with Apple Pay. Runs synchronously inside the click so the
|
|
3118
3977
|
* Apple Pay sheet keeps the user gesture: add to cart → open checkout (sets
|
|
3119
3978
|
* state) → launch the sheet. On authorize, `payment-complete` drives the
|
|
3120
3979
|
* confirmation drawer (same path as the in-checkout Apple Pay button).
|
|
3980
|
+
*
|
|
3981
|
+
* The sheet's amount is fixed before the tap (Apple blocks a sheet opened
|
|
3982
|
+
* after an await, so it cannot wait for the server's cart), while the
|
|
3983
|
+
* charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
|
|
3984
|
+
* what will be charged: an empty cart, and a sheet prepared for this
|
|
3985
|
+
* product × quantity. Otherwise — lines already in the cart (as with Vipps,
|
|
3986
|
+
* see buyWithVipps), or a quantity whose sheet is still being prepared —
|
|
3987
|
+
* the product is added and the checkout opens with Apple Pay preselected:
|
|
3988
|
+
* every line and the total on screen, and its own Apple Pay button pays
|
|
3989
|
+
* that total.
|
|
3121
3990
|
*/
|
|
3122
3991
|
buyWithApplePay() {
|
|
3123
3992
|
if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
|
|
3993
|
+
if (!this.requireSelection()) return;
|
|
3994
|
+
if (!(this.unitPrice > 0)) return;
|
|
3995
|
+
const cartHasLines = (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
|
|
3996
|
+
if (cartHasLines || this.applePayAmount !== this.unitPrice * this.quantity) {
|
|
3997
|
+
this.addCurrentToCart();
|
|
3998
|
+
this.close();
|
|
3999
|
+
this.openCheckoutWith("apple-pay");
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
3124
4002
|
this.addCurrentToCart();
|
|
3125
4003
|
try {
|
|
3126
4004
|
Vio.checkout.open(this.sponsorId);
|
|
@@ -3133,6 +4011,175 @@ var VioProductDetail = class extends LitElement {
|
|
|
3133
4011
|
void this.applePayHandle.show();
|
|
3134
4012
|
this.close();
|
|
3135
4013
|
}
|
|
4014
|
+
/**
|
|
4015
|
+
* The product's cheapest rate for the page's market — the rates the
|
|
4016
|
+
* backend cart offers for its line. A rate in another currency than the
|
|
4017
|
+
* product's price is not shown (never a price the shopper would not pay).
|
|
4018
|
+
*/
|
|
4019
|
+
async loadShippingFrom(productId, seq) {
|
|
4020
|
+
let rates = null;
|
|
4021
|
+
try {
|
|
4022
|
+
const product = Vio.commerceFor(this.sponsorId).channel.product;
|
|
4023
|
+
rates = typeof product.getShippingRates === "function" ? await product.getShippingRates(productId, { currency: this.currency }) : null;
|
|
4024
|
+
} catch {
|
|
4025
|
+
rates = null;
|
|
4026
|
+
}
|
|
4027
|
+
if (seq !== this.fetchSeq) return;
|
|
4028
|
+
const currency = String(this.product?.price?.currency_code ?? this.currency ?? "").toUpperCase();
|
|
4029
|
+
const usable = (rates ?? []).filter((r) => !r.currency || r.currency.toUpperCase() === currency);
|
|
4030
|
+
const cheapest = usable[0];
|
|
4031
|
+
this.shippingFrom = cheapest ? { price: cheapest.price, currency: cheapest.currency || currency } : null;
|
|
4032
|
+
this.shippingRates = usable.map((r) => ({
|
|
4033
|
+
name: String(r.name ?? ""),
|
|
4034
|
+
price: r.price,
|
|
4035
|
+
currency: r.currency || currency,
|
|
4036
|
+
description: String(r.description ?? "").trim(),
|
|
4037
|
+
logoUrl: String(r.logoUrl ?? "")
|
|
4038
|
+
}));
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* `gallery="carousel"`: every photo in one row that snaps one at a time —
|
|
4042
|
+
* swipe on touch, arrows with a mouse — and a dot per photo over it
|
|
4043
|
+
* instead of thumbnails, so the name, price and sizes come up higher.
|
|
4044
|
+
*/
|
|
4045
|
+
renderCarousel(images, title) {
|
|
4046
|
+
const count = images.length;
|
|
4047
|
+
const current2 = Math.min(this.activeImageIndex, Math.max(0, count - 1));
|
|
4048
|
+
return html`
|
|
4049
|
+
<div class="gallery carousel" part="gallery">
|
|
4050
|
+
<div class="carousel-track" part="image" @scroll=${this.onCarouselScroll}>
|
|
4051
|
+
${images.map(
|
|
4052
|
+
(img, idx) => html`
|
|
4053
|
+
<div class="carousel-slide" part="slide" aria-label=${`Bilde ${idx + 1} av ${count}`}>
|
|
4054
|
+
<img src=${img.url} alt=${idx === 0 ? title : ""} loading=${idx === 0 ? "eager" : "lazy"} />
|
|
4055
|
+
</div>
|
|
4056
|
+
`
|
|
4057
|
+
)}
|
|
4058
|
+
</div>
|
|
4059
|
+
${count > 1 ? html`
|
|
4060
|
+
<button
|
|
4061
|
+
class="carousel-arrow prev"
|
|
4062
|
+
part="carousel-arrow"
|
|
4063
|
+
aria-label="Forrige bilde"
|
|
4064
|
+
?disabled=${current2 === 0}
|
|
4065
|
+
@click=${() => this.stepCarousel(-1)}
|
|
4066
|
+
>‹</button>
|
|
4067
|
+
<button
|
|
4068
|
+
class="carousel-arrow next"
|
|
4069
|
+
part="carousel-arrow"
|
|
4070
|
+
aria-label="Neste bilde"
|
|
4071
|
+
?disabled=${current2 === count - 1}
|
|
4072
|
+
@click=${() => this.stepCarousel(1)}
|
|
4073
|
+
>›</button>
|
|
4074
|
+
<div class="carousel-dots" part="image-dots" aria-hidden="true">
|
|
4075
|
+
${images.map((_, idx) => html`<span class="carousel-dot ${idx === current2 ? "active" : ""}"></span>`)}
|
|
4076
|
+
</div>
|
|
4077
|
+
` : ""}
|
|
4078
|
+
</div>
|
|
4079
|
+
`;
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* «Beskrivelse», not shut: the first lines show, fading out, and a tap on
|
|
4083
|
+
* the heading, the text or «Les mer» opens the rest (Angelo, 2026-10-28).
|
|
4084
|
+
* A description that fits in the preview has nothing to open.
|
|
4085
|
+
*/
|
|
4086
|
+
renderDescription(text) {
|
|
4087
|
+
const mode = this.resolvedDescriptionMode;
|
|
4088
|
+
if (mode === "open") {
|
|
4089
|
+
return html`<div class="description-block description-preview open" part="description">
|
|
4090
|
+
<div class="desc-head">Beskrivelse</div>
|
|
4091
|
+
<div class="description">${text}</div>
|
|
4092
|
+
</div>`;
|
|
4093
|
+
}
|
|
4094
|
+
const foldable = mode === "folded" || this.descriptionClipped;
|
|
4095
|
+
const open = this.descriptionOpen || !foldable;
|
|
4096
|
+
const toggle = () => {
|
|
4097
|
+
if (foldable) this.descriptionOpen = !this.descriptionOpen;
|
|
4098
|
+
};
|
|
4099
|
+
return html`<div class="description-block description-preview ${open ? "open" : ""} ${foldable ? "clipped" : ""} ${mode}" part="description">
|
|
4100
|
+
<button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>Beskrivelse</button>
|
|
4101
|
+
${mode === "folded" && !open ? nothing : html`<div class="description ${open ? "" : "collapsed"}" @click=${() => !this.descriptionOpen && toggle()}>${text}</div>`}
|
|
4102
|
+
${mode === "preview" && this.descriptionClipped ? html`<button class="desc-more" type="button" @click=${toggle}>${this.descriptionOpen ? "Vis mindre" : "Les mer"}</button>` : nothing}
|
|
4103
|
+
</div>`;
|
|
4104
|
+
}
|
|
4105
|
+
/** Is the preview hiding part of the text? Measured with the preview applied. */
|
|
4106
|
+
measureDescription() {
|
|
4107
|
+
const el = this.renderRoot.querySelector(".description-preview .description");
|
|
4108
|
+
if (!el || this.descriptionOpen) return;
|
|
4109
|
+
const wasCollapsed = el.classList.contains("collapsed");
|
|
4110
|
+
el.classList.add("collapsed");
|
|
4111
|
+
const clipped = el.scrollHeight > el.clientHeight + 1;
|
|
4112
|
+
if (!wasCollapsed) el.classList.remove("collapsed");
|
|
4113
|
+
if (clipped !== this.descriptionClipped) this.descriptionClipped = clipped;
|
|
4114
|
+
}
|
|
4115
|
+
renderHighlights() {
|
|
4116
|
+
const own = (Array.isArray(this.highlights) ? this.highlights : []).filter((h) => h && h.title);
|
|
4117
|
+
const items = own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? [];
|
|
4118
|
+
if (items.length === 0) return nothing;
|
|
4119
|
+
return html`<ul class="highlights" part="highlights">
|
|
4120
|
+
${items.map((h) => {
|
|
4121
|
+
const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
|
|
4122
|
+
const icon = path ? html`<svg class="hl-icon" viewBox="0 0 24 24" aria-hidden="true"><path d=${path}></path></svg>` : h.icon ? html`<img class="hl-icon" src=${h.icon} alt="" />` : nothing;
|
|
4123
|
+
return html`<li class="hl" part="highlight">
|
|
4124
|
+
${icon}
|
|
4125
|
+
<span class="hl-title">${h.title}</span>
|
|
4126
|
+
${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
|
|
4127
|
+
</li>`;
|
|
4128
|
+
})}
|
|
4129
|
+
</ul>`;
|
|
4130
|
+
}
|
|
4131
|
+
/**
|
|
4132
|
+
* «Frakt & retur», folded like «Beskrivelse»: the delivery options for the
|
|
4133
|
+
* page's country with their price, and the right of withdrawal with the
|
|
4134
|
+
* brand's own terms and returns pages when it gave them.
|
|
4135
|
+
*/
|
|
4136
|
+
renderShippingReturns() {
|
|
4137
|
+
const identity = resolveSponsorIdentity(this.sponsorId);
|
|
4138
|
+
const rates = this.shippingRates;
|
|
4139
|
+
const times = [...new Set(rates.map((r) => r.description).filter(Boolean))];
|
|
4140
|
+
const perRateTime = times.length > 1;
|
|
4141
|
+
const sharedTime = times.length === 1 ? times[0] : times.length === 0 && this.deliveryTime.trim() ? `Leveringstid: ${this.deliveryTime.trim()}` : "";
|
|
4142
|
+
const links = [
|
|
4143
|
+
identity?.refundPolicyUrl ? html`<a href=${identity.refundPolicyUrl} target="_blank" rel="noopener noreferrer">Angrerett og retur</a>` : null,
|
|
4144
|
+
identity?.termsUrl ? html`<a href=${identity.termsUrl} target="_blank" rel="noopener noreferrer">Kjøpsvilkår</a>` : null
|
|
4145
|
+
].filter(Boolean);
|
|
4146
|
+
return html`<details class="description-block shipping-returns" part="shipping-returns">
|
|
4147
|
+
<summary>Frakt & retur</summary>
|
|
4148
|
+
<div class="description shipping-returns-body">
|
|
4149
|
+
<div class="sr-title">Frakt</div>
|
|
4150
|
+
${rates.length ? html`<ul class="sr-rates">
|
|
4151
|
+
${rates.map((r) => {
|
|
4152
|
+
const { carrier, method } = splitRateName(r.name);
|
|
4153
|
+
const logo = r.logoUrl || carrierLogo(this.carrierLogos, carrier);
|
|
4154
|
+
return html`<li>
|
|
4155
|
+
${logo ? html`<img class="sr-logo" src=${logo} alt=${carrier} />` : lineIcon(rateIcon(r.name ?? ""), "line-icon sr-icon")}
|
|
4156
|
+
<span class="sr-rate">
|
|
4157
|
+
<span class="sr-carrier">${carrier || method || "Frakt"}</span>
|
|
4158
|
+
${carrier && method ? html`<span class="sr-method">${method}</span>` : nothing}
|
|
4159
|
+
${perRateTime && r.description ? html`<span class="sr-method">${r.description}</span>` : nothing}
|
|
4160
|
+
</span>
|
|
4161
|
+
<span class="sr-price">${r.price <= 0 ? "Gratis" : formatPrice(r.price, r.currency)}</span>
|
|
4162
|
+
</li>`;
|
|
4163
|
+
})}
|
|
4164
|
+
</ul>` : html`<p>Fraktprisen vises i kassen.</p>`}
|
|
4165
|
+
${sharedTime ? html`<p class="sr-line sr-time">${lineIcon("clock", "line-icon sr-icon")}<span>${sharedTime}</span></p>` : nothing}
|
|
4166
|
+
<div class="sr-title">Retur</div>
|
|
4167
|
+
<p class="sr-line">${lineIcon("return", "line-icon sr-icon")}<span>Du har 14 dagers angrerett fra du mottar varen.</span></p>
|
|
4168
|
+
${links.length ? html`<p class="sr-links">${links}</p>` : nothing}
|
|
4169
|
+
</div>
|
|
4170
|
+
</details>`;
|
|
4171
|
+
}
|
|
4172
|
+
/** Under the price: «Frakt fra X · 14 dagers angrerett» (or «Gratis frakt · …»). */
|
|
4173
|
+
renderDeliveryNote() {
|
|
4174
|
+
const ship = this.shippingFrom;
|
|
4175
|
+
const shipping = ship ? ship.price <= 0 ? "Gratis frakt" : `Frakt fra ${formatPrice(ship.price, ship.currency)}` : "";
|
|
4176
|
+
return html`<div class="delivery-note" part="delivery-note">
|
|
4177
|
+
${shipping ? html`<span part="delivery-shipping">${shipping}</span><span aria-hidden="true"> · </span>` : ""}<span
|
|
4178
|
+
part="delivery-withdrawal"
|
|
4179
|
+
>14 dagers angrerett</span
|
|
4180
|
+
>
|
|
4181
|
+
</div>`;
|
|
4182
|
+
}
|
|
3136
4183
|
/** The seller's Company name; the brand when there is none; nothing while asking. */
|
|
3137
4184
|
headerName() {
|
|
3138
4185
|
if (this.sellerName) return this.sellerName;
|
|
@@ -3215,6 +4262,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3215
4262
|
|
|
3216
4263
|
${this.renderBody()}
|
|
3217
4264
|
</div>
|
|
4265
|
+
${this.renderAddedToast()}
|
|
3218
4266
|
`;
|
|
3219
4267
|
}
|
|
3220
4268
|
renderBody() {
|
|
@@ -3242,7 +4290,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3242
4290
|
const activeImage = this.activeImage;
|
|
3243
4291
|
return html`
|
|
3244
4292
|
<div class="body">
|
|
3245
|
-
|
|
4293
|
+
${this.galleryMode === "carousel" ? this.renderCarousel(images, p.title) : html`<div class="gallery" part="gallery">
|
|
3246
4294
|
<div class="gallery-main" part="image">
|
|
3247
4295
|
${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
|
|
3248
4296
|
</div>
|
|
@@ -3279,26 +4327,30 @@ var VioProductDetail = class extends LitElement {
|
|
|
3279
4327
|
>›</button>
|
|
3280
4328
|
</div>
|
|
3281
4329
|
` : ""}
|
|
3282
|
-
</div
|
|
4330
|
+
</div>`}
|
|
3283
4331
|
|
|
3284
4332
|
<div class="info" part="info">
|
|
3285
4333
|
${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
|
|
3286
|
-
<
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
4334
|
+
<div class="headline">
|
|
4335
|
+
<div class="title-row">
|
|
4336
|
+
<h2 class="name" part="title">${p.title}</h2>
|
|
4337
|
+
<div class="stock ${stock.cls}" part="stock">${stock.text}</div>
|
|
4338
|
+
</div>
|
|
4339
|
+
<div class="price-row">
|
|
4340
|
+
<span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
4341
|
+
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
3291
4342
|
compare,
|
|
3292
4343
|
p.price.currency_code
|
|
3293
4344
|
)}</span>` : ""}
|
|
4345
|
+
</div>
|
|
3294
4346
|
</div>
|
|
3295
4347
|
|
|
3296
|
-
<div class="
|
|
3297
|
-
|
|
3298
|
-
${this.renderOptionPickers(p.options)}
|
|
3299
|
-
|
|
4348
|
+
<div class="purchase">
|
|
3300
4349
|
<div class="buy" part="buy">
|
|
3301
|
-
${
|
|
4350
|
+
${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
|
|
4351
|
+
${this.renderDeliveryNote()}
|
|
4352
|
+
${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
|
|
4353
|
+
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
|
|
3302
4354
|
<vipps-mobilepay-button
|
|
3303
4355
|
class="buy-vipps-official"
|
|
3304
4356
|
part="buy-vipps"
|
|
@@ -3324,6 +4376,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3324
4376
|
<span class="vipps-badge">vipps</span>
|
|
3325
4377
|
</button>
|
|
3326
4378
|
` : ""}
|
|
4379
|
+
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") && this.vippsGoesStraightToApp() ? html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>` : ""}
|
|
3327
4380
|
|
|
3328
4381
|
<div class="qty-cta-row" part="quantity-row">
|
|
3329
4382
|
<div class="qty-stepper">
|
|
@@ -3365,7 +4418,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3365
4418
|
</button>
|
|
3366
4419
|
` : ""}
|
|
3367
4420
|
|
|
3368
|
-
${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
4421
|
+
${this.availableQuantity > 0 && this.hasPrice && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
3369
4422
|
<button
|
|
3370
4423
|
class="buy-applepay"
|
|
3371
4424
|
@click=${this.buyWithApplePay}
|
|
@@ -3411,12 +4464,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
3411
4464
|
${this.renderSellerNote()}
|
|
3412
4465
|
</div>
|
|
3413
4466
|
|
|
3414
|
-
${
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
` : ""}
|
|
4467
|
+
${this.renderHighlights()}
|
|
4468
|
+
|
|
4469
|
+
${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
|
|
4470
|
+
${this.renderShippingReturns()}
|
|
4471
|
+
</div>
|
|
3420
4472
|
</div>
|
|
3421
4473
|
</div>
|
|
3422
4474
|
`;
|
|
@@ -3426,9 +4478,16 @@ var VioProductDetail = class extends LitElement {
|
|
|
3426
4478
|
return html`${options.map((opt) => {
|
|
3427
4479
|
const values = normalizeValues(opt.values);
|
|
3428
4480
|
if (values.length === 0) return "";
|
|
4481
|
+
const chosen = String(this.selectedOptions[opt.name] ?? "").trim();
|
|
4482
|
+
const asked = this.selectionPrompt === opt.name && !chosen;
|
|
3429
4483
|
return html`
|
|
3430
|
-
<div class="option-group" part="option-group">
|
|
3431
|
-
<div class="option-label" part="option-label"
|
|
4484
|
+
<div class="option-group ${asked ? "needs-choice" : ""}" part="option-group">
|
|
4485
|
+
<div class="option-label" part="option-label">
|
|
4486
|
+
<span
|
|
4487
|
+
>${opt.name}${chosen ? html`: <span class="option-chosen" part="option-chosen">${chosen}</span>` : ""}</span
|
|
4488
|
+
>
|
|
4489
|
+
${asked ? html`<span class="option-prompt" part="option-prompt" role="alert">${this.promptFor(opt)}</span>` : ""}
|
|
4490
|
+
</div>
|
|
3432
4491
|
<div class="option-values" part="options">
|
|
3433
4492
|
${values.map((val) => {
|
|
3434
4493
|
const available = this.isValueAvailable(opt.name, val);
|
|
@@ -3493,6 +4552,24 @@ __decorateClass([
|
|
|
3493
4552
|
__decorateClass([
|
|
3494
4553
|
property({ type: String, attribute: "vio-content-title" })
|
|
3495
4554
|
], VioProductDetail.prototype, "vioContentTitle", 2);
|
|
4555
|
+
__decorateClass([
|
|
4556
|
+
property({ type: Boolean, attribute: "no-added-toast" })
|
|
4557
|
+
], VioProductDetail.prototype, "noAddedToast", 2);
|
|
4558
|
+
__decorateClass([
|
|
4559
|
+
property({ type: String })
|
|
4560
|
+
], VioProductDetail.prototype, "gallery", 2);
|
|
4561
|
+
__decorateClass([
|
|
4562
|
+
property({ type: String, attribute: "description-mode" })
|
|
4563
|
+
], VioProductDetail.prototype, "descriptionMode", 2);
|
|
4564
|
+
__decorateClass([
|
|
4565
|
+
property({ type: Array })
|
|
4566
|
+
], VioProductDetail.prototype, "highlights", 2);
|
|
4567
|
+
__decorateClass([
|
|
4568
|
+
property({ type: String, attribute: "delivery-time" })
|
|
4569
|
+
], VioProductDetail.prototype, "deliveryTime", 2);
|
|
4570
|
+
__decorateClass([
|
|
4571
|
+
property({ type: Object, attribute: "carrier-logos" })
|
|
4572
|
+
], VioProductDetail.prototype, "carrierLogos", 2);
|
|
3496
4573
|
__decorateClass([
|
|
3497
4574
|
state()
|
|
3498
4575
|
], VioProductDetail.prototype, "product", 2);
|
|
@@ -3502,6 +4579,12 @@ __decorateClass([
|
|
|
3502
4579
|
__decorateClass([
|
|
3503
4580
|
state()
|
|
3504
4581
|
], VioProductDetail.prototype, "sellerSettled", 2);
|
|
4582
|
+
__decorateClass([
|
|
4583
|
+
state()
|
|
4584
|
+
], VioProductDetail.prototype, "shippingFrom", 2);
|
|
4585
|
+
__decorateClass([
|
|
4586
|
+
state()
|
|
4587
|
+
], VioProductDetail.prototype, "shippingRates", 2);
|
|
3505
4588
|
__decorateClass([
|
|
3506
4589
|
state()
|
|
3507
4590
|
], VioProductDetail.prototype, "isLoading", 2);
|
|
@@ -3511,6 +4594,9 @@ __decorateClass([
|
|
|
3511
4594
|
__decorateClass([
|
|
3512
4595
|
state()
|
|
3513
4596
|
], VioProductDetail.prototype, "selectedOptions", 2);
|
|
4597
|
+
__decorateClass([
|
|
4598
|
+
state()
|
|
4599
|
+
], VioProductDetail.prototype, "selectionPrompt", 2);
|
|
3514
4600
|
__decorateClass([
|
|
3515
4601
|
state()
|
|
3516
4602
|
], VioProductDetail.prototype, "quantity", 2);
|
|
@@ -3526,18 +4612,39 @@ __decorateClass([
|
|
|
3526
4612
|
__decorateClass([
|
|
3527
4613
|
state()
|
|
3528
4614
|
], VioProductDetail.prototype, "thumbsAtEnd", 2);
|
|
4615
|
+
__decorateClass([
|
|
4616
|
+
state()
|
|
4617
|
+
], VioProductDetail.prototype, "descriptionOpen", 2);
|
|
4618
|
+
__decorateClass([
|
|
4619
|
+
state()
|
|
4620
|
+
], VioProductDetail.prototype, "descriptionClipped", 2);
|
|
4621
|
+
__decorateClass([
|
|
4622
|
+
state()
|
|
4623
|
+
], VioProductDetail.prototype, "galleryMode", 2);
|
|
4624
|
+
__decorateClass([
|
|
4625
|
+
state()
|
|
4626
|
+
], VioProductDetail.prototype, "resolvedDescriptionMode", 2);
|
|
3529
4627
|
__decorateClass([
|
|
3530
4628
|
state()
|
|
3531
4629
|
], VioProductDetail.prototype, "logoSquare", 2);
|
|
3532
4630
|
__decorateClass([
|
|
3533
4631
|
state()
|
|
3534
4632
|
], VioProductDetail.prototype, "adding", 2);
|
|
4633
|
+
__decorateClass([
|
|
4634
|
+
state()
|
|
4635
|
+
], VioProductDetail.prototype, "addedToast", 2);
|
|
3535
4636
|
__decorateClass([
|
|
3536
4637
|
state()
|
|
3537
4638
|
], VioProductDetail.prototype, "applePayOk", 2);
|
|
3538
4639
|
__decorateClass([
|
|
3539
4640
|
state()
|
|
3540
4641
|
], VioProductDetail.prototype, "availableMethods", 2);
|
|
4642
|
+
__decorateClass([
|
|
4643
|
+
state()
|
|
4644
|
+
], VioProductDetail.prototype, "methodsLoading", 2);
|
|
4645
|
+
__decorateClass([
|
|
4646
|
+
state()
|
|
4647
|
+
], VioProductDetail.prototype, "vippsExpress", 2);
|
|
3541
4648
|
__decorateClass([
|
|
3542
4649
|
state()
|
|
3543
4650
|
], VioProductDetail.prototype, "vippsOfficial", 2);
|
|
@@ -3550,9 +4657,35 @@ function normalizeValues(raw) {
|
|
|
3550
4657
|
}
|
|
3551
4658
|
return [];
|
|
3552
4659
|
}
|
|
4660
|
+
function splitRateName(name) {
|
|
4661
|
+
const text = String(name ?? "").trim();
|
|
4662
|
+
const m = text.match(/^(.+?)\s+[-–—]\s+(.+)$/);
|
|
4663
|
+
if (!m?.[1] || !m[2]) return { carrier: "", method: text };
|
|
4664
|
+
const method = m[2].trim();
|
|
4665
|
+
return { carrier: m[1].trim(), method: method.charAt(0).toUpperCase() + method.slice(1) };
|
|
4666
|
+
}
|
|
3553
4667
|
var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
3554
4668
|
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
3555
4669
|
);
|
|
4670
|
+
function ratePrice(rate, currency) {
|
|
4671
|
+
const price = rate?.price;
|
|
4672
|
+
if (!price) return null;
|
|
4673
|
+
const amount = typeof price.amount_incl_taxes === "number" ? price.amount_incl_taxes : price.amount;
|
|
4674
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) return null;
|
|
4675
|
+
if (price.currency_code && currency && price.currency_code.toUpperCase() !== currency.toUpperCase()) return null;
|
|
4676
|
+
return amount;
|
|
4677
|
+
}
|
|
4678
|
+
function cartShipping(items, currency) {
|
|
4679
|
+
for (const item of items) {
|
|
4680
|
+
const chosen = ratePrice(item.shipping, currency);
|
|
4681
|
+
if (chosen !== null) return chosen;
|
|
4682
|
+
}
|
|
4683
|
+
for (const item of items) {
|
|
4684
|
+
const offered = (item.availableShippings ?? []).map((r) => ratePrice(r, currency)).filter((p) => p !== null);
|
|
4685
|
+
if (offered.length > 0) return Math.min(...offered);
|
|
4686
|
+
}
|
|
4687
|
+
return null;
|
|
4688
|
+
}
|
|
3556
4689
|
var VioCart = class extends LitElement {
|
|
3557
4690
|
constructor() {
|
|
3558
4691
|
super(...arguments);
|
|
@@ -3564,6 +4697,7 @@ var VioCart = class extends LitElement {
|
|
|
3564
4697
|
this.itemCount = 0;
|
|
3565
4698
|
this.applePayAvailable = false;
|
|
3566
4699
|
this.availableMethods = null;
|
|
4700
|
+
this.methodsLoading = false;
|
|
3567
4701
|
this.vippsOfficial = false;
|
|
3568
4702
|
/** Whether the method lookup has been attempted — see boundOnCartChange. */
|
|
3569
4703
|
this.paymentMethodsAttempted = false;
|
|
@@ -3586,6 +4720,7 @@ var VioCart = class extends LitElement {
|
|
|
3586
4720
|
sheet: () => this.renderRoot.querySelector(".drawer"),
|
|
3587
4721
|
backdrop: () => this.renderRoot.querySelector(".backdrop"),
|
|
3588
4722
|
dragZones: ".handle, .header",
|
|
4723
|
+
scroller: () => this.renderRoot.querySelector(".body"),
|
|
3589
4724
|
close: () => this.close()
|
|
3590
4725
|
});
|
|
3591
4726
|
this.boundOnCartChange = (e) => {
|
|
@@ -3790,6 +4925,14 @@ var VioCart = class extends LitElement {
|
|
|
3790
4925
|
cursor: pointer;
|
|
3791
4926
|
}
|
|
3792
4927
|
.checkout-btn:hover { opacity: 0.9; }
|
|
4928
|
+
.checkout-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4929
|
+
/* Where the express buttons go while the brand's methods load. */
|
|
4930
|
+
.methods-pending {
|
|
4931
|
+
height: 48px;
|
|
4932
|
+
margin-bottom: 10px;
|
|
4933
|
+
border-radius: var(--vio-radius-lg, 8px);
|
|
4934
|
+
background: var(--vio-color-surface-muted, #f1f1f1);
|
|
4935
|
+
}
|
|
3793
4936
|
.footer {
|
|
3794
4937
|
padding: var(--vio-space-lg, 24px);
|
|
3795
4938
|
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
@@ -3802,6 +4945,14 @@ var VioCart = class extends LitElement {
|
|
|
3802
4945
|
font-size: 14px;
|
|
3803
4946
|
}
|
|
3804
4947
|
.subtotal-label { color: var(--vio-color-text-secondary, #666); }
|
|
4948
|
+
.shipping-row,
|
|
4949
|
+
.total-row {
|
|
4950
|
+
display: flex;
|
|
4951
|
+
justify-content: space-between;
|
|
4952
|
+
margin: calc(var(--vio-space-md, 16px) * -0.5) 0 var(--vio-space-md, 16px);
|
|
4953
|
+
font-size: 14px;
|
|
4954
|
+
}
|
|
4955
|
+
.total-row { font-weight: 700; font-size: 16px; margin-top: 0; }
|
|
3805
4956
|
.subtotal-value { font-weight: 700; font-size: 16px; }
|
|
3806
4957
|
|
|
3807
4958
|
/* Apple Pay express — pay the whole cart directly. */
|
|
@@ -3997,6 +5148,7 @@ var VioCart = class extends LitElement {
|
|
|
3997
5148
|
this.loadingPaymentMethods = false;
|
|
3998
5149
|
return;
|
|
3999
5150
|
}
|
|
5151
|
+
this.methodsLoading = true;
|
|
4000
5152
|
try {
|
|
4001
5153
|
const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
|
|
4002
5154
|
if (Array.isArray(methods)) {
|
|
@@ -4011,10 +5163,20 @@ var VioCart = class extends LitElement {
|
|
|
4011
5163
|
this.availableMethods = null;
|
|
4012
5164
|
} finally {
|
|
4013
5165
|
this.loadingPaymentMethods = false;
|
|
5166
|
+
this.methodsLoading = false;
|
|
4014
5167
|
}
|
|
4015
5168
|
}
|
|
4016
|
-
/**
|
|
5169
|
+
/** The methods are on their way and none is known yet. */
|
|
5170
|
+
get methodsPending() {
|
|
5171
|
+
return this.methodsLoading && this.availableMethods === null;
|
|
5172
|
+
}
|
|
5173
|
+
/**
|
|
5174
|
+
* Backend names arrive as e.g. "Apple Pay" — compare letters only. While
|
|
5175
|
+
* the list loads, no method's button is drawn: «unknown = all» painted
|
|
5176
|
+
* Stripe and Klarna for a Vipps-only brand until it came.
|
|
5177
|
+
*/
|
|
4017
5178
|
methodEnabled(...names) {
|
|
5179
|
+
if (this.methodsPending) return false;
|
|
4018
5180
|
return isMethodEnabled(this.availableMethods, ...names);
|
|
4019
5181
|
}
|
|
4020
5182
|
updated(changed) {
|
|
@@ -4025,6 +5187,9 @@ var VioCart = class extends LitElement {
|
|
|
4025
5187
|
if (changed.has("open") && this.open) {
|
|
4026
5188
|
this.confirmedOrder = null;
|
|
4027
5189
|
this.preparedTotal = -1;
|
|
5190
|
+
for (const cart of this.carts.values()) {
|
|
5191
|
+
if (cart.cartId) void Vio.cart.refreshFromServer(cart.sponsorId);
|
|
5192
|
+
}
|
|
4028
5193
|
}
|
|
4029
5194
|
if (this.open && this.itemCount > 0) {
|
|
4030
5195
|
const total = this.totalAcrossSponsors;
|
|
@@ -4054,7 +5219,7 @@ var VioCart = class extends LitElement {
|
|
|
4054
5219
|
/** Apple Pay express — pay the whole cart, then show the confirmation. */
|
|
4055
5220
|
onApplePay() {
|
|
4056
5221
|
const handle = this.applePayHandle;
|
|
4057
|
-
if (!handle || !handle.available || this.
|
|
5222
|
+
if (!handle || !handle.available || !this.payable) return;
|
|
4058
5223
|
const snapshot = this.buildConfirmation();
|
|
4059
5224
|
const sponsorIds = [...this.carts.keys()];
|
|
4060
5225
|
const sponsor = sponsorIds.length === 1 ? sponsorDetail(sponsorIds[0]) : {};
|
|
@@ -4098,7 +5263,7 @@ var VioCart = class extends LitElement {
|
|
|
4098
5263
|
* (betalingsmetode). The host's checkout-open handler closes this cart and
|
|
4099
5264
|
* opens <vio-checkout> in express mode with Klarna preselected. */
|
|
4100
5265
|
onKlarnaExpress() {
|
|
4101
|
-
if (this.
|
|
5266
|
+
if (!this.payable) return;
|
|
4102
5267
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4103
5268
|
if (firstSponsorId === void 0) return;
|
|
4104
5269
|
this.dispatchEvent(
|
|
@@ -4119,7 +5284,7 @@ var VioCart = class extends LitElement {
|
|
|
4119
5284
|
* method present the others were unreachable from here.
|
|
4120
5285
|
*/
|
|
4121
5286
|
onCheckout() {
|
|
4122
|
-
if (this.
|
|
5287
|
+
if (!this.payable) return;
|
|
4123
5288
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4124
5289
|
if (firstSponsorId === void 0) return;
|
|
4125
5290
|
this.dispatchEvent(
|
|
@@ -4131,51 +5296,31 @@ var VioCart = class extends LitElement {
|
|
|
4131
5296
|
);
|
|
4132
5297
|
}
|
|
4133
5298
|
/**
|
|
4134
|
-
* Vipps
|
|
4135
|
-
*
|
|
4136
|
-
*
|
|
4137
|
-
*
|
|
5299
|
+
* Vipps from the cart: the checkout opens with Vipps preselected — the
|
|
5300
|
+
* standard path, where our form collects name, address, mobile and the
|
|
5301
|
+
* delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
|
|
5302
|
+
* It no longer starts Express from here: Express is the product detail's
|
|
5303
|
+
* «Kjøp nå med Vipps» only.
|
|
4138
5304
|
*/
|
|
4139
5305
|
onVipps() {
|
|
4140
|
-
if (this.
|
|
5306
|
+
if (!this.payable) return;
|
|
4141
5307
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4142
5308
|
if (firstSponsorId === void 0) return;
|
|
4143
|
-
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: firstSponsorId, source: "cart" });
|
|
4144
|
-
void this.startVippsExpressOrCheckout(firstSponsorId, redirect);
|
|
4145
|
-
}
|
|
4146
|
-
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
4147
|
-
async loadVippsButton() {
|
|
4148
|
-
this.vippsOfficial = await ensureVippsButton();
|
|
4149
|
-
}
|
|
4150
|
-
async startVippsExpressOrCheckout(sponsorId, redirect) {
|
|
4151
|
-
let fallback = true;
|
|
4152
|
-
try {
|
|
4153
|
-
await Vio.cart.waitForCartMutations(sponsorId);
|
|
4154
|
-
if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
|
|
4155
|
-
redirect.abort("express-off");
|
|
4156
|
-
} else {
|
|
4157
|
-
const res = await Vio.checkout.startVippsExpress(sponsorId);
|
|
4158
|
-
fallback = !res?.payment_url;
|
|
4159
|
-
if (fallback) redirect.abort("not-started");
|
|
4160
|
-
}
|
|
4161
|
-
} catch (err) {
|
|
4162
|
-
if (typeof console !== "undefined") {
|
|
4163
|
-
console.warn("[VioCart] Vipps Express not started:", err);
|
|
4164
|
-
}
|
|
4165
|
-
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
4166
|
-
}
|
|
4167
|
-
if (!fallback) return;
|
|
4168
5309
|
this.dispatchEvent(
|
|
4169
5310
|
new CustomEvent("vio:checkout-open", {
|
|
4170
5311
|
bubbles: true,
|
|
4171
5312
|
composed: true,
|
|
4172
|
-
detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(
|
|
5313
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
|
|
4173
5314
|
})
|
|
4174
5315
|
);
|
|
4175
5316
|
}
|
|
5317
|
+
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
5318
|
+
async loadVippsButton() {
|
|
5319
|
+
this.vippsOfficial = await ensureVippsButton();
|
|
5320
|
+
}
|
|
4176
5321
|
/** Stripe — opens the checkout with Stripe preselected. */
|
|
4177
5322
|
onStripe() {
|
|
4178
|
-
if (this.
|
|
5323
|
+
if (!this.payable) return;
|
|
4179
5324
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4180
5325
|
if (firstSponsorId === void 0) return;
|
|
4181
5326
|
this.dispatchEvent(
|
|
@@ -4190,6 +5335,10 @@ var VioCart = class extends LitElement {
|
|
|
4190
5335
|
sponsorLabel(cart) {
|
|
4191
5336
|
return resolveSponsorIdentity(cart.sponsorId)?.name || cart.sponsorName || "";
|
|
4192
5337
|
}
|
|
5338
|
+
/** Something to pay for: lines, and a total above 0. No button opens a checkout or a payment otherwise. */
|
|
5339
|
+
get payable() {
|
|
5340
|
+
return this.itemCount > 0 && this.totalAcrossSponsors > 0;
|
|
5341
|
+
}
|
|
4193
5342
|
get totalAcrossSponsors() {
|
|
4194
5343
|
let sum = 0;
|
|
4195
5344
|
for (const cart of this.carts.values()) {
|
|
@@ -4197,6 +5346,24 @@ var VioCart = class extends LitElement {
|
|
|
4197
5346
|
}
|
|
4198
5347
|
return sum;
|
|
4199
5348
|
}
|
|
5349
|
+
/**
|
|
5350
|
+
* Shipping across the carts, from what the server cart says of its lines:
|
|
5351
|
+
* the rate chosen for them, else the cheapest one offered (the one the
|
|
5352
|
+
* checkout preselects). null when some cart has no known rate — the footer
|
|
5353
|
+
* says it is worked out in the checkout then.
|
|
5354
|
+
*/
|
|
5355
|
+
get shippingAcrossSponsors() {
|
|
5356
|
+
let sum = 0;
|
|
5357
|
+
let any = false;
|
|
5358
|
+
for (const cart of this.carts.values()) {
|
|
5359
|
+
if (cart.items.length === 0) continue;
|
|
5360
|
+
const rate = cartShipping(cart.items, cart.currency);
|
|
5361
|
+
if (rate === null) return null;
|
|
5362
|
+
sum += rate;
|
|
5363
|
+
any = true;
|
|
5364
|
+
}
|
|
5365
|
+
return any ? sum : null;
|
|
5366
|
+
}
|
|
4200
5367
|
get primaryCurrency() {
|
|
4201
5368
|
for (const cart of this.carts.values()) return cart.currency;
|
|
4202
5369
|
return "NOK";
|
|
@@ -4288,13 +5455,16 @@ var VioCart = class extends LitElement {
|
|
|
4288
5455
|
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
4289
5456
|
</span>
|
|
4290
5457
|
</div>
|
|
5458
|
+
${this.renderShippingRows()}
|
|
4291
5459
|
<button
|
|
4292
5460
|
class="checkout-btn"
|
|
4293
5461
|
@click=${this.onCheckout}
|
|
5462
|
+
?disabled=${!this.payable}
|
|
4294
5463
|
aria-label="Til kassen"
|
|
4295
5464
|
>
|
|
4296
5465
|
Til kassen
|
|
4297
5466
|
</button>
|
|
5467
|
+
${this.methodsPending ? html`<div class="methods-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
|
|
4298
5468
|
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
4299
5469
|
<button
|
|
4300
5470
|
class="applepay-btn"
|
|
@@ -4355,6 +5525,20 @@ var VioCart = class extends LitElement {
|
|
|
4355
5525
|
</aside>
|
|
4356
5526
|
`;
|
|
4357
5527
|
}
|
|
5528
|
+
/** «Frakt» (the chosen or cheapest rate) and «Totalt» with it; «Beregnes i kassen» when unknown. */
|
|
5529
|
+
renderShippingRows() {
|
|
5530
|
+
const shipping = this.shippingAcrossSponsors;
|
|
5531
|
+
return html`
|
|
5532
|
+
<div class="shipping-row" part="cart-shipping">
|
|
5533
|
+
<span class="subtotal-label">Frakt</span>
|
|
5534
|
+
<span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : formatPrice(shipping, this.primaryCurrency)}</span>
|
|
5535
|
+
</div>
|
|
5536
|
+
${shipping !== null ? html`<div class="total-row" part="cart-total">
|
|
5537
|
+
<span>Totalt</span>
|
|
5538
|
+
<span>${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
|
|
5539
|
+
</div>` : ""}
|
|
5540
|
+
`;
|
|
5541
|
+
}
|
|
4358
5542
|
renderConfirmation() {
|
|
4359
5543
|
const o = this.confirmedOrder;
|
|
4360
5544
|
if (!o) return html``;
|
|
@@ -4406,6 +5590,9 @@ __decorateClass([
|
|
|
4406
5590
|
__decorateClass([
|
|
4407
5591
|
state()
|
|
4408
5592
|
], VioCart.prototype, "availableMethods", 2);
|
|
5593
|
+
__decorateClass([
|
|
5594
|
+
state()
|
|
5595
|
+
], VioCart.prototype, "methodsLoading", 2);
|
|
4409
5596
|
__decorateClass([
|
|
4410
5597
|
state()
|
|
4411
5598
|
], VioCart.prototype, "vippsOfficial", 2);
|
|
@@ -4447,6 +5634,8 @@ function errorText(err) {
|
|
|
4447
5634
|
var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
4448
5635
|
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
4449
5636
|
);
|
|
5637
|
+
var EMPTY_CART_MESSAGE2 = "Handlekurven er tom.";
|
|
5638
|
+
var SHIPPING_NOT_SAVED_MESSAGE = "Kunne ikke lagre fraktmetoden. Pr\xF8v igjen.";
|
|
4450
5639
|
var _VioCheckout = class _VioCheckout extends LitElement {
|
|
4451
5640
|
constructor() {
|
|
4452
5641
|
super(...arguments);
|
|
@@ -4552,6 +5741,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4552
5741
|
firstName: "",
|
|
4553
5742
|
lastName: "",
|
|
4554
5743
|
email: "",
|
|
5744
|
+
phone: "",
|
|
4555
5745
|
address: "",
|
|
4556
5746
|
postalCode: "",
|
|
4557
5747
|
city: ""
|
|
@@ -4912,6 +6102,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4912
6102
|
transition: border-color 0.15s;
|
|
4913
6103
|
}
|
|
4914
6104
|
.field input:focus { border-bottom-color: var(--vio-color-text, #0a0a0a); }
|
|
6105
|
+
.field input[aria-invalid='true'] { border-bottom-color: var(--vio-color-accent, #c14a3b); }
|
|
6106
|
+
.field-error { margin-top: 4px; font-size: 12px; color: var(--vio-color-accent, #c14a3b); }
|
|
4915
6107
|
|
|
4916
6108
|
.payment-grid {
|
|
4917
6109
|
display: grid;
|
|
@@ -5600,7 +6792,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5600
6792
|
this.cleanReturnQueryParams(["checkout_id", "payment_processor"]);
|
|
5601
6793
|
return;
|
|
5602
6794
|
}
|
|
5603
|
-
if (vioMethod === "klarna" &&
|
|
6795
|
+
if (vioMethod === "klarna" && storageGet("session", "vio.klarna.pending.v1")) {
|
|
5604
6796
|
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
5605
6797
|
return;
|
|
5606
6798
|
}
|
|
@@ -5766,6 +6958,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5766
6958
|
if (changed.has("open") && this.open) {
|
|
5767
6959
|
this.autoSelectSoleMethod();
|
|
5768
6960
|
}
|
|
6961
|
+
if (changed.has("open") && this.open && this.checkoutState?.sponsorId && !this.orderConfirmed) {
|
|
6962
|
+
void Vio.cart.refreshFromServer(this.checkoutState.sponsorId);
|
|
6963
|
+
}
|
|
5769
6964
|
if ((changed.has("klarnaAvailable") && this.klarnaAvailable || changed.has("applePayAvailable") && this.applePayAvailable) && this.paymentMethodsResolved && !this.checkoutState?.paymentMethod) {
|
|
5770
6965
|
this.autoSelectAttempted = false;
|
|
5771
6966
|
this.autoSelectSoleMethod();
|
|
@@ -5864,12 +7059,34 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5864
7059
|
(i) => Array.isArray(i.availableShippings) && i.availableShippings.length > 0
|
|
5865
7060
|
);
|
|
5866
7061
|
}
|
|
5867
|
-
/**
|
|
7062
|
+
/** The shipping country the mobile's calling code defaults to (no +prefix typed). */
|
|
7063
|
+
get phoneCountry() {
|
|
7064
|
+
return String(this.form.country || getGlobalCountryCode());
|
|
7065
|
+
}
|
|
7066
|
+
/**
|
|
7067
|
+
* The mobile is required only for the standard Vipps payment — shopcart
|
|
7068
|
+
* does not ask Vipps for the profile there. Every other method takes it
|
|
7069
|
+
* when given and never waits for it (a host that pre-fills the form with
|
|
7070
|
+
* `setAddress()` and no phone keeps working).
|
|
7071
|
+
*/
|
|
7072
|
+
get phoneRequired() {
|
|
7073
|
+
return this.checkoutState?.paymentMethod === "vipps";
|
|
7074
|
+
}
|
|
7075
|
+
/** A number typed that is not usable (nothing said while the field is empty). */
|
|
7076
|
+
get phoneError() {
|
|
7077
|
+
const raw = String(this.form.phone ?? "").trim();
|
|
7078
|
+
return raw.length > 0 && !isValidPhone(raw, this.phoneCountry);
|
|
7079
|
+
}
|
|
7080
|
+
/** The mobile as the chosen method needs it: valid when required, else anything. */
|
|
7081
|
+
get phoneReady() {
|
|
7082
|
+
return !this.phoneRequired || isValidPhone(this.form.phone, this.phoneCountry);
|
|
7083
|
+
}
|
|
7084
|
+
/** Whether all mandatory Leveringsadresse fields are filled (the mobile only for Vipps). */
|
|
5868
7085
|
get isAddressValid() {
|
|
5869
7086
|
const f = this.form;
|
|
5870
7087
|
if (!f) return false;
|
|
5871
7088
|
return Boolean(
|
|
5872
|
-
f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0
|
|
7089
|
+
f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0 && this.phoneReady
|
|
5873
7090
|
);
|
|
5874
7091
|
}
|
|
5875
7092
|
/** Whether a valid shipping option is selected (when shippings exist). */
|
|
@@ -6109,21 +7326,44 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6109
7326
|
});
|
|
6110
7327
|
}
|
|
6111
7328
|
/**
|
|
6112
|
-
* Vipps from the checkout:
|
|
6113
|
-
*
|
|
6114
|
-
*
|
|
6115
|
-
*
|
|
6116
|
-
*
|
|
6117
|
-
*
|
|
7329
|
+
* Vipps from the checkout: the STANDARD payment, always (Mote & Livsstil,
|
|
7330
|
+
* 2026-10-28). The shopper filled our form — name, address, mobile — and
|
|
7331
|
+
* chose the delivery with its price here; the payment carries that address
|
|
7332
|
+
* and the rate on the cart, and Vipps only approves and pays. Express (the
|
|
7333
|
+
* app filling in name and address) is the product detail's «Kjøp nå med
|
|
7334
|
+
* Vipps» alone, even when the brand has Express on.
|
|
6118
7335
|
*/
|
|
6119
7336
|
async startVippsFromCheckout() {
|
|
6120
7337
|
const sponsorId = this.checkoutState?.sponsorId;
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
7338
|
+
await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: false });
|
|
7339
|
+
}
|
|
7340
|
+
/**
|
|
7341
|
+
* The chosen delivery on the cart before a hosted payment is minted (its
|
|
7342
|
+
* total depends on it). False — and the payment must not start — when it
|
|
7343
|
+
* could not be saved: the selected rate has no supplier to save it under,
|
|
7344
|
+
* or the backend refused. A cart with no rates at all has nothing to save.
|
|
7345
|
+
*/
|
|
7346
|
+
async persistSelectedShipping(method) {
|
|
7347
|
+
if (this.availableShippingsList.length === 0) return true;
|
|
7348
|
+
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
7349
|
+
if (!selectedOpt?.supplierId) {
|
|
7350
|
+
if (typeof console !== "undefined") {
|
|
7351
|
+
console.warn(`[VioCheckout] no supplier for the selected shipping \u2014 ${method} not started`);
|
|
7352
|
+
}
|
|
7353
|
+
return false;
|
|
7354
|
+
}
|
|
7355
|
+
try {
|
|
7356
|
+
const updated = await Vio.checkout.updateShippingsBySupplier(
|
|
7357
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
7358
|
+
this.checkoutState?.sponsorId
|
|
7359
|
+
);
|
|
7360
|
+
return updated != null;
|
|
7361
|
+
} catch (err) {
|
|
7362
|
+
if (typeof console !== "undefined") {
|
|
7363
|
+
console.warn(`[VioCheckout] updateShippingsBySupplier on ${method} pay failed:`, err);
|
|
7364
|
+
}
|
|
7365
|
+
return false;
|
|
6125
7366
|
}
|
|
6126
|
-
await Vio.checkout.startVippsPayment(sponsorId, this.form);
|
|
6127
7367
|
}
|
|
6128
7368
|
async onApplePay() {
|
|
6129
7369
|
if (this.applePayInProgress) return;
|
|
@@ -6142,6 +7382,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6142
7382
|
if (this.shippingBlocked) return;
|
|
6143
7383
|
this.paymentError = null;
|
|
6144
7384
|
this.paymentNotice = null;
|
|
7385
|
+
if (!this.cartPayable) {
|
|
7386
|
+
this.paymentError = EMPTY_CART_MESSAGE2;
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
6145
7389
|
if (method === "apple-pay") {
|
|
6146
7390
|
void this.onApplePay();
|
|
6147
7391
|
return;
|
|
@@ -6162,8 +7406,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6162
7406
|
Vio.checkout.selectPaymentMethod("nexi");
|
|
6163
7407
|
return;
|
|
6164
7408
|
}
|
|
6165
|
-
|
|
6166
|
-
const
|
|
7409
|
+
{
|
|
7410
|
+
const phoneMissing = method === "vipps" && !isValidPhone(this.form.phone, this.phoneCountry);
|
|
7411
|
+
const missing = !this.isAddressValid || phoneMissing ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
|
|
6167
7412
|
if (missing) {
|
|
6168
7413
|
if (this.checkoutState?.paymentMethod !== method) {
|
|
6169
7414
|
Vio.checkout.selectPaymentMethod(method);
|
|
@@ -6187,20 +7432,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6187
7432
|
this.stripeRequested = true;
|
|
6188
7433
|
return;
|
|
6189
7434
|
}
|
|
6190
|
-
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
6191
|
-
if (selectedOpt?.supplierId) {
|
|
6192
|
-
try {
|
|
6193
|
-
await Vio.checkout.updateShippingsBySupplier(
|
|
6194
|
-
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
6195
|
-
this.checkoutState?.sponsorId
|
|
6196
|
-
);
|
|
6197
|
-
} catch (err) {
|
|
6198
|
-
if (typeof console !== "undefined") {
|
|
6199
|
-
console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
|
|
6200
|
-
}
|
|
6201
|
-
}
|
|
6202
|
-
}
|
|
6203
7435
|
this.stripeLoading = true;
|
|
7436
|
+
if (!await this.persistSelectedShipping("stripe")) {
|
|
7437
|
+
this.stripeLoading = false;
|
|
7438
|
+
this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
|
|
7439
|
+
return;
|
|
7440
|
+
}
|
|
6204
7441
|
void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
6205
7442
|
if (typeof console !== "undefined") {
|
|
6206
7443
|
console.warn("[VioCheckout] Stripe payment could not start:", err);
|
|
@@ -6212,6 +7449,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6212
7449
|
}
|
|
6213
7450
|
if (method === "vipps") {
|
|
6214
7451
|
this.vippsLoading = true;
|
|
7452
|
+
if (!await this.persistSelectedShipping("vipps")) {
|
|
7453
|
+
this.vippsLoading = false;
|
|
7454
|
+
this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
|
|
7455
|
+
return;
|
|
7456
|
+
}
|
|
6215
7457
|
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.checkoutState?.sponsorId, source: "checkout" });
|
|
6216
7458
|
void this.startVippsFromCheckout().catch((err) => {
|
|
6217
7459
|
if (typeof console !== "undefined") {
|
|
@@ -6233,7 +7475,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6233
7475
|
const s = this.checkoutState;
|
|
6234
7476
|
if (!s || !s.paymentMethod) return;
|
|
6235
7477
|
if (this.shippingBlocked) return;
|
|
6236
|
-
if (
|
|
7478
|
+
if (!this.cartPayable) {
|
|
7479
|
+
this.paymentError = EMPTY_CART_MESSAGE2;
|
|
7480
|
+
return;
|
|
7481
|
+
}
|
|
7482
|
+
if (!this.isAddressValid) {
|
|
6237
7483
|
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
6238
7484
|
return;
|
|
6239
7485
|
}
|
|
@@ -6458,6 +7704,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6458
7704
|
return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
|
|
6459
7705
|
isFormFirstWidgetMethod(method ?? "");
|
|
6460
7706
|
}
|
|
7707
|
+
/**
|
|
7708
|
+
* A total above 0 — every pay button needs it. The state's subtotal follows
|
|
7709
|
+
* the cart (CheckoutManager.onCartChange): a cart emptied or paid in
|
|
7710
|
+
* another tab drops it to 0 here.
|
|
7711
|
+
*/
|
|
7712
|
+
get cartPayable() {
|
|
7713
|
+
return (this.checkoutState?.subtotal ?? 0) > 0;
|
|
7714
|
+
}
|
|
6461
7715
|
orderTotal() {
|
|
6462
7716
|
const s = this.checkoutState;
|
|
6463
7717
|
if (!s) return "";
|
|
@@ -7603,7 +8857,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7603
8857
|
renderCheckoutBody() {
|
|
7604
8858
|
const method = this.checkoutState?.paymentMethod;
|
|
7605
8859
|
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
7606
|
-
const isVipps = method === "vipps";
|
|
7607
8860
|
const needsOurForm = !!method && !collectsOwnAddress(method);
|
|
7608
8861
|
return html`
|
|
7609
8862
|
${needsOurForm ? html`
|
|
@@ -7642,6 +8895,22 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7642
8895
|
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
7643
8896
|
/>
|
|
7644
8897
|
</div>
|
|
8898
|
+
<div class="field">
|
|
8899
|
+
<label for="vio-checkout-phone">${this.phoneRequired ? "Mobilnummer *" : "Mobilnummer (valgfritt)"}</label>
|
|
8900
|
+
<input
|
|
8901
|
+
id="vio-checkout-phone"
|
|
8902
|
+
class="phone-input"
|
|
8903
|
+
type="tel"
|
|
8904
|
+
?required=${this.phoneRequired}
|
|
8905
|
+
inputmode="tel"
|
|
8906
|
+
autocomplete="tel"
|
|
8907
|
+
placeholder=${this.phoneCountry.toUpperCase() === "NO" ? "8 siffer" : "+46 70 123 45 67"}
|
|
8908
|
+
aria-invalid=${this.phoneError ? "true" : "false"}
|
|
8909
|
+
.value=${this.form.phone ?? ""}
|
|
8910
|
+
@input=${(e) => this.onFieldChange("phone", e.target.value)}
|
|
8911
|
+
/>
|
|
8912
|
+
${this.phoneError ? html`<div class="field-error" role="alert">Skriv et gyldig mobilnummer (med landskode, f.eks. +46, hvis det ikke er norsk)</div>` : ""}
|
|
8913
|
+
</div>
|
|
7645
8914
|
<div class="field">
|
|
7646
8915
|
<label>Adresse *</label>
|
|
7647
8916
|
<input
|
|
@@ -7706,22 +8975,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7706
8975
|
</section>
|
|
7707
8976
|
` : ""}
|
|
7708
8977
|
|
|
7709
|
-
${isVipps ? html`
|
|
7710
|
-
<section class="section">
|
|
7711
|
-
<div class="section-label">Kontakt</div>
|
|
7712
|
-
<div class="field">
|
|
7713
|
-
<label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
|
|
7714
|
-
<input
|
|
7715
|
-
type="email"
|
|
7716
|
-
required
|
|
7717
|
-
autocomplete="email"
|
|
7718
|
-
.value=${this.form.email}
|
|
7719
|
-
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
7720
|
-
/>
|
|
7721
|
-
</div>
|
|
7722
|
-
</section>
|
|
7723
|
-
` : ""}
|
|
7724
|
-
|
|
7725
8978
|
<section class="section">
|
|
7726
8979
|
<div class="section-label">
|
|
7727
8980
|
${needsOurForm ? "Steg 2" : "Betaling"}
|
|
@@ -7761,7 +9014,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7761
9014
|
<button
|
|
7762
9015
|
class="payment-btn primary complete-cta"
|
|
7763
9016
|
@click=${() => void this.onPay("stripe")}
|
|
7764
|
-
?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9017
|
+
?disabled=${this.stripeLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7765
9018
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7766
9019
|
>
|
|
7767
9020
|
${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
|
|
@@ -7771,7 +9024,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7771
9024
|
<button
|
|
7772
9025
|
class="payment-btn primary complete-cta"
|
|
7773
9026
|
@click=${() => void this.onPay("vipps")}
|
|
7774
|
-
?disabled=${this.vippsLoading}
|
|
9027
|
+
?disabled=${this.vippsLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9028
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7775
9029
|
>
|
|
7776
9030
|
${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
|
|
7777
9031
|
</button>
|
|
@@ -7780,7 +9034,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7780
9034
|
<button
|
|
7781
9035
|
class="payment-btn primary complete-cta"
|
|
7782
9036
|
@click=${() => void this.onPay("apple-pay")}
|
|
7783
|
-
?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9037
|
+
?disabled=${this.applePayInProgress || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7784
9038
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7785
9039
|
>
|
|
7786
9040
|
${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
|
|
@@ -7790,7 +9044,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7790
9044
|
<button
|
|
7791
9045
|
class="payment-btn primary complete-cta"
|
|
7792
9046
|
@click=${this.onCompleteOrder}
|
|
7793
|
-
?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
|
|
9047
|
+
?disabled=${!this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7794
9048
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7795
9049
|
>
|
|
7796
9050
|
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
@@ -8099,5 +9353,5 @@ function registerVioElements() {
|
|
|
8099
9353
|
}
|
|
8100
9354
|
|
|
8101
9355
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
8102
|
-
//# sourceMappingURL=chunk-
|
|
8103
|
-
//# sourceMappingURL=chunk-
|
|
9356
|
+
//# sourceMappingURL=chunk-C5Q6W4B2.js.map
|
|
9357
|
+
//# sourceMappingURL=chunk-C5Q6W4B2.js.map
|