@vio-live/web-sdk 0.29.0 → 0.33.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 +219 -0
- package/README.md +127 -19
- package/dist/chunk-QGJUBVH3.js +3 -0
- package/dist/{chunk-67QESEV2.js.map → chunk-QGJUBVH3.js.map} +1 -1
- package/dist/{chunk-L6YYYKQK.js → chunk-T5B74YBQ.js} +1433 -205
- package/dist/chunk-T5B74YBQ.js.map +1 -0
- package/dist/{chunk-AG4G2S2M.js → chunk-ZORYG4W3.js} +1153 -169
- package/dist/chunk-ZORYG4W3.js.map +1 -0
- package/dist/core/index.d.ts +891 -556
- 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-Dok4d21V.d.ts → tokens-B7GVA_qB.d.ts} +325 -21
- 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-AG4G2S2M.js.map +0 -1
- package/dist/chunk-L6YYYKQK.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, shippingDescriptionOf, 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-ZORYG4W3.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?.();
|
|
@@ -1019,6 +1151,21 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1019
1151
|
* sponsor of the campaign the slot shows (ADR-0034 — "the campaign of the
|
|
1020
1152
|
* block is its sponsor"), else 0 (unknown).
|
|
1021
1153
|
*/
|
|
1154
|
+
/**
|
|
1155
|
+
* The campaign whose channel sells a sponsor's products here (one article =
|
|
1156
|
+
* one campaign = one channel): the campaign this slot credits for that
|
|
1157
|
+
* sponsor (`Vio.attribution`), then the element's own (`vio-campaign`, an
|
|
1158
|
+
* ancestor's `data-vio-campaign`).
|
|
1159
|
+
*/
|
|
1160
|
+
commerceCampaigns(sponsorId) {
|
|
1161
|
+
const own = this.slotRegistration.campaignHint;
|
|
1162
|
+
try {
|
|
1163
|
+
const resolved = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot || null, { sponsorId, campaignId: own }).campaignId : void 0;
|
|
1164
|
+
return resolved !== void 0 ? [resolved, ...own] : own;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return own;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1022
1169
|
effectiveSponsorId() {
|
|
1023
1170
|
if (this.sponsorId > 0) return this.sponsorId;
|
|
1024
1171
|
const fromSlot = typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.effectiveSlot || null, { campaignId: this.slotRegistration.campaignHint })?.primarySponsor?.id) : NaN;
|
|
@@ -1031,6 +1178,10 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1031
1178
|
}
|
|
1032
1179
|
this.isLoading = true;
|
|
1033
1180
|
this.fetchError = "";
|
|
1181
|
+
if (!this.impressed) {
|
|
1182
|
+
this.unobserveImpression?.();
|
|
1183
|
+
this.unobserveImpression = null;
|
|
1184
|
+
}
|
|
1034
1185
|
try {
|
|
1035
1186
|
if (this.productRefs.trim() === "" && !this.commerceKey && this.sponsorId <= 0 && !Vio.bootstrapCache) {
|
|
1036
1187
|
try {
|
|
@@ -1052,7 +1203,7 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1052
1203
|
}
|
|
1053
1204
|
const fetched = await Promise.all(
|
|
1054
1205
|
Array.from(bySponsor.entries()).map(async ([sponsorId2, ids]) => {
|
|
1055
|
-
const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2);
|
|
1206
|
+
const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2, { campaignId: this.commerceCampaigns(sponsorId2) });
|
|
1056
1207
|
const products = await commerce.channel.product.getByIds({
|
|
1057
1208
|
product_ids: ids,
|
|
1058
1209
|
currency: this.currency,
|
|
@@ -1074,10 +1225,11 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1074
1225
|
this.fetchError = err instanceof Error ? err.message : String(err);
|
|
1075
1226
|
} finally {
|
|
1076
1227
|
this.isLoading = false;
|
|
1228
|
+
this.observeForAnalytics();
|
|
1077
1229
|
}
|
|
1078
1230
|
}
|
|
1079
1231
|
observeForAnalytics() {
|
|
1080
|
-
if (this.unobserveImpression) return;
|
|
1232
|
+
if (this.unobserveImpression || this.isLoading || !this.isConnected) return;
|
|
1081
1233
|
try {
|
|
1082
1234
|
this.unobserveImpression = Vio.analytics.observeImpression(
|
|
1083
1235
|
this,
|
|
@@ -1092,7 +1244,8 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1092
1244
|
onImpression: () => {
|
|
1093
1245
|
this.impressed = true;
|
|
1094
1246
|
this.maybeTrackList();
|
|
1095
|
-
}
|
|
1247
|
+
},
|
|
1248
|
+
commerce: () => this.listCommerce()
|
|
1096
1249
|
}
|
|
1097
1250
|
);
|
|
1098
1251
|
} catch {
|
|
@@ -1113,19 +1266,25 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1113
1266
|
locationId: this.effectiveSlot || this.label || void 0,
|
|
1114
1267
|
...this.attribution()
|
|
1115
1268
|
},
|
|
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
|
-
},
|
|
1269
|
+
commerce: this.listCommerce() ?? void 0,
|
|
1124
1270
|
props: this.label ? { list_name: this.label } : void 0
|
|
1125
1271
|
});
|
|
1126
1272
|
} catch {
|
|
1127
1273
|
}
|
|
1128
1274
|
}
|
|
1275
|
+
/** The products on show as analytics lines (impression and view_item_list); null before they load. */
|
|
1276
|
+
listCommerce() {
|
|
1277
|
+
const entries = this.fetched ?? [];
|
|
1278
|
+
if (entries.length === 0) return null;
|
|
1279
|
+
return {
|
|
1280
|
+
items: entries.map(({ product }) => ({
|
|
1281
|
+
productId: product.id,
|
|
1282
|
+
name: product.title,
|
|
1283
|
+
price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
|
|
1284
|
+
})),
|
|
1285
|
+
currency: entries[0]?.product.price?.currency_code
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1129
1288
|
parseRefs(raw) {
|
|
1130
1289
|
return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
|
|
1131
1290
|
const [sidStr, pidStr] = pair.split(":");
|
|
@@ -1165,6 +1324,10 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1165
1324
|
name=${p.title}
|
|
1166
1325
|
price=${displayPrice(p)}
|
|
1167
1326
|
retailer=${retailer}
|
|
1327
|
+
description=${this.descriptionFor(p)}
|
|
1328
|
+
?show-description=${this.showsDescription(p)}
|
|
1329
|
+
button-style=${this.buttonStyle}
|
|
1330
|
+
button-label=${this.buttonLabel}
|
|
1168
1331
|
image-url=${img}
|
|
1169
1332
|
?has-variants=${(p.variants?.length ?? 0) > 1}
|
|
1170
1333
|
.vioSlot=${this.effectiveSlot}
|
|
@@ -1190,7 +1353,7 @@ var VioProductCarousel = class extends LitElement {
|
|
|
1190
1353
|
</div>
|
|
1191
1354
|
${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
|
|
1192
1355
|
</div>
|
|
1193
|
-
<div class="viewport">
|
|
1356
|
+
<div class="viewport ${this.resolvedLayout}">
|
|
1194
1357
|
<button
|
|
1195
1358
|
class="nav-btn prev"
|
|
1196
1359
|
@click=${() => this.scrollByDirection(-1)}
|
|
@@ -1241,6 +1404,24 @@ __decorateClass([
|
|
|
1241
1404
|
__decorateClass([
|
|
1242
1405
|
property({ type: String })
|
|
1243
1406
|
], VioProductCarousel.prototype, "currency", 2);
|
|
1407
|
+
__decorateClass([
|
|
1408
|
+
property({ type: String, reflect: true })
|
|
1409
|
+
], VioProductCarousel.prototype, "layout", 2);
|
|
1410
|
+
__decorateClass([
|
|
1411
|
+
property({ type: Boolean, attribute: "show-description" })
|
|
1412
|
+
], VioProductCarousel.prototype, "showDescription", 2);
|
|
1413
|
+
__decorateClass([
|
|
1414
|
+
property({ type: Object })
|
|
1415
|
+
], VioProductCarousel.prototype, "descriptions", 2);
|
|
1416
|
+
__decorateClass([
|
|
1417
|
+
property({ type: String, attribute: "button-style" })
|
|
1418
|
+
], VioProductCarousel.prototype, "buttonStyle", 2);
|
|
1419
|
+
__decorateClass([
|
|
1420
|
+
property({ type: String, attribute: "button-label" })
|
|
1421
|
+
], VioProductCarousel.prototype, "buttonLabel", 2);
|
|
1422
|
+
__decorateClass([
|
|
1423
|
+
state()
|
|
1424
|
+
], VioProductCarousel.prototype, "resolvedLayout", 2);
|
|
1244
1425
|
__decorateClass([
|
|
1245
1426
|
property({ type: String, attribute: "vio-slot" })
|
|
1246
1427
|
], VioProductCarousel.prototype, "vioSlot", 2);
|
|
@@ -1274,6 +1455,10 @@ __decorateClass([
|
|
|
1274
1455
|
__decorateClass([
|
|
1275
1456
|
state()
|
|
1276
1457
|
], VioProductCarousel.prototype, "fetchError", 2);
|
|
1458
|
+
function cardDescription(description) {
|
|
1459
|
+
if (!description) return "";
|
|
1460
|
+
return description.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\s+/g, " ").trim();
|
|
1461
|
+
}
|
|
1277
1462
|
|
|
1278
1463
|
// src/ui/payment-redirect.ts
|
|
1279
1464
|
var OVERLAY_CLASS = "vio-redirect-overlay";
|
|
@@ -1381,7 +1566,7 @@ var NO_EXPRESS_BUTTON_METHODS = [
|
|
|
1381
1566
|
...EMBEDDED_METHODS,
|
|
1382
1567
|
...FORM_FIRST_WIDGET_METHODS
|
|
1383
1568
|
];
|
|
1384
|
-
var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS
|
|
1569
|
+
var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS];
|
|
1385
1570
|
var isEmbeddedMethod = (name) => EMBEDDED_METHODS.includes(normalizeMethodName(name));
|
|
1386
1571
|
var isFormFirstWidgetMethod = (name) => FORM_FIRST_WIDGET_METHODS.includes(normalizeMethodName(name));
|
|
1387
1572
|
var collectsOwnAddress = (name) => COLLECTS_OWN_ADDRESS.includes(normalizeMethodName(name));
|
|
@@ -1595,6 +1780,8 @@ var BottomSheet = class {
|
|
|
1595
1780
|
this.locked = false;
|
|
1596
1781
|
this.drag = null;
|
|
1597
1782
|
this.dragEndedAt = -Infinity;
|
|
1783
|
+
/** A finger down in the content, not yet known to be a pull (down) or a scroll. */
|
|
1784
|
+
this.pull = null;
|
|
1598
1785
|
this.onPointerDown = (e) => {
|
|
1599
1786
|
if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
|
|
1600
1787
|
if (this.drag || !isPhoneSheet()) return;
|
|
@@ -1620,45 +1807,54 @@ var BottomSheet = class {
|
|
|
1620
1807
|
if (!drag.moving) {
|
|
1621
1808
|
if (dy < -SLOP) return this.abandon();
|
|
1622
1809
|
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));
|
|
1810
|
+
this.startMoving(drag);
|
|
1640
1811
|
}
|
|
1812
|
+
this.follow(drag, e.clientY, e.timeStamp);
|
|
1641
1813
|
if (e.cancelable) e.preventDefault();
|
|
1642
1814
|
};
|
|
1643
1815
|
this.onPointerUp = (e) => {
|
|
1644
1816
|
const drag = this.drag;
|
|
1645
1817
|
if (!drag || e.pointerId !== drag.pointerId) return;
|
|
1646
1818
|
this.stopTracking();
|
|
1647
|
-
this.drag
|
|
1648
|
-
|
|
1649
|
-
|
|
1819
|
+
this.finish(drag);
|
|
1820
|
+
};
|
|
1821
|
+
// ── Pull from the content (touch) ─────────────────────────────────────────
|
|
1822
|
+
this.onTouchStart = (e) => {
|
|
1823
|
+
this.pull = null;
|
|
1824
|
+
if (this.drag || e.touches.length !== 1 || !isPhoneSheet()) return;
|
|
1650
1825
|
const sheet = this.opts.sheet();
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1826
|
+
if (!sheet || !e.composedPath().includes(sheet)) return;
|
|
1827
|
+
if (inZone(e, sheet, this.opts.dragZones)) return;
|
|
1828
|
+
const t = e.touches[0];
|
|
1829
|
+
if (t) this.pull = { startX: t.clientX, startY: t.clientY };
|
|
1830
|
+
};
|
|
1831
|
+
this.onTouchMove = (e) => {
|
|
1832
|
+
const t = e.touches[0];
|
|
1833
|
+
if (!t) return;
|
|
1834
|
+
if (this.pull) {
|
|
1835
|
+
const dx = t.clientX - this.pull.startX;
|
|
1836
|
+
const dy = t.clientY - this.pull.startY;
|
|
1837
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) <= SLOP) return;
|
|
1838
|
+
const startY = this.pull.startY;
|
|
1839
|
+
this.pull = null;
|
|
1840
|
+
if (Math.abs(dx) >= Math.abs(dy) || dy < 0 || this.scrolledDown(e)) return;
|
|
1841
|
+
this.drag = { pointerId: -1, startY, lastY: startY, lastT: e.timeStamp, velocity: 0, moving: false, offset: 0 };
|
|
1842
|
+
this.startMoving(this.drag);
|
|
1661
1843
|
}
|
|
1844
|
+
const drag = this.drag;
|
|
1845
|
+
if (!drag || drag.pointerId !== -1) return;
|
|
1846
|
+
this.follow(drag, t.clientY, e.timeStamp);
|
|
1847
|
+
if (e.cancelable) e.preventDefault();
|
|
1848
|
+
};
|
|
1849
|
+
this.onTouchEnd = (e) => {
|
|
1850
|
+
this.pull = null;
|
|
1851
|
+
const drag = this.drag;
|
|
1852
|
+
if (!drag || drag.pointerId !== -1 || e.touches.length > 0) return;
|
|
1853
|
+
this.finish(drag);
|
|
1854
|
+
};
|
|
1855
|
+
this.onTouchCancel = () => {
|
|
1856
|
+
this.pull = null;
|
|
1857
|
+
if (this.drag?.pointerId === -1) this.abandon();
|
|
1662
1858
|
};
|
|
1663
1859
|
this.onPointerCancel = (e) => {
|
|
1664
1860
|
if (!this.drag || e.pointerId !== this.drag.pointerId) return;
|
|
@@ -1693,12 +1889,21 @@ var BottomSheet = class {
|
|
|
1693
1889
|
}
|
|
1694
1890
|
this.opts.host.addEventListener("pointerdown", this.onPointerDown);
|
|
1695
1891
|
this.opts.host.addEventListener("click", this.onClickCapture, true);
|
|
1892
|
+
this.opts.host.addEventListener("touchstart", this.onTouchStart, { passive: true });
|
|
1893
|
+
this.opts.host.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
1894
|
+
this.opts.host.addEventListener("touchend", this.onTouchEnd);
|
|
1895
|
+
this.opts.host.addEventListener("touchcancel", this.onTouchCancel);
|
|
1696
1896
|
}
|
|
1697
1897
|
closed() {
|
|
1698
1898
|
this.opts.host.removeEventListener("pointerdown", this.onPointerDown);
|
|
1699
1899
|
this.opts.host.removeEventListener("click", this.onClickCapture, true);
|
|
1900
|
+
this.opts.host.removeEventListener("touchstart", this.onTouchStart);
|
|
1901
|
+
this.opts.host.removeEventListener("touchmove", this.onTouchMove);
|
|
1902
|
+
this.opts.host.removeEventListener("touchend", this.onTouchEnd);
|
|
1903
|
+
this.opts.host.removeEventListener("touchcancel", this.onTouchCancel);
|
|
1700
1904
|
this.stopTracking();
|
|
1701
1905
|
this.drag = null;
|
|
1906
|
+
this.pull = null;
|
|
1702
1907
|
const sheet = this.opts.sheet();
|
|
1703
1908
|
if (sheet) {
|
|
1704
1909
|
sheet.style.transform = "";
|
|
@@ -1714,6 +1919,58 @@ var BottomSheet = class {
|
|
|
1714
1919
|
unlockPageScroll();
|
|
1715
1920
|
}
|
|
1716
1921
|
}
|
|
1922
|
+
/** Is the content under the finger scrolled away from its top (the sheet's scroller, or one inside it)? */
|
|
1923
|
+
scrolledDown(e) {
|
|
1924
|
+
const sheet = this.opts.sheet();
|
|
1925
|
+
const scroller = this.opts.scroller?.() ?? sheet;
|
|
1926
|
+
if (scroller && scroller.scrollTop > 0) return true;
|
|
1927
|
+
for (const node of e.composedPath()) {
|
|
1928
|
+
if (node === sheet) break;
|
|
1929
|
+
if (node instanceof HTMLElement && node.scrollTop > 0 && node.scrollHeight > node.clientHeight) return true;
|
|
1930
|
+
}
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1933
|
+
// ── Shared by both ────────────────────────────────────────────────────────
|
|
1934
|
+
/** The press is a drag now: the sheet and backdrop follow the finger without easing. */
|
|
1935
|
+
startMoving(drag) {
|
|
1936
|
+
drag.moving = true;
|
|
1937
|
+
const sheet = this.opts.sheet();
|
|
1938
|
+
if (sheet) sheet.style.transition = "none";
|
|
1939
|
+
const backdrop = this.opts.backdrop?.();
|
|
1940
|
+
if (backdrop) backdrop.style.transition = "none";
|
|
1941
|
+
}
|
|
1942
|
+
follow(drag, y, timeStamp) {
|
|
1943
|
+
const dt = timeStamp - drag.lastT;
|
|
1944
|
+
if (dt > 0) drag.velocity = 0.8 * ((y - drag.lastY) / dt) + 0.2 * drag.velocity;
|
|
1945
|
+
drag.lastY = y;
|
|
1946
|
+
drag.lastT = timeStamp;
|
|
1947
|
+
drag.offset = Math.max(0, y - drag.startY);
|
|
1948
|
+
const sheet = this.opts.sheet();
|
|
1949
|
+
if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
|
|
1950
|
+
const backdrop = this.opts.backdrop?.();
|
|
1951
|
+
if (backdrop && sheet) {
|
|
1952
|
+
const height = sheet.getBoundingClientRect().height || window.innerHeight;
|
|
1953
|
+
backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
/** Let go: close if far or fast enough (and allowed), else spring back. */
|
|
1957
|
+
finish(drag) {
|
|
1958
|
+
this.drag = null;
|
|
1959
|
+
if (!drag.moving) return;
|
|
1960
|
+
this.dragEndedAt = performance.now();
|
|
1961
|
+
const sheet = this.opts.sheet();
|
|
1962
|
+
const height = sheet?.getBoundingClientRect().height || window.innerHeight;
|
|
1963
|
+
const far = drag.offset >= Math.min(CLOSE_DISTANCE, height / 4);
|
|
1964
|
+
const flick = drag.velocity >= FLICK_VELOCITY && drag.offset >= FLICK_MIN;
|
|
1965
|
+
const allowed = !this.opts.canClose || this.opts.canClose();
|
|
1966
|
+
this.release();
|
|
1967
|
+
if ((far || flick) && allowed) {
|
|
1968
|
+
if (sheet) sheet.style.transform = "translateY(100%)";
|
|
1969
|
+
this.opts.close();
|
|
1970
|
+
} else if (sheet) {
|
|
1971
|
+
sheet.style.transform = "";
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1717
1974
|
/** Give up on this press and put the sheet back where it was. */
|
|
1718
1975
|
abandon() {
|
|
1719
1976
|
const moving = this.drag?.moving;
|
|
@@ -1780,9 +2037,40 @@ function unlockPageScroll() {
|
|
|
1780
2037
|
}
|
|
1781
2038
|
|
|
1782
2039
|
// src/ui/components/vio-product-detail.ts
|
|
2040
|
+
var ADDED_TOAST_MS = 8e3;
|
|
1783
2041
|
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
1784
2042
|
'<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
2043
|
);
|
|
2044
|
+
var HIGHLIGHT_ICONS = {
|
|
2045
|
+
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",
|
|
2046
|
+
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",
|
|
2047
|
+
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",
|
|
2048
|
+
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",
|
|
2049
|
+
return: "M4 9h11.5a4.5 4.5 0 0 1 0 9H8M4 9l4-4M4 9l4 4",
|
|
2050
|
+
leaf: "M5 19C5 10 10 4.5 20 4.5c0 10-5.5 15-14 15M5 19l7-7",
|
|
2051
|
+
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",
|
|
2052
|
+
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",
|
|
2053
|
+
mailbox: "M3 19v-8a4 4 0 0 1 8 0v8m-8 0h18v-7a4 4 0 0 0-4-4H7m8 0V4h3M6 12h2",
|
|
2054
|
+
home: "M3 11 12 4l9 7M5.5 9.5V20h13V9.5M10 20v-5h4v5",
|
|
2055
|
+
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",
|
|
2056
|
+
clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13.5V12l3 2"
|
|
2057
|
+
};
|
|
2058
|
+
function rateIcon(method) {
|
|
2059
|
+
const m = method.toLowerCase();
|
|
2060
|
+
if (/postkasse|mailbox|brevkasse/.test(m)) return "mailbox";
|
|
2061
|
+
if (/hente|pakkeboks|automat|postkontor|pickup|utlever/.test(m)) return "pin";
|
|
2062
|
+
if (/dør|hjem|home|door/.test(m)) return "home";
|
|
2063
|
+
return "truck";
|
|
2064
|
+
}
|
|
2065
|
+
function carrierLogo(logos, carrier) {
|
|
2066
|
+
if (!logos || !carrier) return "";
|
|
2067
|
+
const want = carrier.trim().toLowerCase();
|
|
2068
|
+
for (const [name, url] of Object.entries(logos)) if (name.trim().toLowerCase() === want && url) return url;
|
|
2069
|
+
return "";
|
|
2070
|
+
}
|
|
2071
|
+
function lineIcon(name, cls = "line-icon") {
|
|
2072
|
+
return html`<svg class=${cls} viewBox="0 0 24 24" aria-hidden="true"><path d=${HIGHLIGHT_ICONS[name] ?? ""}></path></svg>`;
|
|
2073
|
+
}
|
|
1786
2074
|
var VioProductDetail = class extends LitElement {
|
|
1787
2075
|
constructor() {
|
|
1788
2076
|
super(...arguments);
|
|
@@ -1798,6 +2086,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
1798
2086
|
this.vioCampaign = "";
|
|
1799
2087
|
this.vioContentId = "";
|
|
1800
2088
|
this.vioContentTitle = "";
|
|
2089
|
+
this.noAddedToast = false;
|
|
2090
|
+
this.gallery = "";
|
|
2091
|
+
this.descriptionMode = "";
|
|
2092
|
+
this.highlights = [];
|
|
2093
|
+
this.deliveryTime = "";
|
|
2094
|
+
this.carrierLogos = {};
|
|
1801
2095
|
this.slotRegistration = new SlotRegistration(this, "detail");
|
|
1802
2096
|
/** What the click that opened the detail carried ('' when nothing), and for which product. */
|
|
1803
2097
|
this.eventSlot = "";
|
|
@@ -1808,18 +2102,31 @@ var VioProductDetail = class extends LitElement {
|
|
|
1808
2102
|
this.product = null;
|
|
1809
2103
|
this.sellerName = null;
|
|
1810
2104
|
this.sellerSettled = false;
|
|
2105
|
+
this.shippingFrom = null;
|
|
2106
|
+
this.shippingRates = [];
|
|
1811
2107
|
this.isLoading = false;
|
|
1812
2108
|
this.fetchError = "";
|
|
1813
2109
|
this.selectedOptions = {};
|
|
2110
|
+
this.selectionPrompt = "";
|
|
1814
2111
|
this.quantity = 1;
|
|
1815
2112
|
this.activeImageIndex = 0;
|
|
1816
2113
|
this.thumbsOverflow = false;
|
|
1817
2114
|
this.thumbsAtStart = true;
|
|
1818
2115
|
this.thumbsAtEnd = false;
|
|
2116
|
+
this.descriptionOpen = false;
|
|
2117
|
+
this.descriptionClipped = false;
|
|
2118
|
+
this.galleryMode = "thumbs";
|
|
2119
|
+
this.resolvedDescriptionMode = "preview";
|
|
1819
2120
|
this.logoSquare = false;
|
|
1820
2121
|
this.adding = false;
|
|
2122
|
+
this.addedToast = null;
|
|
2123
|
+
this.addedToastTimer = null;
|
|
1821
2124
|
this.applePayOk = false;
|
|
1822
2125
|
this.availableMethods = null;
|
|
2126
|
+
this.methodsLoading = false;
|
|
2127
|
+
this.vippsExpress = false;
|
|
2128
|
+
/** Whose methods `availableMethods` are — another brand's are not shown while its own load. */
|
|
2129
|
+
this.methodsSponsorId = 0;
|
|
1823
2130
|
this.vippsOfficial = false;
|
|
1824
2131
|
/** Race guard: only the latest fetch may write state (two quick taps on
|
|
1825
2132
|
* different cards would otherwise let the slow response win). */
|
|
@@ -1873,6 +2180,15 @@ var VioProductDetail = class extends LitElement {
|
|
|
1873
2180
|
this.show();
|
|
1874
2181
|
void this.fetchProduct();
|
|
1875
2182
|
};
|
|
2183
|
+
/** The photo in view is the active one (dots, Apple Pay sheet, events). */
|
|
2184
|
+
this.onCarouselScroll = () => {
|
|
2185
|
+
const track = this.carouselTrack();
|
|
2186
|
+
const step = this.carouselStep(track);
|
|
2187
|
+
if (!track || !step) return;
|
|
2188
|
+
const last = track.children.length - 1;
|
|
2189
|
+
const idx = Math.min(last, Math.max(0, Math.round(track.scrollLeft / step)));
|
|
2190
|
+
if (idx !== this.activeImageIndex) this.activeImageIndex = idx;
|
|
2191
|
+
};
|
|
1876
2192
|
this.onThumbsScroll = () => {
|
|
1877
2193
|
const row = this.thumbRow();
|
|
1878
2194
|
if (!row) return;
|
|
@@ -1903,6 +2219,21 @@ var VioProductDetail = class extends LitElement {
|
|
|
1903
2219
|
const own = this.slotRegistration.campaignHint;
|
|
1904
2220
|
return this.fromCard && this.eventCampaignId !== void 0 ? [this.eventCampaignId, ...own] : own;
|
|
1905
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* The campaign whose channel sells the open product (one article = one
|
|
2224
|
+
* campaign = one channel): the campaign its events credit, then the opening
|
|
2225
|
+
* click's and this element's own — so the product, its rates and its
|
|
2226
|
+
* seller are read with that channel's key, the one the cart is created on.
|
|
2227
|
+
*/
|
|
2228
|
+
commerceCampaigns() {
|
|
2229
|
+
const hint = this.activeCampaignHint;
|
|
2230
|
+
try {
|
|
2231
|
+
const resolved = this.attribution().campaignId;
|
|
2232
|
+
return typeof resolved === "number" ? [resolved, ...hint] : hint;
|
|
2233
|
+
} catch {
|
|
2234
|
+
return hint;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
1906
2237
|
slotAttributes() {
|
|
1907
2238
|
return {
|
|
1908
2239
|
vioSlot: this.vioSlot,
|
|
@@ -2187,6 +2518,103 @@ var VioProductDetail = class extends LitElement {
|
|
|
2187
2518
|
.thumbs.overflows .thumbs-arrow { display: flex; }
|
|
2188
2519
|
}
|
|
2189
2520
|
|
|
2521
|
+
/* Carousel (gallery="carousel" / theme detailGallery): the same box as the
|
|
2522
|
+
main image — proportion, fit, background, inset — one per photo, in a
|
|
2523
|
+
row that snaps. --vio-detail-carousel-peek shows the edge of the next. */
|
|
2524
|
+
.carousel {
|
|
2525
|
+
position: relative;
|
|
2526
|
+
margin-inline: calc(
|
|
2527
|
+
var(--vio-detail-image-inset, var(--vio-detail-padding-x, var(--vio-space-lg, 24px))) -
|
|
2528
|
+
var(--vio-detail-padding-x, var(--vio-space-lg, 24px))
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
.carousel-track {
|
|
2532
|
+
display: flex;
|
|
2533
|
+
gap: var(--vio-detail-carousel-gap, 0px);
|
|
2534
|
+
overflow-x: auto;
|
|
2535
|
+
scroll-snap-type: x mandatory;
|
|
2536
|
+
scroll-behavior: smooth;
|
|
2537
|
+
scrollbar-width: none;
|
|
2538
|
+
-webkit-overflow-scrolling: touch;
|
|
2539
|
+
overscroll-behavior-x: contain;
|
|
2540
|
+
/* Sideways is the carousel's, up and down the panel's; no double-tap
|
|
2541
|
+
zoom delay. The sheet only drags from its top bar, never from here. */
|
|
2542
|
+
touch-action: pan-x pan-y;
|
|
2543
|
+
border-radius: var(--vio-detail-image-radius, 0);
|
|
2544
|
+
}
|
|
2545
|
+
.carousel-track::-webkit-scrollbar { display: none; }
|
|
2546
|
+
.carousel-slide {
|
|
2547
|
+
flex: 0 0 calc(100% - var(--vio-detail-carousel-peek, 0px));
|
|
2548
|
+
aspect-ratio: var(--vio-detail-image-aspect, 4 / 5);
|
|
2549
|
+
max-height: var(--vio-detail-image-max-height, 85vh);
|
|
2550
|
+
background: var(--vio-detail-image-bg, var(--vio-color-surface-muted, #f2f2f2));
|
|
2551
|
+
scroll-snap-align: start;
|
|
2552
|
+
scroll-snap-stop: always;
|
|
2553
|
+
overflow: hidden;
|
|
2554
|
+
}
|
|
2555
|
+
.carousel-slide img {
|
|
2556
|
+
display: block;
|
|
2557
|
+
/* A press on a photo is a swipe: no image drag, no long-press menu, no selection. */
|
|
2558
|
+
pointer-events: none;
|
|
2559
|
+
user-select: none;
|
|
2560
|
+
-webkit-user-select: none;
|
|
2561
|
+
-webkit-user-drag: none;
|
|
2562
|
+
-webkit-touch-callout: none;
|
|
2563
|
+
width: 100%;
|
|
2564
|
+
height: 100%;
|
|
2565
|
+
object-fit: var(--vio-detail-image-fit, cover);
|
|
2566
|
+
object-position: var(--vio-detail-image-position, center);
|
|
2567
|
+
transform: scale(var(--vio-detail-image-zoom, 1));
|
|
2568
|
+
}
|
|
2569
|
+
/* One dot per photo over the bottom of the image: there is more than one,
|
|
2570
|
+
and which one this is. White with a shadow reads on light and dark photos. */
|
|
2571
|
+
.carousel-dots {
|
|
2572
|
+
position: absolute;
|
|
2573
|
+
left: 0;
|
|
2574
|
+
right: 0;
|
|
2575
|
+
bottom: 12px;
|
|
2576
|
+
display: flex;
|
|
2577
|
+
justify-content: center;
|
|
2578
|
+
gap: 6px;
|
|
2579
|
+
pointer-events: none;
|
|
2580
|
+
}
|
|
2581
|
+
.carousel-dot {
|
|
2582
|
+
width: 6px;
|
|
2583
|
+
height: 6px;
|
|
2584
|
+
border-radius: 50%;
|
|
2585
|
+
background: var(--vio-detail-dot-color, rgba(255, 255, 255, 0.6));
|
|
2586
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.35);
|
|
2587
|
+
transition: background 0.2s ease, transform 0.2s ease;
|
|
2588
|
+
}
|
|
2589
|
+
.carousel-dot.active {
|
|
2590
|
+
background: var(--vio-detail-dot-active-color, #fff);
|
|
2591
|
+
transform: scale(1.35);
|
|
2592
|
+
}
|
|
2593
|
+
.carousel-arrow {
|
|
2594
|
+
display: none;
|
|
2595
|
+
position: absolute;
|
|
2596
|
+
top: 50%;
|
|
2597
|
+
transform: translateY(-50%);
|
|
2598
|
+
width: 36px;
|
|
2599
|
+
height: 36px;
|
|
2600
|
+
border-radius: 50%;
|
|
2601
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2602
|
+
background: var(--vio-color-surface, #fff);
|
|
2603
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
2604
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
2605
|
+
cursor: pointer;
|
|
2606
|
+
align-items: center;
|
|
2607
|
+
justify-content: center;
|
|
2608
|
+
padding: 0;
|
|
2609
|
+
font: 18px/1 sans-serif;
|
|
2610
|
+
}
|
|
2611
|
+
.carousel-arrow.prev { left: 12px; }
|
|
2612
|
+
.carousel-arrow.next { right: 12px; }
|
|
2613
|
+
.carousel-arrow:disabled { visibility: hidden; }
|
|
2614
|
+
@media (hover: hover) and (pointer: fine) {
|
|
2615
|
+
.carousel-arrow { display: flex; }
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2190
2618
|
/* Info column */
|
|
2191
2619
|
.info { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); min-width: 0; }
|
|
2192
2620
|
.brand {
|
|
@@ -2204,11 +2632,88 @@ var VioProductDetail = class extends LitElement {
|
|
|
2204
2632
|
margin: 0;
|
|
2205
2633
|
letter-spacing: -0.01em;
|
|
2206
2634
|
}
|
|
2635
|
+
/* The brand's promises: one row, an icon over a title and a line of text,
|
|
2636
|
+
centred like a shop's own trust strip. */
|
|
2637
|
+
.highlights {
|
|
2638
|
+
list-style: none;
|
|
2639
|
+
/* --vio-detail-highlights-display: none hides the strip (theme detailHighlightsDisplay). */
|
|
2640
|
+
margin: 0;
|
|
2641
|
+
padding: var(--vio-detail-highlights-padding, 16px 0 2px);
|
|
2642
|
+
display: var(--vio-detail-highlights-display, grid);
|
|
2643
|
+
grid-auto-flow: column;
|
|
2644
|
+
grid-auto-columns: 1fr;
|
|
2645
|
+
gap: 10px;
|
|
2646
|
+
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2647
|
+
}
|
|
2648
|
+
/* Like a shop's trust strip: a thin icon, the promise in regular weight,
|
|
2649
|
+
a smaller line under it — nothing bold. */
|
|
2650
|
+
.hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
|
|
2651
|
+
.hl-icon {
|
|
2652
|
+
width: var(--vio-detail-highlight-icon-size, 22px);
|
|
2653
|
+
height: var(--vio-detail-highlight-icon-size, 22px);
|
|
2654
|
+
margin-bottom: 8px;
|
|
2655
|
+
fill: none;
|
|
2656
|
+
stroke: currentColor;
|
|
2657
|
+
stroke-width: 1.2;
|
|
2658
|
+
stroke-linecap: round;
|
|
2659
|
+
stroke-linejoin: round;
|
|
2660
|
+
object-fit: contain;
|
|
2661
|
+
}
|
|
2662
|
+
.hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
|
|
2663
|
+
.hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
|
|
2664
|
+
/* «Frakt & retur»: same fold as «Beskrivelse», with a small list of rates.
|
|
2665
|
+
--vio-detail-shipping-returns-display: none hides it (theme detailShippingReturnsDisplay). */
|
|
2666
|
+
.shipping-returns { display: var(--vio-detail-shipping-returns-display, block); }
|
|
2667
|
+
.description.shipping-returns-body { white-space: normal; }
|
|
2668
|
+
.shipping-returns-body p { margin: 0 0 10px; }
|
|
2669
|
+
.sr-title {
|
|
2670
|
+
font-size: 11px;
|
|
2671
|
+
text-transform: uppercase;
|
|
2672
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
2673
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2674
|
+
font-weight: 600;
|
|
2675
|
+
margin: 4px 0 6px;
|
|
2676
|
+
}
|
|
2677
|
+
.sr-rates { list-style: none; margin: 0 0 10px; padding: 0; }
|
|
2678
|
+
/* One row per carrier: «Helthjem» over «Til postkasse/dørmatte», price right. */
|
|
2679
|
+
.sr-rates li {
|
|
2680
|
+
display: flex;
|
|
2681
|
+
align-items: center;
|
|
2682
|
+
gap: 12px;
|
|
2683
|
+
padding: 8px 0;
|
|
2684
|
+
border-bottom: 1px solid var(--vio-color-border, #eee);
|
|
2685
|
+
}
|
|
2686
|
+
.sr-rates li:last-child { border-bottom: none; }
|
|
2687
|
+
.sr-rate { flex: 1; display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
|
|
2688
|
+
.line-icon { fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
|
|
2689
|
+
.sr-icon { flex: 0 0 auto; width: 20px; height: 20px; color: var(--vio-color-text, #0a0a0a); }
|
|
2690
|
+
.sr-line { display: flex; align-items: center; gap: 12px; }
|
|
2691
|
+
/* A carrier's logo in a fixed box, so rows line up whatever its shape. */
|
|
2692
|
+
.sr-logo { flex: 0 0 auto; width: var(--vio-detail-carrier-logo-width, 56px); height: 24px; object-fit: contain; object-position: left center; }
|
|
2693
|
+
.sr-carrier { font-weight: 600; color: var(--vio-color-text, #0a0a0a); }
|
|
2694
|
+
.sr-method { font-size: 13px; }
|
|
2695
|
+
.sr-time { margin: 0 0 14px; }
|
|
2696
|
+
.sr-price { font-weight: 600; color: var(--vio-color-text, #0a0a0a); white-space: nowrap; }
|
|
2697
|
+
.sr-links { display: flex; gap: 16px; padding-left: 32px; }
|
|
2698
|
+
.sr-links a { color: inherit; }
|
|
2699
|
+
/* Name and price read as one block (stock on the name's line, right);
|
|
2700
|
+
the sizes come after a wider gap, then shipping/returns by the buttons. */
|
|
2701
|
+
.headline { display: flex; flex-direction: column; gap: var(--vio-detail-headline-gap, 8px); }
|
|
2702
|
+
.title-row {
|
|
2703
|
+
display: flex;
|
|
2704
|
+
align-items: baseline;
|
|
2705
|
+
justify-content: space-between;
|
|
2706
|
+
gap: 16px;
|
|
2707
|
+
}
|
|
2708
|
+
.title-row .name { min-width: 0; }
|
|
2709
|
+
.title-row .stock { flex: 0 0 auto; white-space: nowrap; }
|
|
2207
2710
|
.price-row {
|
|
2208
2711
|
display: flex;
|
|
2209
2712
|
align-items: baseline;
|
|
2210
2713
|
gap: 16px;
|
|
2211
2714
|
}
|
|
2715
|
+
/* The sizes follow the price closely: they are the next thing to do. */
|
|
2716
|
+
.headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
|
|
2212
2717
|
.price {
|
|
2213
2718
|
font-size: var(--vio-detail-price-size, 24px);
|
|
2214
2719
|
font-weight: var(--vio-detail-price-weight, 700);
|
|
@@ -2218,6 +2723,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
2218
2723
|
color: var(--vio-color-text-tertiary, #999);
|
|
2219
2724
|
text-decoration: line-through;
|
|
2220
2725
|
}
|
|
2726
|
+
.delivery-note {
|
|
2727
|
+
margin-top: calc(var(--vio-detail-info-gap, 20px) * -0.6);
|
|
2728
|
+
font-size: 13px;
|
|
2729
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2730
|
+
}
|
|
2221
2731
|
.stock {
|
|
2222
2732
|
font-size: 12px;
|
|
2223
2733
|
text-transform: uppercase;
|
|
@@ -2229,8 +2739,15 @@ var VioProductDetail = class extends LitElement {
|
|
|
2229
2739
|
.stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
|
|
2230
2740
|
|
|
2231
2741
|
/* Option pickers */
|
|
2232
|
-
.option-group { display: flex; flex-direction: column; gap:
|
|
2742
|
+
.option-group { display: flex; flex-direction: column; gap: 8px; }
|
|
2743
|
+
/* The prompt sits on the label's own line: tapping a buy button before
|
|
2744
|
+
choosing must not push the pills down under the shopper's finger. */
|
|
2233
2745
|
.option-label {
|
|
2746
|
+
display: flex;
|
|
2747
|
+
align-items: baseline;
|
|
2748
|
+
justify-content: space-between;
|
|
2749
|
+
gap: 12px;
|
|
2750
|
+
min-height: 1.4em;
|
|
2234
2751
|
font-size: 11px;
|
|
2235
2752
|
text-transform: uppercase;
|
|
2236
2753
|
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
@@ -2276,6 +2793,19 @@ var VioProductDetail = class extends LitElement {
|
|
|
2276
2793
|
background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
|
|
2277
2794
|
}
|
|
2278
2795
|
.option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
|
|
2796
|
+
/* The value chosen, beside the option's name: «Størrelse: M». */
|
|
2797
|
+
.option-chosen { color: var(--vio-color-text, #0a0a0a); font-weight: 700; }
|
|
2798
|
+
/* A buy button was tapped before choosing: say what is missing. */
|
|
2799
|
+
.option-prompt {
|
|
2800
|
+
text-transform: none;
|
|
2801
|
+
letter-spacing: normal;
|
|
2802
|
+
font-size: 13px;
|
|
2803
|
+
font-weight: 600;
|
|
2804
|
+
color: var(--vio-color-accent, #c14a3b);
|
|
2805
|
+
}
|
|
2806
|
+
.option-group.needs-choice .option-pill:not(.unavailable) {
|
|
2807
|
+
border-color: var(--vio-color-accent, #c14a3b);
|
|
2808
|
+
}
|
|
2279
2809
|
|
|
2280
2810
|
/* The buy zone: Vipps first, then quantity + add to cart, then the other
|
|
2281
2811
|
ways to pay. --vio-detail-buy-position: sticky pins it to the bottom of
|
|
@@ -2292,6 +2822,37 @@ var VioProductDetail = class extends LitElement {
|
|
|
2292
2822
|
box-shadow: var(--vio-detail-buy-shadow, none);
|
|
2293
2823
|
}
|
|
2294
2824
|
.buy > * { margin-top: 0 !important; }
|
|
2825
|
+
/* The buy zone and the description, apart from the name, price and stock:
|
|
2826
|
+
a pinned buy zone can slide up over what is in this box (the
|
|
2827
|
+
description) but never above it — never over the name, the price or
|
|
2828
|
+
the stock line (Mote & Livsstil, 2026-10-28). */
|
|
2829
|
+
.purchase { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); }
|
|
2830
|
+
/* The pickers live in the buy zone: when it is pinned to the bottom of the
|
|
2831
|
+
panel (desktop, or a host's sticky), it can never cover them — they
|
|
2832
|
+
travel with the buttons they gate (Mote & Livsstil, 2026-10-28). */
|
|
2833
|
+
.buy-options {
|
|
2834
|
+
display: flex;
|
|
2835
|
+
flex-direction: column;
|
|
2836
|
+
gap: var(--vio-detail-info-gap, 20px);
|
|
2837
|
+
padding-bottom: 4px;
|
|
2838
|
+
}
|
|
2839
|
+
/* Where the express buttons go while the brand's methods load: neutral,
|
|
2840
|
+
the height of one button, no method named. */
|
|
2841
|
+
.buy-pending {
|
|
2842
|
+
height: var(--vio-detail-button-height, 52px);
|
|
2843
|
+
border-radius: var(--vio-detail-button-radius, 0);
|
|
2844
|
+
background: linear-gradient(
|
|
2845
|
+
100deg,
|
|
2846
|
+
var(--vio-color-surface-muted, #f1f1f1) 30%,
|
|
2847
|
+
rgba(255, 255, 255, 0.65) 50%,
|
|
2848
|
+
var(--vio-color-surface-muted, #f1f1f1) 70%
|
|
2849
|
+
);
|
|
2850
|
+
background-size: 200% 100%;
|
|
2851
|
+
animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
|
|
2852
|
+
}
|
|
2853
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2854
|
+
.buy-pending { animation: none; background: var(--vio-color-surface-muted, #f1f1f1); }
|
|
2855
|
+
}
|
|
2295
2856
|
/* Under the buttons: who the shopper is buying from. */
|
|
2296
2857
|
.seller-note {
|
|
2297
2858
|
margin: 2px 0 0;
|
|
@@ -2424,6 +2985,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
2424
2985
|
}
|
|
2425
2986
|
.buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
|
|
2426
2987
|
.buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2988
|
+
/* Under the Express button: what happens in the app. */
|
|
2989
|
+
.vipps-note {
|
|
2990
|
+
margin: 0;
|
|
2991
|
+
font-size: 12px;
|
|
2992
|
+
line-height: 1.4;
|
|
2993
|
+
color: var(--vio-color-text-secondary, #666);
|
|
2994
|
+
text-align: center;
|
|
2995
|
+
}
|
|
2427
2996
|
/* Vipps' own button (web component) — same slot, their drawing. */
|
|
2428
2997
|
.buy-vipps-official { display: block; width: 100%; margin-top: 10px; cursor: pointer; }
|
|
2429
2998
|
/* Apple Pay express button — black, per Apple's button guidelines. Only
|
|
@@ -2475,6 +3044,61 @@ var VioProductDetail = class extends LitElement {
|
|
|
2475
3044
|
transition: transform 0.15s ease;
|
|
2476
3045
|
}
|
|
2477
3046
|
.description-block[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
|
|
3047
|
+
/* «Beskrivelse» with a preview: the same heading as the folds, then the
|
|
3048
|
+
first lines fading out, then «Les mer». */
|
|
3049
|
+
.desc-head {
|
|
3050
|
+
all: unset;
|
|
3051
|
+
box-sizing: border-box;
|
|
3052
|
+
width: 100%;
|
|
3053
|
+
cursor: pointer;
|
|
3054
|
+
display: flex;
|
|
3055
|
+
align-items: center;
|
|
3056
|
+
justify-content: space-between;
|
|
3057
|
+
padding: 14px 0 6px;
|
|
3058
|
+
font-size: 11px;
|
|
3059
|
+
text-transform: uppercase;
|
|
3060
|
+
letter-spacing: var(--vio-tracking-label, 0.1em);
|
|
3061
|
+
color: var(--vio-color-text-secondary, #666);
|
|
3062
|
+
font-weight: 600;
|
|
3063
|
+
font-family: inherit;
|
|
3064
|
+
}
|
|
3065
|
+
.description-preview.clipped .desc-head::after {
|
|
3066
|
+
content: '';
|
|
3067
|
+
width: 8px;
|
|
3068
|
+
height: 8px;
|
|
3069
|
+
border-right: 1.5px solid currentColor;
|
|
3070
|
+
border-bottom: 1.5px solid currentColor;
|
|
3071
|
+
transform: rotate(45deg) translateY(-2px);
|
|
3072
|
+
transition: transform 0.15s ease;
|
|
3073
|
+
}
|
|
3074
|
+
.description-preview.open .desc-head::after { transform: rotate(-135deg) translateY(-2px); }
|
|
3075
|
+
.description-preview .description { padding-bottom: 6px; }
|
|
3076
|
+
.description-preview.folded .desc-head, .description-preview.open:not(.clipped) .desc-head { padding-bottom: 14px; }
|
|
3077
|
+
.description-preview.folded.open .desc-head { padding-bottom: 6px; }
|
|
3078
|
+
.description-preview.folded .description { padding-bottom: 14px; }
|
|
3079
|
+
.description-preview:not(.clipped) .description { padding-bottom: 14px; }
|
|
3080
|
+
.description.collapsed {
|
|
3081
|
+
display: -webkit-box;
|
|
3082
|
+
-webkit-box-orient: vertical;
|
|
3083
|
+
-webkit-line-clamp: var(--vio-detail-description-preview-lines, 3);
|
|
3084
|
+
line-clamp: var(--vio-detail-description-preview-lines, 3);
|
|
3085
|
+
overflow: hidden;
|
|
3086
|
+
cursor: pointer;
|
|
3087
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent);
|
|
3088
|
+
mask-image: linear-gradient(to bottom, #000 45%, transparent);
|
|
3089
|
+
}
|
|
3090
|
+
.desc-more {
|
|
3091
|
+
all: unset;
|
|
3092
|
+
cursor: pointer;
|
|
3093
|
+
display: inline-block;
|
|
3094
|
+
margin: 0 0 14px;
|
|
3095
|
+
font-size: 13px;
|
|
3096
|
+
font-weight: 600;
|
|
3097
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3098
|
+
text-decoration: underline;
|
|
3099
|
+
text-underline-offset: 3px;
|
|
3100
|
+
font-family: inherit;
|
|
3101
|
+
}
|
|
2478
3102
|
.description {
|
|
2479
3103
|
font-size: 14px;
|
|
2480
3104
|
line-height: 1.6;
|
|
@@ -2482,15 +3106,13 @@ var VioProductDetail = class extends LitElement {
|
|
|
2482
3106
|
padding-bottom: 14px;
|
|
2483
3107
|
white-space: pre-line;
|
|
2484
3108
|
}
|
|
2485
|
-
/*
|
|
2486
|
-
(
|
|
3109
|
+
/* Same order on every screen: buy, then the description at the very
|
|
3110
|
+
bottom (folded). Hosts that want the buy zone pinned can still set
|
|
3111
|
+
--vio-detail-buy-position: sticky. */
|
|
2487
3112
|
@media (min-width: 601px) {
|
|
2488
|
-
.description-block { order: 1; }
|
|
2489
3113
|
.buy {
|
|
2490
|
-
|
|
2491
|
-
position: var(--vio-detail-buy-position, sticky);
|
|
3114
|
+
position: var(--vio-detail-buy-position, static);
|
|
2492
3115
|
padding: var(--vio-detail-buy-padding, 12px 0 16px);
|
|
2493
|
-
box-shadow: var(--vio-detail-buy-shadow, 0 -1px 0 var(--vio-color-border, #e5e5e5));
|
|
2494
3116
|
}
|
|
2495
3117
|
}
|
|
2496
3118
|
|
|
@@ -2530,6 +3152,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
2530
3152
|
background: var(--vio-color-surface-muted, #f1f1f1);
|
|
2531
3153
|
}
|
|
2532
3154
|
}
|
|
3155
|
+
/* «Lagt i handlekurven»: above the page, below where a thumb reaches. */
|
|
3156
|
+
.added-toast {
|
|
3157
|
+
position: fixed;
|
|
3158
|
+
left: 50%;
|
|
3159
|
+
bottom: max(16px, env(safe-area-inset-bottom));
|
|
3160
|
+
transform: translateX(-50%);
|
|
3161
|
+
z-index: 1102;
|
|
3162
|
+
width: min(420px, calc(100vw - 32px));
|
|
3163
|
+
box-sizing: border-box;
|
|
3164
|
+
padding: 14px 16px;
|
|
3165
|
+
background: var(--vio-color-surface, #fff);
|
|
3166
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3167
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
3168
|
+
border-radius: var(--vio-radius-lg, 8px);
|
|
3169
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
|
3170
|
+
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
3171
|
+
display: flex;
|
|
3172
|
+
flex-direction: column;
|
|
3173
|
+
gap: 12px;
|
|
3174
|
+
}
|
|
3175
|
+
.added-toast-text { font-size: 14px; font-weight: 600; }
|
|
3176
|
+
.added-toast-text::before { content: '✓ '; color: #2e7d32; }
|
|
3177
|
+
.added-toast-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
3178
|
+
.added-toast-actions button {
|
|
3179
|
+
min-height: 44px;
|
|
3180
|
+
padding: 0 12px;
|
|
3181
|
+
font-family: inherit;
|
|
3182
|
+
font-size: 13px;
|
|
3183
|
+
font-weight: 600;
|
|
3184
|
+
cursor: pointer;
|
|
3185
|
+
border-radius: var(--vio-detail-button-radius, 0);
|
|
3186
|
+
}
|
|
3187
|
+
.added-toast-checkout {
|
|
3188
|
+
background: var(--vio-color-text, #0a0a0a);
|
|
3189
|
+
color: var(--vio-color-text-on-primary, #fff);
|
|
3190
|
+
border: 1px solid var(--vio-color-text, #0a0a0a);
|
|
3191
|
+
}
|
|
3192
|
+
.added-toast-continue {
|
|
3193
|
+
background: transparent;
|
|
3194
|
+
color: var(--vio-color-text, #0a0a0a);
|
|
3195
|
+
border: 1px solid var(--vio-color-border, #e5e5e5);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2533
3198
|
.error {
|
|
2534
3199
|
padding: 80px 24px;
|
|
2535
3200
|
text-align: center;
|
|
@@ -2545,6 +3210,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
2545
3210
|
document.addEventListener("vio:product-click", this.boundProductClick);
|
|
2546
3211
|
}
|
|
2547
3212
|
disconnectedCallback() {
|
|
3213
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3214
|
+
this.addedToastTimer = null;
|
|
2548
3215
|
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
2549
3216
|
this.slotRegistration.clear();
|
|
2550
3217
|
this.bottomSheet.disconnect();
|
|
@@ -2555,16 +3222,63 @@ var VioProductDetail = class extends LitElement {
|
|
|
2555
3222
|
this.dialogFocus.update(this.open);
|
|
2556
3223
|
this.bottomSheet.update(this.open);
|
|
2557
3224
|
}
|
|
2558
|
-
if (changed.has("product") || changed.has("open"))
|
|
3225
|
+
if (changed.has("product") || changed.has("open")) {
|
|
3226
|
+
this.measureThumbs();
|
|
3227
|
+
this.measureDescription();
|
|
3228
|
+
}
|
|
2559
3229
|
if ((changed.has("quantity") || changed.has("selectedOptions")) && this.product && this.applePayAskedFor !== null) {
|
|
2560
3230
|
if (this.unitPrice * this.quantity !== this.applePayAskedFor) this.prepareApplePaySheet();
|
|
2561
3231
|
}
|
|
2562
|
-
if (changed.has("activeImageIndex"))
|
|
3232
|
+
if (changed.has("activeImageIndex")) {
|
|
3233
|
+
this.revealActiveThumb();
|
|
3234
|
+
this.revealActiveSlide();
|
|
3235
|
+
}
|
|
3236
|
+
if (changed.has("selectedOptions") && this.galleryMode === "carousel") {
|
|
3237
|
+
if (this.activeImageIndex !== 0) this.activeImageIndex = 0;
|
|
3238
|
+
else this.revealActiveSlide();
|
|
3239
|
+
}
|
|
2563
3240
|
this.measureLogo();
|
|
2564
3241
|
if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
|
|
2565
3242
|
this.slotRegistration.sync(this.slotAttributes());
|
|
2566
3243
|
}
|
|
2567
3244
|
}
|
|
3245
|
+
willUpdate(changed) {
|
|
3246
|
+
if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
|
|
3247
|
+
if (changed.has("descriptionMode") || changed.has("open") && this.open) {
|
|
3248
|
+
const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
|
|
3249
|
+
this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
/** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
|
|
3253
|
+
themeChoice(own, cssVar) {
|
|
3254
|
+
const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
|
|
3255
|
+
return (own || fromTheme).trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
3256
|
+
}
|
|
3257
|
+
/** The attribute wins; else the theme's `--vio-detail-gallery`; else thumbnails. */
|
|
3258
|
+
resolveGalleryMode() {
|
|
3259
|
+
return this.themeChoice(this.gallery, "--vio-detail-gallery") === "carousel" ? "carousel" : "thumbs";
|
|
3260
|
+
}
|
|
3261
|
+
carouselTrack() {
|
|
3262
|
+
return this.renderRoot.querySelector(".carousel-track");
|
|
3263
|
+
}
|
|
3264
|
+
/** One photo's width plus the gap (with a peek a photo is narrower than the row). */
|
|
3265
|
+
carouselStep(track) {
|
|
3266
|
+
const slide = track?.firstElementChild;
|
|
3267
|
+
if (!track || !slide || slide.offsetWidth === 0) return 0;
|
|
3268
|
+
return slide.offsetWidth + (parseFloat(getComputedStyle(track).columnGap) || 0);
|
|
3269
|
+
}
|
|
3270
|
+
/** Bring the active photo into view — only when it is not already the one a swipe is on. */
|
|
3271
|
+
revealActiveSlide() {
|
|
3272
|
+
const track = this.carouselTrack();
|
|
3273
|
+
const step = this.carouselStep(track);
|
|
3274
|
+
if (!track || !step) return;
|
|
3275
|
+
const target = Math.min(this.activeImageIndex * step, track.scrollWidth - track.clientWidth);
|
|
3276
|
+
if (Math.abs(track.scrollLeft - target) > step / 2) track.scrollTo({ left: target });
|
|
3277
|
+
}
|
|
3278
|
+
stepCarousel(direction) {
|
|
3279
|
+
const last = this.allImages().length - 1;
|
|
3280
|
+
this.activeImageIndex = Math.min(last, Math.max(0, this.activeImageIndex + direction));
|
|
3281
|
+
}
|
|
2568
3282
|
thumbRow() {
|
|
2569
3283
|
return this.renderRoot.querySelector(".gallery-thumbs");
|
|
2570
3284
|
}
|
|
@@ -2593,6 +3307,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2593
3307
|
}
|
|
2594
3308
|
show() {
|
|
2595
3309
|
this.open = true;
|
|
3310
|
+
this.hideAddedToast();
|
|
2596
3311
|
}
|
|
2597
3312
|
close() {
|
|
2598
3313
|
this.open = false;
|
|
@@ -2606,9 +3321,12 @@ var VioProductDetail = class extends LitElement {
|
|
|
2606
3321
|
this.product = null;
|
|
2607
3322
|
this.sellerName = null;
|
|
2608
3323
|
this.sellerSettled = false;
|
|
3324
|
+
this.shippingFrom = null;
|
|
2609
3325
|
this.selectedOptions = {};
|
|
3326
|
+
this.selectionPrompt = "";
|
|
2610
3327
|
this.quantity = 1;
|
|
2611
3328
|
this.activeImageIndex = 0;
|
|
3329
|
+
this.descriptionOpen = false;
|
|
2612
3330
|
this.fetchError = "";
|
|
2613
3331
|
}
|
|
2614
3332
|
}, 400);
|
|
@@ -2624,13 +3342,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
2624
3342
|
this.fetchError = "";
|
|
2625
3343
|
this.sellerName = null;
|
|
2626
3344
|
this.sellerSettled = false;
|
|
3345
|
+
this.shippingFrom = null;
|
|
2627
3346
|
let products = null;
|
|
2628
3347
|
let lastError = null;
|
|
2629
3348
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
2630
3349
|
try {
|
|
2631
3350
|
const activeCurrency = getGlobalCurrency() || this.currency;
|
|
2632
3351
|
this.currency = activeCurrency;
|
|
2633
|
-
const commerce = Vio.commerceFor(this.sponsorId);
|
|
3352
|
+
const commerce = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() });
|
|
2634
3353
|
products = await commerce.channel.product.getByIds({
|
|
2635
3354
|
product_ids: [productIdNum],
|
|
2636
3355
|
currency: activeCurrency,
|
|
@@ -2653,6 +3372,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2653
3372
|
this.optMapCache = /* @__PURE__ */ new WeakMap();
|
|
2654
3373
|
this.product = products?.[0] ?? null;
|
|
2655
3374
|
if (this.product) void this.loadSeller(productIdNum, seq);
|
|
3375
|
+
if (this.product) void this.loadShippingFrom(productIdNum, seq);
|
|
2656
3376
|
if (this.product) {
|
|
2657
3377
|
try {
|
|
2658
3378
|
const price = Number(
|
|
@@ -2685,9 +3405,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
2685
3405
|
const initial = {};
|
|
2686
3406
|
for (const opt of this.product.options ?? []) {
|
|
2687
3407
|
const values = normalizeValues(opt.values);
|
|
2688
|
-
if (values.length
|
|
3408
|
+
if (values.length === 1) initial[opt.name] = values[0];
|
|
2689
3409
|
}
|
|
2690
3410
|
this.selectedOptions = initial;
|
|
3411
|
+
this.selectionPrompt = "";
|
|
2691
3412
|
this.prepareApplePaySheet();
|
|
2692
3413
|
void this.loadAvailablePaymentMethods();
|
|
2693
3414
|
}
|
|
@@ -2697,22 +3418,41 @@ var VioProductDetail = class extends LitElement {
|
|
|
2697
3418
|
async loadAvailablePaymentMethods() {
|
|
2698
3419
|
const spId = this.sponsorId || (typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.activeSlot || null, { campaignId: this.activeCampaignHint })?.primarySponsor?.id) : 0) || globalThis.__VIO_SPONSOR_ID__;
|
|
2699
3420
|
if (!spId) return;
|
|
3421
|
+
if (spId !== this.methodsSponsorId) {
|
|
3422
|
+
this.availableMethods = null;
|
|
3423
|
+
this.vippsExpress = false;
|
|
3424
|
+
}
|
|
3425
|
+
this.methodsSponsorId = spId;
|
|
3426
|
+
this.methodsLoading = true;
|
|
2700
3427
|
try {
|
|
2701
3428
|
const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
|
|
2702
3429
|
if (Array.isArray(methods)) {
|
|
2703
3430
|
this.availableMethods = methods.map((m) => m.name);
|
|
3431
|
+
const vipps = methods.find(
|
|
3432
|
+
(m) => String(m?.name ?? "").toLowerCase() === "vipps"
|
|
3433
|
+
);
|
|
3434
|
+
this.vippsExpress = !!vipps && vipps.config?.find((c) => c?.name === "express")?.value !== "false";
|
|
2704
3435
|
} else {
|
|
2705
3436
|
this.availableMethods = null;
|
|
3437
|
+
this.vippsExpress = false;
|
|
2706
3438
|
}
|
|
2707
3439
|
} catch (err) {
|
|
2708
3440
|
if (typeof console !== "undefined") {
|
|
2709
3441
|
console.warn("[VioProductDetail] Failed to load payment methods:", err);
|
|
2710
3442
|
}
|
|
2711
3443
|
this.availableMethods = null;
|
|
3444
|
+
this.vippsExpress = false;
|
|
3445
|
+
} finally {
|
|
3446
|
+
this.methodsLoading = false;
|
|
2712
3447
|
}
|
|
2713
3448
|
}
|
|
3449
|
+
/** The methods are still on their way: a placeholder stands where the buttons go. */
|
|
3450
|
+
get methodsPending() {
|
|
3451
|
+
return this.methodsLoading && this.availableMethods === null;
|
|
3452
|
+
}
|
|
2714
3453
|
/** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
|
|
2715
3454
|
methodEnabled(...names) {
|
|
3455
|
+
if (this.methodsPending) return false;
|
|
2716
3456
|
return isMethodEnabled(this.availableMethods, ...names);
|
|
2717
3457
|
}
|
|
2718
3458
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -2901,26 +3641,92 @@ var VioProductDetail = class extends LitElement {
|
|
|
2901
3641
|
const price = v?.price ?? this.product?.price;
|
|
2902
3642
|
return price?.amount_incl_taxes ?? price?.amount ?? 0;
|
|
2903
3643
|
}
|
|
3644
|
+
/**
|
|
3645
|
+
* There is something to pay for — whether the express buttons show. With
|
|
3646
|
+
* every option chosen, the chosen variant's price; before that, the
|
|
3647
|
+
* product's or any variant's: the first variant (what `unitPrice` falls
|
|
3648
|
+
* back to) at 0 must not hide Vipps for a size that costs 699.
|
|
3649
|
+
*/
|
|
3650
|
+
get hasPrice() {
|
|
3651
|
+
if (!this.missingOption()) return this.unitPrice > 0;
|
|
3652
|
+
const priceOf = (p) => Number(p?.amount_incl_taxes ?? p?.amount ?? 0);
|
|
3653
|
+
return priceOf(this.product?.price) > 0 || (this.product?.variants ?? []).some((v) => priceOf(v.price) > 0);
|
|
3654
|
+
}
|
|
2904
3655
|
get compareAt() {
|
|
2905
3656
|
const v = this.selectedVariant();
|
|
2906
3657
|
const price = v?.price ?? this.product?.price;
|
|
2907
3658
|
return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
|
|
2908
3659
|
}
|
|
3660
|
+
/** The options the shopper has to choose (those with values), in order. */
|
|
3661
|
+
choosableOptions() {
|
|
3662
|
+
return (this.product?.options ?? []).filter((o) => normalizeValues(o.values).length > 0);
|
|
3663
|
+
}
|
|
3664
|
+
/** The first option still without a value, or null when every one is chosen. */
|
|
3665
|
+
missingOption() {
|
|
3666
|
+
return this.choosableOptions().find((o) => !String(this.selectedOptions[o.name] ?? "").trim()) ?? null;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Units that can be sold. With every option chosen, the chosen variant's;
|
|
3670
|
+
* before that, the most any variant still matching the partial choice has
|
|
3671
|
+
* — so the stepper and «Utsolgt» do not speak for a variant (the first
|
|
3672
|
+
* one) the shopper never picked.
|
|
3673
|
+
*/
|
|
2909
3674
|
get availableQuantity() {
|
|
3675
|
+
if (this.missingOption() && Array.isArray(this.product?.variants) && this.product.variants.length > 0) {
|
|
3676
|
+
const sel = this.selectedOptions;
|
|
3677
|
+
const qtys = this.product.variants.filter((v2) => this.variantMatchesSelection(v2, sel)).map((v2) => this.variantSellableQty(v2));
|
|
3678
|
+
return qtys.length > 0 ? Math.max(0, ...qtys) : 0;
|
|
3679
|
+
}
|
|
2910
3680
|
const v = this.selectedVariant();
|
|
2911
3681
|
if (v) return this.variantSellableQty(v);
|
|
2912
3682
|
return this.product?.quantity ?? 0;
|
|
2913
3683
|
}
|
|
3684
|
+
/**
|
|
3685
|
+
* «Få igjen — N stk» only for the variant actually chosen: before a size is
|
|
3686
|
+
* picked the count belonged to the first variant, which said «Få igjen» of
|
|
3687
|
+
* a product with plenty of other sizes.
|
|
3688
|
+
*/
|
|
2914
3689
|
stockLabel() {
|
|
2915
3690
|
const qty = this.availableQuantity;
|
|
2916
3691
|
if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
|
|
2917
|
-
if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
3692
|
+
if (qty <= 3 && !this.missingOption()) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
|
|
2918
3693
|
return { text: "P\xE5 lager", cls: "in-stock" };
|
|
2919
3694
|
}
|
|
3695
|
+
/** «Velg størrelse» for a size, «Velg farge» for a colour… */
|
|
3696
|
+
promptFor(option) {
|
|
3697
|
+
const name = String(option.name ?? "").trim();
|
|
3698
|
+
if (!name || /^(st(ø|o)rrelse|str\.?|size)$/i.test(name)) return "Velg st\xF8rrelse";
|
|
3699
|
+
return `Velg ${name.toLowerCase()}`;
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Every buy button goes through here first: with an option not chosen yet,
|
|
3703
|
+
* nothing is added or paid — the picker says what is missing and comes
|
|
3704
|
+
* into view (it sits right above the buttons, inside the buy zone).
|
|
3705
|
+
*/
|
|
3706
|
+
requireSelection() {
|
|
3707
|
+
const missing = this.missingOption();
|
|
3708
|
+
if (!missing) {
|
|
3709
|
+
this.selectionPrompt = "";
|
|
3710
|
+
return true;
|
|
3711
|
+
}
|
|
3712
|
+
this.selectionPrompt = missing.name;
|
|
3713
|
+
void this.updateComplete.then(() => {
|
|
3714
|
+
const group = this.renderRoot.querySelector(".option-group.needs-choice");
|
|
3715
|
+
group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
|
|
3716
|
+
});
|
|
3717
|
+
return false;
|
|
3718
|
+
}
|
|
2920
3719
|
/** Pick an option value. If the exact combination doesn't exist, jump to an
|
|
2921
3720
|
* in-stock variant that carries the value and adopt its full option map
|
|
2922
3721
|
* (keeps the selection always resolvable to a real variant). */
|
|
2923
3722
|
selectOption(optionName, value) {
|
|
3723
|
+
this.applyOption(optionName, value);
|
|
3724
|
+
if (this.selectionPrompt && !this.missingOption()) this.selectionPrompt = "";
|
|
3725
|
+
else if (this.selectionPrompt) this.selectionPrompt = this.missingOption()?.name ?? "";
|
|
3726
|
+
const max = this.availableQuantity;
|
|
3727
|
+
if (max > 0 && this.quantity > max) this.quantity = max;
|
|
3728
|
+
}
|
|
3729
|
+
applyOption(optionName, value) {
|
|
2924
3730
|
const newSelection = { ...this.selectedOptions, [optionName]: value };
|
|
2925
3731
|
const exactInStock = this.product?.variants?.find(
|
|
2926
3732
|
(v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
|
|
@@ -2988,6 +3794,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
2988
3794
|
}
|
|
2989
3795
|
async addToCart() {
|
|
2990
3796
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3797
|
+
if (!this.requireSelection()) return;
|
|
2991
3798
|
this.adding = true;
|
|
2992
3799
|
try {
|
|
2993
3800
|
const variantId = this.addCurrentToCart();
|
|
@@ -3004,11 +3811,49 @@ var VioProductDetail = class extends LitElement {
|
|
|
3004
3811
|
}
|
|
3005
3812
|
})
|
|
3006
3813
|
);
|
|
3814
|
+
this.showAddedToast();
|
|
3007
3815
|
this.close();
|
|
3008
3816
|
} finally {
|
|
3009
3817
|
this.adding = false;
|
|
3010
3818
|
}
|
|
3011
3819
|
}
|
|
3820
|
+
/** The toast after an add; it goes by itself after a while. */
|
|
3821
|
+
showAddedToast() {
|
|
3822
|
+
if (this.noAddedToast) return;
|
|
3823
|
+
this.addedToast = {
|
|
3824
|
+
sponsorId: this.sponsorId,
|
|
3825
|
+
detail: { sponsorId: this.sponsorId, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
|
|
3826
|
+
};
|
|
3827
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3828
|
+
this.addedToastTimer = setTimeout(() => this.hideAddedToast(), ADDED_TOAST_MS);
|
|
3829
|
+
}
|
|
3830
|
+
hideAddedToast() {
|
|
3831
|
+
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3832
|
+
this.addedToastTimer = null;
|
|
3833
|
+
this.addedToast = null;
|
|
3834
|
+
}
|
|
3835
|
+
/** «Til kassen»: the checkout of the brand just added to — the host opens it on `vio:checkout-open`, as from the cart. */
|
|
3836
|
+
toastToCheckout() {
|
|
3837
|
+
const toast = this.addedToast;
|
|
3838
|
+
this.hideAddedToast();
|
|
3839
|
+
if (!toast) return;
|
|
3840
|
+
const cart = Vio.cart.getCart(toast.sponsorId);
|
|
3841
|
+
const total = (cart?.items ?? []).reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
3842
|
+
if (!cart || cart.items.length === 0 || !(total > 0)) return;
|
|
3843
|
+
this.dispatchEvent(new CustomEvent("vio:checkout-open", { bubbles: true, composed: true, detail: toast.detail }));
|
|
3844
|
+
}
|
|
3845
|
+
renderAddedToast() {
|
|
3846
|
+
if (!this.addedToast) return nothing;
|
|
3847
|
+
return html`
|
|
3848
|
+
<div class="added-toast" part="added-toast" role="status" aria-live="polite">
|
|
3849
|
+
<div class="added-toast-text">Lagt i handlekurven</div>
|
|
3850
|
+
<div class="added-toast-actions">
|
|
3851
|
+
<button class="added-toast-checkout" part="added-toast-checkout" @click=${this.toastToCheckout}>Til kassen</button>
|
|
3852
|
+
<button class="added-toast-continue" part="added-toast-continue" @click=${this.hideAddedToast}>Fortsett å handle</button>
|
|
3853
|
+
</div>
|
|
3854
|
+
</div>
|
|
3855
|
+
`;
|
|
3856
|
+
}
|
|
3012
3857
|
/**
|
|
3013
3858
|
* Express buy: add to cart, then open the checkout overlay with Klarna
|
|
3014
3859
|
* preselected. The host app handles `vio:checkout-open` (same event the cart
|
|
@@ -3017,6 +3862,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3017
3862
|
*/
|
|
3018
3863
|
async buyWithKlarna() {
|
|
3019
3864
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3865
|
+
if (!this.requireSelection()) return;
|
|
3020
3866
|
this.adding = true;
|
|
3021
3867
|
try {
|
|
3022
3868
|
this.addCurrentToCart();
|
|
@@ -3049,6 +3895,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3049
3895
|
*/
|
|
3050
3896
|
buyNow() {
|
|
3051
3897
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3898
|
+
if (!this.requireSelection()) return;
|
|
3052
3899
|
this.addCurrentToCart();
|
|
3053
3900
|
this.close();
|
|
3054
3901
|
this.dispatchEvent(
|
|
@@ -3071,6 +3918,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
3071
3918
|
}
|
|
3072
3919
|
buyWithVipps() {
|
|
3073
3920
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3921
|
+
if (!this.requireSelection()) return;
|
|
3922
|
+
if (!(this.unitPrice > 0)) return;
|
|
3074
3923
|
if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
|
|
3075
3924
|
this.addCurrentToCart();
|
|
3076
3925
|
this.close();
|
|
@@ -3082,6 +3931,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
3082
3931
|
this.close();
|
|
3083
3932
|
void this.startVippsExpressOrCheckout(redirect);
|
|
3084
3933
|
}
|
|
3934
|
+
/**
|
|
3935
|
+
* The Vipps button will open the app with Express (see buyWithVipps): the
|
|
3936
|
+
* brand has Express on, and this brand's cart is empty — with lines in it
|
|
3937
|
+
* the checkout opens first.
|
|
3938
|
+
*/
|
|
3939
|
+
vippsGoesStraightToApp() {
|
|
3940
|
+
return this.vippsExpress && (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) === 0;
|
|
3941
|
+
}
|
|
3085
3942
|
async startVippsExpressOrCheckout(redirect) {
|
|
3086
3943
|
let fallback = true;
|
|
3087
3944
|
try {
|
|
@@ -3118,6 +3975,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3118
3975
|
/** Buy with Stripe — opens the checkout with Stripe preselected. */
|
|
3119
3976
|
buyWithStripe() {
|
|
3120
3977
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3978
|
+
if (!this.requireSelection()) return;
|
|
3121
3979
|
this.addCurrentToCart();
|
|
3122
3980
|
this.close();
|
|
3123
3981
|
this.dispatchEvent(
|
|
@@ -3162,6 +4020,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
3162
4020
|
*/
|
|
3163
4021
|
buyWithApplePay() {
|
|
3164
4022
|
if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
|
|
4023
|
+
if (!this.requireSelection()) return;
|
|
4024
|
+
if (!(this.unitPrice > 0)) return;
|
|
3165
4025
|
const cartHasLines = (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
|
|
3166
4026
|
if (cartHasLines || this.applePayAmount !== this.unitPrice * this.quantity) {
|
|
3167
4027
|
this.addCurrentToCart();
|
|
@@ -3181,6 +4041,180 @@ var VioProductDetail = class extends LitElement {
|
|
|
3181
4041
|
void this.applePayHandle.show();
|
|
3182
4042
|
this.close();
|
|
3183
4043
|
}
|
|
4044
|
+
/**
|
|
4045
|
+
* The product's cheapest rate for the page's market — the rates the
|
|
4046
|
+
* backend cart offers for its line. A rate in another currency than the
|
|
4047
|
+
* product's price is not shown (never a price the shopper would not pay).
|
|
4048
|
+
*/
|
|
4049
|
+
async loadShippingFrom(productId, seq) {
|
|
4050
|
+
let rates = null;
|
|
4051
|
+
try {
|
|
4052
|
+
const product = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product;
|
|
4053
|
+
rates = typeof product.getShippingRates === "function" ? await product.getShippingRates(productId, { currency: this.currency }) : null;
|
|
4054
|
+
} catch {
|
|
4055
|
+
rates = null;
|
|
4056
|
+
}
|
|
4057
|
+
if (seq !== this.fetchSeq) return;
|
|
4058
|
+
const currency = String(this.product?.price?.currency_code ?? this.currency ?? "").toUpperCase();
|
|
4059
|
+
const usable = (rates ?? []).filter((r) => !r.currency || r.currency.toUpperCase() === currency);
|
|
4060
|
+
const cheapest = usable[0];
|
|
4061
|
+
this.shippingFrom = cheapest ? { price: cheapest.price, currency: cheapest.currency || currency } : null;
|
|
4062
|
+
this.shippingRates = usable.map((r) => ({
|
|
4063
|
+
name: String(r.name ?? ""),
|
|
4064
|
+
price: r.price,
|
|
4065
|
+
currency: r.currency || currency,
|
|
4066
|
+
description: String(r.description ?? "").trim(),
|
|
4067
|
+
logoUrl: String(r.logoUrl ?? "")
|
|
4068
|
+
}));
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* `gallery="carousel"`: every photo in one row that snaps one at a time —
|
|
4072
|
+
* swipe on touch, arrows with a mouse — and a dot per photo over it
|
|
4073
|
+
* instead of thumbnails, so the name, price and sizes come up higher.
|
|
4074
|
+
*/
|
|
4075
|
+
renderCarousel(images, title) {
|
|
4076
|
+
const count = images.length;
|
|
4077
|
+
const current2 = Math.min(this.activeImageIndex, Math.max(0, count - 1));
|
|
4078
|
+
return html`
|
|
4079
|
+
<div class="gallery carousel" part="gallery">
|
|
4080
|
+
<div class="carousel-track" part="image" @scroll=${this.onCarouselScroll}>
|
|
4081
|
+
${images.map(
|
|
4082
|
+
(img, idx) => html`
|
|
4083
|
+
<div class="carousel-slide" part="slide" aria-label=${`Bilde ${idx + 1} av ${count}`}>
|
|
4084
|
+
<img src=${img.url} alt=${idx === 0 ? title : ""} loading=${idx === 0 ? "eager" : "lazy"} />
|
|
4085
|
+
</div>
|
|
4086
|
+
`
|
|
4087
|
+
)}
|
|
4088
|
+
</div>
|
|
4089
|
+
${count > 1 ? html`
|
|
4090
|
+
<button
|
|
4091
|
+
class="carousel-arrow prev"
|
|
4092
|
+
part="carousel-arrow"
|
|
4093
|
+
aria-label="Forrige bilde"
|
|
4094
|
+
?disabled=${current2 === 0}
|
|
4095
|
+
@click=${() => this.stepCarousel(-1)}
|
|
4096
|
+
>‹</button>
|
|
4097
|
+
<button
|
|
4098
|
+
class="carousel-arrow next"
|
|
4099
|
+
part="carousel-arrow"
|
|
4100
|
+
aria-label="Neste bilde"
|
|
4101
|
+
?disabled=${current2 === count - 1}
|
|
4102
|
+
@click=${() => this.stepCarousel(1)}
|
|
4103
|
+
>›</button>
|
|
4104
|
+
<div class="carousel-dots" part="image-dots" aria-hidden="true">
|
|
4105
|
+
${images.map((_, idx) => html`<span class="carousel-dot ${idx === current2 ? "active" : ""}"></span>`)}
|
|
4106
|
+
</div>
|
|
4107
|
+
` : ""}
|
|
4108
|
+
</div>
|
|
4109
|
+
`;
|
|
4110
|
+
}
|
|
4111
|
+
/**
|
|
4112
|
+
* «Beskrivelse», not shut: the first lines show, fading out, and a tap on
|
|
4113
|
+
* the heading, the text or «Les mer» opens the rest (Angelo, 2026-10-28).
|
|
4114
|
+
* A description that fits in the preview has nothing to open.
|
|
4115
|
+
*/
|
|
4116
|
+
renderDescription(text) {
|
|
4117
|
+
const mode = this.resolvedDescriptionMode;
|
|
4118
|
+
if (mode === "open") {
|
|
4119
|
+
return html`<div class="description-block description-preview open" part="description">
|
|
4120
|
+
<div class="desc-head">Beskrivelse</div>
|
|
4121
|
+
<div class="description">${text}</div>
|
|
4122
|
+
</div>`;
|
|
4123
|
+
}
|
|
4124
|
+
const foldable = mode === "folded" || this.descriptionClipped;
|
|
4125
|
+
const open = this.descriptionOpen || !foldable;
|
|
4126
|
+
const toggle = () => {
|
|
4127
|
+
if (foldable) this.descriptionOpen = !this.descriptionOpen;
|
|
4128
|
+
};
|
|
4129
|
+
return html`<div class="description-block description-preview ${open ? "open" : ""} ${foldable ? "clipped" : ""} ${mode}" part="description">
|
|
4130
|
+
<button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>Beskrivelse</button>
|
|
4131
|
+
${mode === "folded" && !open ? nothing : html`<div class="description ${open ? "" : "collapsed"}" @click=${() => !this.descriptionOpen && toggle()}>${text}</div>`}
|
|
4132
|
+
${mode === "preview" && this.descriptionClipped ? html`<button class="desc-more" type="button" @click=${toggle}>${this.descriptionOpen ? "Vis mindre" : "Les mer"}</button>` : nothing}
|
|
4133
|
+
</div>`;
|
|
4134
|
+
}
|
|
4135
|
+
/** Is the preview hiding part of the text? Measured with the preview applied. */
|
|
4136
|
+
measureDescription() {
|
|
4137
|
+
const el = this.renderRoot.querySelector(".description-preview .description");
|
|
4138
|
+
if (!el || this.descriptionOpen) return;
|
|
4139
|
+
const wasCollapsed = el.classList.contains("collapsed");
|
|
4140
|
+
el.classList.add("collapsed");
|
|
4141
|
+
const clipped = el.scrollHeight > el.clientHeight + 1;
|
|
4142
|
+
if (!wasCollapsed) el.classList.remove("collapsed");
|
|
4143
|
+
if (clipped !== this.descriptionClipped) this.descriptionClipped = clipped;
|
|
4144
|
+
}
|
|
4145
|
+
renderHighlights() {
|
|
4146
|
+
const own = (Array.isArray(this.highlights) ? this.highlights : []).filter((h) => h && h.title);
|
|
4147
|
+
const items = own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? [];
|
|
4148
|
+
if (items.length === 0) return nothing;
|
|
4149
|
+
return html`<ul class="highlights" part="highlights">
|
|
4150
|
+
${items.map((h) => {
|
|
4151
|
+
const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
|
|
4152
|
+
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;
|
|
4153
|
+
return html`<li class="hl" part="highlight">
|
|
4154
|
+
${icon}
|
|
4155
|
+
<span class="hl-title">${h.title}</span>
|
|
4156
|
+
${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
|
|
4157
|
+
</li>`;
|
|
4158
|
+
})}
|
|
4159
|
+
</ul>`;
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* «Frakt & retur», folded like «Beskrivelse»: the delivery options for the
|
|
4163
|
+
* page's country with their price, and the right of withdrawal with the
|
|
4164
|
+
* brand's own terms and returns pages when it gave them.
|
|
4165
|
+
*/
|
|
4166
|
+
renderShippingReturns() {
|
|
4167
|
+
const identity = resolveSponsorIdentity(this.sponsorId);
|
|
4168
|
+
const rates = shippingRatesShown(this.shippingRates, [
|
|
4169
|
+
this.product?.title,
|
|
4170
|
+
this.product?.brand,
|
|
4171
|
+
this.sellerName,
|
|
4172
|
+
identity?.name
|
|
4173
|
+
]);
|
|
4174
|
+
const times = [...new Set(rates.map((r) => r.description).filter(Boolean))];
|
|
4175
|
+
const perRateTime = times.length > 1;
|
|
4176
|
+
const sharedTime = times.length === 1 ? times[0] : times.length === 0 && this.deliveryTime.trim() ? `Leveringstid: ${this.deliveryTime.trim()}` : "";
|
|
4177
|
+
const links = [
|
|
4178
|
+
identity?.refundPolicyUrl ? html`<a href=${identity.refundPolicyUrl} target="_blank" rel="noopener noreferrer">Angrerett og retur</a>` : null,
|
|
4179
|
+
identity?.termsUrl ? html`<a href=${identity.termsUrl} target="_blank" rel="noopener noreferrer">Kjøpsvilkår</a>` : null
|
|
4180
|
+
].filter(Boolean);
|
|
4181
|
+
return html`<details class="description-block shipping-returns" part="shipping-returns">
|
|
4182
|
+
<summary>Frakt & retur</summary>
|
|
4183
|
+
<div class="description shipping-returns-body">
|
|
4184
|
+
<div class="sr-title">Frakt</div>
|
|
4185
|
+
${rates.length ? html`<ul class="sr-rates">
|
|
4186
|
+
${rates.map((r) => {
|
|
4187
|
+
const { carrier, method } = splitRateName(r.name);
|
|
4188
|
+
const logo = r.logoUrl || carrierLogo(this.carrierLogos, carrier);
|
|
4189
|
+
return html`<li>
|
|
4190
|
+
${logo ? html`<img class="sr-logo" src=${logo} alt=${carrier} />` : lineIcon(rateIcon(r.name ?? ""), "line-icon sr-icon")}
|
|
4191
|
+
<span class="sr-rate">
|
|
4192
|
+
<span class="sr-carrier">${carrier || method || "Frakt"}</span>
|
|
4193
|
+
${carrier && method ? html`<span class="sr-method">${method}</span>` : nothing}
|
|
4194
|
+
${perRateTime && r.description ? html`<span class="sr-method">${r.description}</span>` : nothing}
|
|
4195
|
+
</span>
|
|
4196
|
+
<span class="sr-price">${r.price <= 0 ? "Gratis" : formatPrice(r.price, r.currency)}</span>
|
|
4197
|
+
</li>`;
|
|
4198
|
+
})}
|
|
4199
|
+
</ul>` : html`<p>Fraktprisen vises i kassen.</p>`}
|
|
4200
|
+
${sharedTime ? html`<p class="sr-line sr-time">${lineIcon("clock", "line-icon sr-icon")}<span>${sharedTime}</span></p>` : nothing}
|
|
4201
|
+
<div class="sr-title">Retur</div>
|
|
4202
|
+
<p class="sr-line">${lineIcon("return", "line-icon sr-icon")}<span>Du har 14 dagers angrerett fra du mottar varen.</span></p>
|
|
4203
|
+
${links.length ? html`<p class="sr-links">${links}</p>` : nothing}
|
|
4204
|
+
</div>
|
|
4205
|
+
</details>`;
|
|
4206
|
+
}
|
|
4207
|
+
/** Under the price: «Frakt fra X · 14 dagers angrerett» (or «Gratis frakt · …»). */
|
|
4208
|
+
renderDeliveryNote() {
|
|
4209
|
+
const ship = this.shippingFrom;
|
|
4210
|
+
const shipping = ship ? ship.price <= 0 ? "Gratis frakt" : `Frakt fra ${formatPrice(ship.price, ship.currency)}` : "";
|
|
4211
|
+
return html`<div class="delivery-note" part="delivery-note">
|
|
4212
|
+
${shipping ? html`<span part="delivery-shipping">${shipping}</span><span aria-hidden="true"> · </span>` : ""}<span
|
|
4213
|
+
part="delivery-withdrawal"
|
|
4214
|
+
>14 dagers angrerett</span
|
|
4215
|
+
>
|
|
4216
|
+
</div>`;
|
|
4217
|
+
}
|
|
3184
4218
|
/** The seller's Company name; the brand when there is none; nothing while asking. */
|
|
3185
4219
|
headerName() {
|
|
3186
4220
|
if (this.sellerName) return this.sellerName;
|
|
@@ -3189,7 +4223,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3189
4223
|
async loadSeller(productId, seq) {
|
|
3190
4224
|
let name = null;
|
|
3191
4225
|
try {
|
|
3192
|
-
name = await Vio.commerceFor(this.sponsorId).channel.product.getSellerCompany(productId);
|
|
4226
|
+
name = await Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product.getSellerCompany(productId);
|
|
3193
4227
|
} catch {
|
|
3194
4228
|
name = null;
|
|
3195
4229
|
}
|
|
@@ -3263,6 +4297,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3263
4297
|
|
|
3264
4298
|
${this.renderBody()}
|
|
3265
4299
|
</div>
|
|
4300
|
+
${this.renderAddedToast()}
|
|
3266
4301
|
`;
|
|
3267
4302
|
}
|
|
3268
4303
|
renderBody() {
|
|
@@ -3290,7 +4325,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3290
4325
|
const activeImage = this.activeImage;
|
|
3291
4326
|
return html`
|
|
3292
4327
|
<div class="body">
|
|
3293
|
-
|
|
4328
|
+
${this.galleryMode === "carousel" ? this.renderCarousel(images, p.title) : html`<div class="gallery" part="gallery">
|
|
3294
4329
|
<div class="gallery-main" part="image">
|
|
3295
4330
|
${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
|
|
3296
4331
|
</div>
|
|
@@ -3327,26 +4362,30 @@ var VioProductDetail = class extends LitElement {
|
|
|
3327
4362
|
>›</button>
|
|
3328
4363
|
</div>
|
|
3329
4364
|
` : ""}
|
|
3330
|
-
</div
|
|
4365
|
+
</div>`}
|
|
3331
4366
|
|
|
3332
4367
|
<div class="info" part="info">
|
|
3333
4368
|
${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
|
|
3334
|
-
<
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
4369
|
+
<div class="headline">
|
|
4370
|
+
<div class="title-row">
|
|
4371
|
+
<h2 class="name" part="title">${p.title}</h2>
|
|
4372
|
+
<div class="stock ${stock.cls}" part="stock">${stock.text}</div>
|
|
4373
|
+
</div>
|
|
4374
|
+
<div class="price-row">
|
|
4375
|
+
<span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
4376
|
+
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
3339
4377
|
compare,
|
|
3340
4378
|
p.price.currency_code
|
|
3341
4379
|
)}</span>` : ""}
|
|
4380
|
+
</div>
|
|
3342
4381
|
</div>
|
|
3343
4382
|
|
|
3344
|
-
<div class="
|
|
3345
|
-
|
|
3346
|
-
${this.renderOptionPickers(p.options)}
|
|
3347
|
-
|
|
4383
|
+
<div class="purchase">
|
|
3348
4384
|
<div class="buy" part="buy">
|
|
3349
|
-
${
|
|
4385
|
+
${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
|
|
4386
|
+
${this.renderDeliveryNote()}
|
|
4387
|
+
${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
|
|
4388
|
+
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
|
|
3350
4389
|
<vipps-mobilepay-button
|
|
3351
4390
|
class="buy-vipps-official"
|
|
3352
4391
|
part="buy-vipps"
|
|
@@ -3372,6 +4411,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3372
4411
|
<span class="vipps-badge">vipps</span>
|
|
3373
4412
|
</button>
|
|
3374
4413
|
` : ""}
|
|
4414
|
+
${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>` : ""}
|
|
3375
4415
|
|
|
3376
4416
|
<div class="qty-cta-row" part="quantity-row">
|
|
3377
4417
|
<div class="qty-stepper">
|
|
@@ -3413,7 +4453,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3413
4453
|
</button>
|
|
3414
4454
|
` : ""}
|
|
3415
4455
|
|
|
3416
|
-
${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
4456
|
+
${this.availableQuantity > 0 && this.hasPrice && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
|
|
3417
4457
|
<button
|
|
3418
4458
|
class="buy-applepay"
|
|
3419
4459
|
@click=${this.buyWithApplePay}
|
|
@@ -3459,12 +4499,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
3459
4499
|
${this.renderSellerNote()}
|
|
3460
4500
|
</div>
|
|
3461
4501
|
|
|
3462
|
-
${
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
` : ""}
|
|
4502
|
+
${this.renderHighlights()}
|
|
4503
|
+
|
|
4504
|
+
${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
|
|
4505
|
+
${this.renderShippingReturns()}
|
|
4506
|
+
</div>
|
|
3468
4507
|
</div>
|
|
3469
4508
|
</div>
|
|
3470
4509
|
`;
|
|
@@ -3474,9 +4513,16 @@ var VioProductDetail = class extends LitElement {
|
|
|
3474
4513
|
return html`${options.map((opt) => {
|
|
3475
4514
|
const values = normalizeValues(opt.values);
|
|
3476
4515
|
if (values.length === 0) return "";
|
|
4516
|
+
const chosen = String(this.selectedOptions[opt.name] ?? "").trim();
|
|
4517
|
+
const asked = this.selectionPrompt === opt.name && !chosen;
|
|
3477
4518
|
return html`
|
|
3478
|
-
<div class="option-group" part="option-group">
|
|
3479
|
-
<div class="option-label" part="option-label"
|
|
4519
|
+
<div class="option-group ${asked ? "needs-choice" : ""}" part="option-group">
|
|
4520
|
+
<div class="option-label" part="option-label">
|
|
4521
|
+
<span
|
|
4522
|
+
>${opt.name}${chosen ? html`: <span class="option-chosen" part="option-chosen">${chosen}</span>` : ""}</span
|
|
4523
|
+
>
|
|
4524
|
+
${asked ? html`<span class="option-prompt" part="option-prompt" role="alert">${this.promptFor(opt)}</span>` : ""}
|
|
4525
|
+
</div>
|
|
3480
4526
|
<div class="option-values" part="options">
|
|
3481
4527
|
${values.map((val) => {
|
|
3482
4528
|
const available = this.isValueAvailable(opt.name, val);
|
|
@@ -3541,6 +4587,24 @@ __decorateClass([
|
|
|
3541
4587
|
__decorateClass([
|
|
3542
4588
|
property({ type: String, attribute: "vio-content-title" })
|
|
3543
4589
|
], VioProductDetail.prototype, "vioContentTitle", 2);
|
|
4590
|
+
__decorateClass([
|
|
4591
|
+
property({ type: Boolean, attribute: "no-added-toast" })
|
|
4592
|
+
], VioProductDetail.prototype, "noAddedToast", 2);
|
|
4593
|
+
__decorateClass([
|
|
4594
|
+
property({ type: String })
|
|
4595
|
+
], VioProductDetail.prototype, "gallery", 2);
|
|
4596
|
+
__decorateClass([
|
|
4597
|
+
property({ type: String, attribute: "description-mode" })
|
|
4598
|
+
], VioProductDetail.prototype, "descriptionMode", 2);
|
|
4599
|
+
__decorateClass([
|
|
4600
|
+
property({ type: Array })
|
|
4601
|
+
], VioProductDetail.prototype, "highlights", 2);
|
|
4602
|
+
__decorateClass([
|
|
4603
|
+
property({ type: String, attribute: "delivery-time" })
|
|
4604
|
+
], VioProductDetail.prototype, "deliveryTime", 2);
|
|
4605
|
+
__decorateClass([
|
|
4606
|
+
property({ type: Object, attribute: "carrier-logos" })
|
|
4607
|
+
], VioProductDetail.prototype, "carrierLogos", 2);
|
|
3544
4608
|
__decorateClass([
|
|
3545
4609
|
state()
|
|
3546
4610
|
], VioProductDetail.prototype, "product", 2);
|
|
@@ -3550,6 +4614,12 @@ __decorateClass([
|
|
|
3550
4614
|
__decorateClass([
|
|
3551
4615
|
state()
|
|
3552
4616
|
], VioProductDetail.prototype, "sellerSettled", 2);
|
|
4617
|
+
__decorateClass([
|
|
4618
|
+
state()
|
|
4619
|
+
], VioProductDetail.prototype, "shippingFrom", 2);
|
|
4620
|
+
__decorateClass([
|
|
4621
|
+
state()
|
|
4622
|
+
], VioProductDetail.prototype, "shippingRates", 2);
|
|
3553
4623
|
__decorateClass([
|
|
3554
4624
|
state()
|
|
3555
4625
|
], VioProductDetail.prototype, "isLoading", 2);
|
|
@@ -3559,6 +4629,9 @@ __decorateClass([
|
|
|
3559
4629
|
__decorateClass([
|
|
3560
4630
|
state()
|
|
3561
4631
|
], VioProductDetail.prototype, "selectedOptions", 2);
|
|
4632
|
+
__decorateClass([
|
|
4633
|
+
state()
|
|
4634
|
+
], VioProductDetail.prototype, "selectionPrompt", 2);
|
|
3562
4635
|
__decorateClass([
|
|
3563
4636
|
state()
|
|
3564
4637
|
], VioProductDetail.prototype, "quantity", 2);
|
|
@@ -3574,18 +4647,39 @@ __decorateClass([
|
|
|
3574
4647
|
__decorateClass([
|
|
3575
4648
|
state()
|
|
3576
4649
|
], VioProductDetail.prototype, "thumbsAtEnd", 2);
|
|
4650
|
+
__decorateClass([
|
|
4651
|
+
state()
|
|
4652
|
+
], VioProductDetail.prototype, "descriptionOpen", 2);
|
|
4653
|
+
__decorateClass([
|
|
4654
|
+
state()
|
|
4655
|
+
], VioProductDetail.prototype, "descriptionClipped", 2);
|
|
4656
|
+
__decorateClass([
|
|
4657
|
+
state()
|
|
4658
|
+
], VioProductDetail.prototype, "galleryMode", 2);
|
|
4659
|
+
__decorateClass([
|
|
4660
|
+
state()
|
|
4661
|
+
], VioProductDetail.prototype, "resolvedDescriptionMode", 2);
|
|
3577
4662
|
__decorateClass([
|
|
3578
4663
|
state()
|
|
3579
4664
|
], VioProductDetail.prototype, "logoSquare", 2);
|
|
3580
4665
|
__decorateClass([
|
|
3581
4666
|
state()
|
|
3582
4667
|
], VioProductDetail.prototype, "adding", 2);
|
|
4668
|
+
__decorateClass([
|
|
4669
|
+
state()
|
|
4670
|
+
], VioProductDetail.prototype, "addedToast", 2);
|
|
3583
4671
|
__decorateClass([
|
|
3584
4672
|
state()
|
|
3585
4673
|
], VioProductDetail.prototype, "applePayOk", 2);
|
|
3586
4674
|
__decorateClass([
|
|
3587
4675
|
state()
|
|
3588
4676
|
], VioProductDetail.prototype, "availableMethods", 2);
|
|
4677
|
+
__decorateClass([
|
|
4678
|
+
state()
|
|
4679
|
+
], VioProductDetail.prototype, "methodsLoading", 2);
|
|
4680
|
+
__decorateClass([
|
|
4681
|
+
state()
|
|
4682
|
+
], VioProductDetail.prototype, "vippsExpress", 2);
|
|
3589
4683
|
__decorateClass([
|
|
3590
4684
|
state()
|
|
3591
4685
|
], VioProductDetail.prototype, "vippsOfficial", 2);
|
|
@@ -3598,9 +4692,38 @@ function normalizeValues(raw) {
|
|
|
3598
4692
|
}
|
|
3599
4693
|
return [];
|
|
3600
4694
|
}
|
|
4695
|
+
function shippingRatesShown(rates, repeats = []) {
|
|
4696
|
+
return rates.map((r) => ({ ...r, description: shippingDescriptionOf(r.description, [r.name, ...repeats]) }));
|
|
4697
|
+
}
|
|
4698
|
+
function splitRateName(name) {
|
|
4699
|
+
const text = String(name ?? "").trim();
|
|
4700
|
+
const m = text.match(/^(.+?)\s+[-–—]\s+(.+)$/);
|
|
4701
|
+
if (!m?.[1] || !m[2]) return { carrier: "", method: text };
|
|
4702
|
+
const method = m[2].trim();
|
|
4703
|
+
return { carrier: m[1].trim(), method: method.charAt(0).toUpperCase() + method.slice(1) };
|
|
4704
|
+
}
|
|
3601
4705
|
var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
3602
4706
|
'<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>'
|
|
3603
4707
|
);
|
|
4708
|
+
function ratePrice(rate, currency) {
|
|
4709
|
+
const price = rate?.price;
|
|
4710
|
+
if (!price) return null;
|
|
4711
|
+
const amount = typeof price.amount_incl_taxes === "number" ? price.amount_incl_taxes : price.amount;
|
|
4712
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) return null;
|
|
4713
|
+
if (price.currency_code && currency && price.currency_code.toUpperCase() !== currency.toUpperCase()) return null;
|
|
4714
|
+
return amount;
|
|
4715
|
+
}
|
|
4716
|
+
function cartShipping(items, currency) {
|
|
4717
|
+
for (const item of items) {
|
|
4718
|
+
const chosen = ratePrice(item.shipping, currency);
|
|
4719
|
+
if (chosen !== null) return chosen;
|
|
4720
|
+
}
|
|
4721
|
+
for (const item of items) {
|
|
4722
|
+
const offered = (item.availableShippings ?? []).map((r) => ratePrice(r, currency)).filter((p) => p !== null);
|
|
4723
|
+
if (offered.length > 0) return Math.min(...offered);
|
|
4724
|
+
}
|
|
4725
|
+
return null;
|
|
4726
|
+
}
|
|
3604
4727
|
var VioCart = class extends LitElement {
|
|
3605
4728
|
constructor() {
|
|
3606
4729
|
super(...arguments);
|
|
@@ -3612,6 +4735,7 @@ var VioCart = class extends LitElement {
|
|
|
3612
4735
|
this.itemCount = 0;
|
|
3613
4736
|
this.applePayAvailable = false;
|
|
3614
4737
|
this.availableMethods = null;
|
|
4738
|
+
this.methodsLoading = false;
|
|
3615
4739
|
this.vippsOfficial = false;
|
|
3616
4740
|
/** Whether the method lookup has been attempted — see boundOnCartChange. */
|
|
3617
4741
|
this.paymentMethodsAttempted = false;
|
|
@@ -3634,6 +4758,7 @@ var VioCart = class extends LitElement {
|
|
|
3634
4758
|
sheet: () => this.renderRoot.querySelector(".drawer"),
|
|
3635
4759
|
backdrop: () => this.renderRoot.querySelector(".backdrop"),
|
|
3636
4760
|
dragZones: ".handle, .header",
|
|
4761
|
+
scroller: () => this.renderRoot.querySelector(".body"),
|
|
3637
4762
|
close: () => this.close()
|
|
3638
4763
|
});
|
|
3639
4764
|
this.boundOnCartChange = (e) => {
|
|
@@ -3838,6 +4963,14 @@ var VioCart = class extends LitElement {
|
|
|
3838
4963
|
cursor: pointer;
|
|
3839
4964
|
}
|
|
3840
4965
|
.checkout-btn:hover { opacity: 0.9; }
|
|
4966
|
+
.checkout-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4967
|
+
/* Where the express buttons go while the brand's methods load. */
|
|
4968
|
+
.methods-pending {
|
|
4969
|
+
height: 48px;
|
|
4970
|
+
margin-bottom: 10px;
|
|
4971
|
+
border-radius: var(--vio-radius-lg, 8px);
|
|
4972
|
+
background: var(--vio-color-surface-muted, #f1f1f1);
|
|
4973
|
+
}
|
|
3841
4974
|
.footer {
|
|
3842
4975
|
padding: var(--vio-space-lg, 24px);
|
|
3843
4976
|
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
@@ -3850,6 +4983,14 @@ var VioCart = class extends LitElement {
|
|
|
3850
4983
|
font-size: 14px;
|
|
3851
4984
|
}
|
|
3852
4985
|
.subtotal-label { color: var(--vio-color-text-secondary, #666); }
|
|
4986
|
+
.shipping-row,
|
|
4987
|
+
.total-row {
|
|
4988
|
+
display: flex;
|
|
4989
|
+
justify-content: space-between;
|
|
4990
|
+
margin: calc(var(--vio-space-md, 16px) * -0.5) 0 var(--vio-space-md, 16px);
|
|
4991
|
+
font-size: 14px;
|
|
4992
|
+
}
|
|
4993
|
+
.total-row { font-weight: 700; font-size: 16px; margin-top: 0; }
|
|
3853
4994
|
.subtotal-value { font-weight: 700; font-size: 16px; }
|
|
3854
4995
|
|
|
3855
4996
|
/* Apple Pay express — pay the whole cart directly. */
|
|
@@ -4045,6 +5186,7 @@ var VioCart = class extends LitElement {
|
|
|
4045
5186
|
this.loadingPaymentMethods = false;
|
|
4046
5187
|
return;
|
|
4047
5188
|
}
|
|
5189
|
+
this.methodsLoading = true;
|
|
4048
5190
|
try {
|
|
4049
5191
|
const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
|
|
4050
5192
|
if (Array.isArray(methods)) {
|
|
@@ -4059,10 +5201,20 @@ var VioCart = class extends LitElement {
|
|
|
4059
5201
|
this.availableMethods = null;
|
|
4060
5202
|
} finally {
|
|
4061
5203
|
this.loadingPaymentMethods = false;
|
|
5204
|
+
this.methodsLoading = false;
|
|
4062
5205
|
}
|
|
4063
5206
|
}
|
|
4064
|
-
/**
|
|
5207
|
+
/** The methods are on their way and none is known yet. */
|
|
5208
|
+
get methodsPending() {
|
|
5209
|
+
return this.methodsLoading && this.availableMethods === null;
|
|
5210
|
+
}
|
|
5211
|
+
/**
|
|
5212
|
+
* Backend names arrive as e.g. "Apple Pay" — compare letters only. While
|
|
5213
|
+
* the list loads, no method's button is drawn: «unknown = all» painted
|
|
5214
|
+
* Stripe and Klarna for a Vipps-only brand until it came.
|
|
5215
|
+
*/
|
|
4065
5216
|
methodEnabled(...names) {
|
|
5217
|
+
if (this.methodsPending) return false;
|
|
4066
5218
|
return isMethodEnabled(this.availableMethods, ...names);
|
|
4067
5219
|
}
|
|
4068
5220
|
updated(changed) {
|
|
@@ -4105,7 +5257,7 @@ var VioCart = class extends LitElement {
|
|
|
4105
5257
|
/** Apple Pay express — pay the whole cart, then show the confirmation. */
|
|
4106
5258
|
onApplePay() {
|
|
4107
5259
|
const handle = this.applePayHandle;
|
|
4108
|
-
if (!handle || !handle.available || this.
|
|
5260
|
+
if (!handle || !handle.available || !this.payable) return;
|
|
4109
5261
|
const snapshot = this.buildConfirmation();
|
|
4110
5262
|
const sponsorIds = [...this.carts.keys()];
|
|
4111
5263
|
const sponsor = sponsorIds.length === 1 ? sponsorDetail(sponsorIds[0]) : {};
|
|
@@ -4149,7 +5301,7 @@ var VioCart = class extends LitElement {
|
|
|
4149
5301
|
* (betalingsmetode). The host's checkout-open handler closes this cart and
|
|
4150
5302
|
* opens <vio-checkout> in express mode with Klarna preselected. */
|
|
4151
5303
|
onKlarnaExpress() {
|
|
4152
|
-
if (this.
|
|
5304
|
+
if (!this.payable) return;
|
|
4153
5305
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4154
5306
|
if (firstSponsorId === void 0) return;
|
|
4155
5307
|
this.dispatchEvent(
|
|
@@ -4170,7 +5322,7 @@ var VioCart = class extends LitElement {
|
|
|
4170
5322
|
* method present the others were unreachable from here.
|
|
4171
5323
|
*/
|
|
4172
5324
|
onCheckout() {
|
|
4173
|
-
if (this.
|
|
5325
|
+
if (!this.payable) return;
|
|
4174
5326
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4175
5327
|
if (firstSponsorId === void 0) return;
|
|
4176
5328
|
this.dispatchEvent(
|
|
@@ -4182,51 +5334,31 @@ var VioCart = class extends LitElement {
|
|
|
4182
5334
|
);
|
|
4183
5335
|
}
|
|
4184
5336
|
/**
|
|
4185
|
-
* Vipps
|
|
4186
|
-
*
|
|
4187
|
-
*
|
|
4188
|
-
*
|
|
5337
|
+
* Vipps from the cart: the checkout opens with Vipps preselected — the
|
|
5338
|
+
* standard path, where our form collects name, address, mobile and the
|
|
5339
|
+
* delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
|
|
5340
|
+
* It no longer starts Express from here: Express is the product detail's
|
|
5341
|
+
* «Kjøp nå med Vipps» only.
|
|
4189
5342
|
*/
|
|
4190
5343
|
onVipps() {
|
|
4191
|
-
if (this.
|
|
5344
|
+
if (!this.payable) return;
|
|
4192
5345
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4193
5346
|
if (firstSponsorId === void 0) return;
|
|
4194
|
-
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: firstSponsorId, source: "cart" });
|
|
4195
|
-
void this.startVippsExpressOrCheckout(firstSponsorId, redirect);
|
|
4196
|
-
}
|
|
4197
|
-
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
4198
|
-
async loadVippsButton() {
|
|
4199
|
-
this.vippsOfficial = await ensureVippsButton();
|
|
4200
|
-
}
|
|
4201
|
-
async startVippsExpressOrCheckout(sponsorId, redirect) {
|
|
4202
|
-
let fallback = true;
|
|
4203
|
-
try {
|
|
4204
|
-
await Vio.cart.waitForCartMutations(sponsorId);
|
|
4205
|
-
if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
|
|
4206
|
-
redirect.abort("express-off");
|
|
4207
|
-
} else {
|
|
4208
|
-
const res = await Vio.checkout.startVippsExpress(sponsorId);
|
|
4209
|
-
fallback = !res?.payment_url;
|
|
4210
|
-
if (fallback) redirect.abort("not-started");
|
|
4211
|
-
}
|
|
4212
|
-
} catch (err) {
|
|
4213
|
-
if (typeof console !== "undefined") {
|
|
4214
|
-
console.warn("[VioCart] Vipps Express not started:", err);
|
|
4215
|
-
}
|
|
4216
|
-
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
4217
|
-
}
|
|
4218
|
-
if (!fallback) return;
|
|
4219
5347
|
this.dispatchEvent(
|
|
4220
5348
|
new CustomEvent("vio:checkout-open", {
|
|
4221
5349
|
bubbles: true,
|
|
4222
5350
|
composed: true,
|
|
4223
|
-
detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(
|
|
5351
|
+
detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
|
|
4224
5352
|
})
|
|
4225
5353
|
);
|
|
4226
5354
|
}
|
|
5355
|
+
/** Vipps' own button when its script answers; our orange one otherwise. */
|
|
5356
|
+
async loadVippsButton() {
|
|
5357
|
+
this.vippsOfficial = await ensureVippsButton();
|
|
5358
|
+
}
|
|
4227
5359
|
/** Stripe — opens the checkout with Stripe preselected. */
|
|
4228
5360
|
onStripe() {
|
|
4229
|
-
if (this.
|
|
5361
|
+
if (!this.payable) return;
|
|
4230
5362
|
const firstSponsorId = [...this.carts.keys()][0];
|
|
4231
5363
|
if (firstSponsorId === void 0) return;
|
|
4232
5364
|
this.dispatchEvent(
|
|
@@ -4241,6 +5373,10 @@ var VioCart = class extends LitElement {
|
|
|
4241
5373
|
sponsorLabel(cart) {
|
|
4242
5374
|
return resolveSponsorIdentity(cart.sponsorId)?.name || cart.sponsorName || "";
|
|
4243
5375
|
}
|
|
5376
|
+
/** Something to pay for: lines, and a total above 0. No button opens a checkout or a payment otherwise. */
|
|
5377
|
+
get payable() {
|
|
5378
|
+
return this.itemCount > 0 && this.totalAcrossSponsors > 0;
|
|
5379
|
+
}
|
|
4244
5380
|
get totalAcrossSponsors() {
|
|
4245
5381
|
let sum = 0;
|
|
4246
5382
|
for (const cart of this.carts.values()) {
|
|
@@ -4248,6 +5384,24 @@ var VioCart = class extends LitElement {
|
|
|
4248
5384
|
}
|
|
4249
5385
|
return sum;
|
|
4250
5386
|
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Shipping across the carts, from what the server cart says of its lines:
|
|
5389
|
+
* the rate chosen for them, else the cheapest one offered (the one the
|
|
5390
|
+
* checkout preselects). null when some cart has no known rate — the footer
|
|
5391
|
+
* says it is worked out in the checkout then.
|
|
5392
|
+
*/
|
|
5393
|
+
get shippingAcrossSponsors() {
|
|
5394
|
+
let sum = 0;
|
|
5395
|
+
let any = false;
|
|
5396
|
+
for (const cart of this.carts.values()) {
|
|
5397
|
+
if (cart.items.length === 0) continue;
|
|
5398
|
+
const rate = cartShipping(cart.items, cart.currency);
|
|
5399
|
+
if (rate === null) return null;
|
|
5400
|
+
sum += rate;
|
|
5401
|
+
any = true;
|
|
5402
|
+
}
|
|
5403
|
+
return any ? sum : null;
|
|
5404
|
+
}
|
|
4251
5405
|
get primaryCurrency() {
|
|
4252
5406
|
for (const cart of this.carts.values()) return cart.currency;
|
|
4253
5407
|
return "NOK";
|
|
@@ -4339,13 +5493,16 @@ var VioCart = class extends LitElement {
|
|
|
4339
5493
|
${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
|
|
4340
5494
|
</span>
|
|
4341
5495
|
</div>
|
|
5496
|
+
${this.renderShippingRows()}
|
|
4342
5497
|
<button
|
|
4343
5498
|
class="checkout-btn"
|
|
4344
5499
|
@click=${this.onCheckout}
|
|
5500
|
+
?disabled=${!this.payable}
|
|
4345
5501
|
aria-label="Til kassen"
|
|
4346
5502
|
>
|
|
4347
5503
|
Til kassen
|
|
4348
5504
|
</button>
|
|
5505
|
+
${this.methodsPending ? html`<div class="methods-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
|
|
4349
5506
|
${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
|
|
4350
5507
|
<button
|
|
4351
5508
|
class="applepay-btn"
|
|
@@ -4406,6 +5563,20 @@ var VioCart = class extends LitElement {
|
|
|
4406
5563
|
</aside>
|
|
4407
5564
|
`;
|
|
4408
5565
|
}
|
|
5566
|
+
/** «Frakt» (the chosen or cheapest rate) and «Totalt» with it; «Beregnes i kassen» when unknown. */
|
|
5567
|
+
renderShippingRows() {
|
|
5568
|
+
const shipping = this.shippingAcrossSponsors;
|
|
5569
|
+
return html`
|
|
5570
|
+
<div class="shipping-row" part="cart-shipping">
|
|
5571
|
+
<span class="subtotal-label">Frakt</span>
|
|
5572
|
+
<span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : formatPrice(shipping, this.primaryCurrency)}</span>
|
|
5573
|
+
</div>
|
|
5574
|
+
${shipping !== null ? html`<div class="total-row" part="cart-total">
|
|
5575
|
+
<span>Totalt</span>
|
|
5576
|
+
<span>${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
|
|
5577
|
+
</div>` : ""}
|
|
5578
|
+
`;
|
|
5579
|
+
}
|
|
4409
5580
|
renderConfirmation() {
|
|
4410
5581
|
const o = this.confirmedOrder;
|
|
4411
5582
|
if (!o) return html``;
|
|
@@ -4457,6 +5628,9 @@ __decorateClass([
|
|
|
4457
5628
|
__decorateClass([
|
|
4458
5629
|
state()
|
|
4459
5630
|
], VioCart.prototype, "availableMethods", 2);
|
|
5631
|
+
__decorateClass([
|
|
5632
|
+
state()
|
|
5633
|
+
], VioCart.prototype, "methodsLoading", 2);
|
|
4460
5634
|
__decorateClass([
|
|
4461
5635
|
state()
|
|
4462
5636
|
], VioCart.prototype, "vippsOfficial", 2);
|
|
@@ -4498,13 +5672,14 @@ function errorText(err) {
|
|
|
4498
5672
|
var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
4499
5673
|
'<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>'
|
|
4500
5674
|
);
|
|
5675
|
+
var EMPTY_CART_MESSAGE2 = "Handlekurven er tom.";
|
|
5676
|
+
var SHIPPING_NOT_SAVED_MESSAGE = "Kunne ikke lagre fraktmetoden. Pr\xF8v igjen.";
|
|
4501
5677
|
var _VioCheckout = class _VioCheckout extends LitElement {
|
|
4502
5678
|
constructor() {
|
|
4503
5679
|
super(...arguments);
|
|
4504
5680
|
this.open = false;
|
|
4505
5681
|
this.heading = "Kasse";
|
|
4506
5682
|
this.shippingLabel = "Beregnes ved kasse";
|
|
4507
|
-
this.vippsShipsInApp = false;
|
|
4508
5683
|
this.express = false;
|
|
4509
5684
|
this.checkoutState = null;
|
|
4510
5685
|
this.items = [];
|
|
@@ -4604,6 +5779,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4604
5779
|
firstName: "",
|
|
4605
5780
|
lastName: "",
|
|
4606
5781
|
email: "",
|
|
5782
|
+
phone: "",
|
|
4607
5783
|
address: "",
|
|
4608
5784
|
postalCode: "",
|
|
4609
5785
|
city: ""
|
|
@@ -4964,6 +6140,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
4964
6140
|
transition: border-color 0.15s;
|
|
4965
6141
|
}
|
|
4966
6142
|
.field input:focus { border-bottom-color: var(--vio-color-text, #0a0a0a); }
|
|
6143
|
+
.field input[aria-invalid='true'] { border-bottom-color: var(--vio-color-accent, #c14a3b); }
|
|
6144
|
+
.field-error { margin-top: 4px; font-size: 12px; color: var(--vio-color-accent, #c14a3b); }
|
|
4967
6145
|
|
|
4968
6146
|
.payment-grid {
|
|
4969
6147
|
display: grid;
|
|
@@ -5652,7 +6830,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5652
6830
|
this.cleanReturnQueryParams(["checkout_id", "payment_processor"]);
|
|
5653
6831
|
return;
|
|
5654
6832
|
}
|
|
5655
|
-
if (vioMethod === "klarna" &&
|
|
6833
|
+
if (vioMethod === "klarna" && storageGet("session", "vio.klarna.pending.v1")) {
|
|
5656
6834
|
this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
|
|
5657
6835
|
return;
|
|
5658
6836
|
}
|
|
@@ -5919,12 +7097,34 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5919
7097
|
(i) => Array.isArray(i.availableShippings) && i.availableShippings.length > 0
|
|
5920
7098
|
);
|
|
5921
7099
|
}
|
|
5922
|
-
/**
|
|
7100
|
+
/** The shipping country the mobile's calling code defaults to (no +prefix typed). */
|
|
7101
|
+
get phoneCountry() {
|
|
7102
|
+
return String(this.form.country || getGlobalCountryCode());
|
|
7103
|
+
}
|
|
7104
|
+
/**
|
|
7105
|
+
* The mobile is required only for the standard Vipps payment — shopcart
|
|
7106
|
+
* does not ask Vipps for the profile there. Every other method takes it
|
|
7107
|
+
* when given and never waits for it (a host that pre-fills the form with
|
|
7108
|
+
* `setAddress()` and no phone keeps working).
|
|
7109
|
+
*/
|
|
7110
|
+
get phoneRequired() {
|
|
7111
|
+
return this.checkoutState?.paymentMethod === "vipps";
|
|
7112
|
+
}
|
|
7113
|
+
/** A number typed that is not usable (nothing said while the field is empty). */
|
|
7114
|
+
get phoneError() {
|
|
7115
|
+
const raw = String(this.form.phone ?? "").trim();
|
|
7116
|
+
return raw.length > 0 && !isValidPhone(raw, this.phoneCountry);
|
|
7117
|
+
}
|
|
7118
|
+
/** The mobile as the chosen method needs it: valid when required, else anything. */
|
|
7119
|
+
get phoneReady() {
|
|
7120
|
+
return !this.phoneRequired || isValidPhone(this.form.phone, this.phoneCountry);
|
|
7121
|
+
}
|
|
7122
|
+
/** Whether all mandatory Leveringsadresse fields are filled (the mobile only for Vipps). */
|
|
5923
7123
|
get isAddressValid() {
|
|
5924
7124
|
const f = this.form;
|
|
5925
7125
|
if (!f) return false;
|
|
5926
7126
|
return Boolean(
|
|
5927
|
-
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
|
|
7127
|
+
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
|
|
5928
7128
|
);
|
|
5929
7129
|
}
|
|
5930
7130
|
/** Whether a valid shipping option is selected (when shippings exist). */
|
|
@@ -5947,7 +7147,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
5947
7147
|
if (Array.isArray(methods)) {
|
|
5948
7148
|
this.availableMethods = methods.map((m) => m.name);
|
|
5949
7149
|
this.stripeMode = await Vio.checkout.getStripeMode(spId);
|
|
5950
|
-
this.vippsShipsInApp = this.availableMethods.some((m) => String(m).toLowerCase() === "vipps") ? await Vio.checkout.getVippsExpressEnabled(spId) : false;
|
|
5951
7150
|
} else {
|
|
5952
7151
|
this.availableMethods = null;
|
|
5953
7152
|
}
|
|
@@ -6165,21 +7364,44 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6165
7364
|
});
|
|
6166
7365
|
}
|
|
6167
7366
|
/**
|
|
6168
|
-
* Vipps from the checkout:
|
|
6169
|
-
*
|
|
6170
|
-
*
|
|
6171
|
-
*
|
|
6172
|
-
*
|
|
6173
|
-
*
|
|
7367
|
+
* Vipps from the checkout: the STANDARD payment, always (Mote & Livsstil,
|
|
7368
|
+
* 2026-10-28). The shopper filled our form — name, address, mobile — and
|
|
7369
|
+
* chose the delivery with its price here; the payment carries that address
|
|
7370
|
+
* and the rate on the cart, and Vipps only approves and pays. Express (the
|
|
7371
|
+
* app filling in name and address) is the product detail's «Kjøp nå med
|
|
7372
|
+
* Vipps» alone, even when the brand has Express on.
|
|
6174
7373
|
*/
|
|
6175
7374
|
async startVippsFromCheckout() {
|
|
6176
7375
|
const sponsorId = this.checkoutState?.sponsorId;
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
7376
|
+
await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: false });
|
|
7377
|
+
}
|
|
7378
|
+
/**
|
|
7379
|
+
* The chosen delivery on the cart before a hosted payment is minted (its
|
|
7380
|
+
* total depends on it). False — and the payment must not start — when it
|
|
7381
|
+
* could not be saved: the selected rate has no supplier to save it under,
|
|
7382
|
+
* or the backend refused. A cart with no rates at all has nothing to save.
|
|
7383
|
+
*/
|
|
7384
|
+
async persistSelectedShipping(method) {
|
|
7385
|
+
if (this.availableShippingsList.length === 0) return true;
|
|
7386
|
+
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
7387
|
+
if (!selectedOpt?.supplierId) {
|
|
7388
|
+
if (typeof console !== "undefined") {
|
|
7389
|
+
console.warn(`[VioCheckout] no supplier for the selected shipping \u2014 ${method} not started`);
|
|
7390
|
+
}
|
|
7391
|
+
return false;
|
|
7392
|
+
}
|
|
7393
|
+
try {
|
|
7394
|
+
const updated = await Vio.checkout.updateShippingsBySupplier(
|
|
7395
|
+
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
7396
|
+
this.checkoutState?.sponsorId
|
|
7397
|
+
);
|
|
7398
|
+
return updated != null;
|
|
7399
|
+
} catch (err) {
|
|
7400
|
+
if (typeof console !== "undefined") {
|
|
7401
|
+
console.warn(`[VioCheckout] updateShippingsBySupplier on ${method} pay failed:`, err);
|
|
7402
|
+
}
|
|
7403
|
+
return false;
|
|
6181
7404
|
}
|
|
6182
|
-
await Vio.checkout.startVippsPayment(sponsorId, this.form);
|
|
6183
7405
|
}
|
|
6184
7406
|
async onApplePay() {
|
|
6185
7407
|
if (this.applePayInProgress) return;
|
|
@@ -6198,6 +7420,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6198
7420
|
if (this.shippingBlocked) return;
|
|
6199
7421
|
this.paymentError = null;
|
|
6200
7422
|
this.paymentNotice = null;
|
|
7423
|
+
if (!this.cartPayable) {
|
|
7424
|
+
this.paymentError = EMPTY_CART_MESSAGE2;
|
|
7425
|
+
return;
|
|
7426
|
+
}
|
|
6201
7427
|
if (method === "apple-pay") {
|
|
6202
7428
|
void this.onApplePay();
|
|
6203
7429
|
return;
|
|
@@ -6218,8 +7444,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6218
7444
|
Vio.checkout.selectPaymentMethod("nexi");
|
|
6219
7445
|
return;
|
|
6220
7446
|
}
|
|
6221
|
-
|
|
6222
|
-
const
|
|
7447
|
+
{
|
|
7448
|
+
const phoneMissing = method === "vipps" && !isValidPhone(this.form.phone, this.phoneCountry);
|
|
7449
|
+
const missing = !this.isAddressValid || phoneMissing ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
|
|
6223
7450
|
if (missing) {
|
|
6224
7451
|
if (this.checkoutState?.paymentMethod !== method) {
|
|
6225
7452
|
Vio.checkout.selectPaymentMethod(method);
|
|
@@ -6243,20 +7470,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6243
7470
|
this.stripeRequested = true;
|
|
6244
7471
|
return;
|
|
6245
7472
|
}
|
|
6246
|
-
const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
|
|
6247
|
-
if (selectedOpt?.supplierId) {
|
|
6248
|
-
try {
|
|
6249
|
-
await Vio.checkout.updateShippingsBySupplier(
|
|
6250
|
-
[{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
|
|
6251
|
-
this.checkoutState?.sponsorId
|
|
6252
|
-
);
|
|
6253
|
-
} catch (err) {
|
|
6254
|
-
if (typeof console !== "undefined") {
|
|
6255
|
-
console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
|
|
6256
|
-
}
|
|
6257
|
-
}
|
|
6258
|
-
}
|
|
6259
7473
|
this.stripeLoading = true;
|
|
7474
|
+
if (!await this.persistSelectedShipping("stripe")) {
|
|
7475
|
+
this.stripeLoading = false;
|
|
7476
|
+
this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
|
|
7477
|
+
return;
|
|
7478
|
+
}
|
|
6260
7479
|
void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
|
|
6261
7480
|
if (typeof console !== "undefined") {
|
|
6262
7481
|
console.warn("[VioCheckout] Stripe payment could not start:", err);
|
|
@@ -6268,6 +7487,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6268
7487
|
}
|
|
6269
7488
|
if (method === "vipps") {
|
|
6270
7489
|
this.vippsLoading = true;
|
|
7490
|
+
if (!await this.persistSelectedShipping("vipps")) {
|
|
7491
|
+
this.vippsLoading = false;
|
|
7492
|
+
this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
|
|
7493
|
+
return;
|
|
7494
|
+
}
|
|
6271
7495
|
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.checkoutState?.sponsorId, source: "checkout" });
|
|
6272
7496
|
void this.startVippsFromCheckout().catch((err) => {
|
|
6273
7497
|
if (typeof console !== "undefined") {
|
|
@@ -6289,7 +7513,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6289
7513
|
const s = this.checkoutState;
|
|
6290
7514
|
if (!s || !s.paymentMethod) return;
|
|
6291
7515
|
if (this.shippingBlocked) return;
|
|
6292
|
-
if (
|
|
7516
|
+
if (!this.cartPayable) {
|
|
7517
|
+
this.paymentError = EMPTY_CART_MESSAGE2;
|
|
7518
|
+
return;
|
|
7519
|
+
}
|
|
7520
|
+
if (!this.isAddressValid) {
|
|
6293
7521
|
this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
|
|
6294
7522
|
return;
|
|
6295
7523
|
}
|
|
@@ -6514,6 +7742,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6514
7742
|
return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
|
|
6515
7743
|
isFormFirstWidgetMethod(method ?? "");
|
|
6516
7744
|
}
|
|
7745
|
+
/**
|
|
7746
|
+
* A total above 0 — every pay button needs it. The state's subtotal follows
|
|
7747
|
+
* the cart (CheckoutManager.onCartChange): a cart emptied or paid in
|
|
7748
|
+
* another tab drops it to 0 here.
|
|
7749
|
+
*/
|
|
7750
|
+
get cartPayable() {
|
|
7751
|
+
return (this.checkoutState?.subtotal ?? 0) > 0;
|
|
7752
|
+
}
|
|
6517
7753
|
orderTotal() {
|
|
6518
7754
|
const s = this.checkoutState;
|
|
6519
7755
|
if (!s) return "";
|
|
@@ -6523,7 +7759,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6523
7759
|
* express always has one; the normal flow has one once the backend
|
|
6524
7760
|
* shippings loaded (the choice IS persisted on the cart and charged). */
|
|
6525
7761
|
shippingMajor() {
|
|
6526
|
-
if (this.vippsChoosesShipping()) return 0;
|
|
6527
7762
|
if (this.checkoutState?.paymentMethod === "qliro" && typeof this.qliroShipping?.price === "number") {
|
|
6528
7763
|
return this.qliroShipping.price;
|
|
6529
7764
|
}
|
|
@@ -6538,10 +7773,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
6538
7773
|
if (!o) return 0;
|
|
6539
7774
|
return o.priceMajor ?? o.price / 100;
|
|
6540
7775
|
}
|
|
6541
|
-
/** Vipps is the method and it runs as Express: the delivery is chosen in Vipps. */
|
|
6542
|
-
vippsChoosesShipping() {
|
|
6543
|
-
return this.checkoutState?.paymentMethod === "vipps" && this.vippsShipsInApp;
|
|
6544
|
-
}
|
|
6545
7776
|
/** The rate the summary names — Qliro's own pick while Qliro is the method. */
|
|
6546
7777
|
summaryShippingName() {
|
|
6547
7778
|
if (this.checkoutState?.paymentMethod === "qliro" && this.qliroShipping) {
|
|
@@ -7664,7 +8895,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7664
8895
|
renderCheckoutBody() {
|
|
7665
8896
|
const method = this.checkoutState?.paymentMethod;
|
|
7666
8897
|
const showCompleteCta = !!method && !this.isExpressMethod(method);
|
|
7667
|
-
const isVipps = method === "vipps";
|
|
7668
8898
|
const needsOurForm = !!method && !collectsOwnAddress(method);
|
|
7669
8899
|
return html`
|
|
7670
8900
|
${needsOurForm ? html`
|
|
@@ -7703,6 +8933,22 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7703
8933
|
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
7704
8934
|
/>
|
|
7705
8935
|
</div>
|
|
8936
|
+
<div class="field">
|
|
8937
|
+
<label for="vio-checkout-phone">${this.phoneRequired ? "Mobilnummer *" : "Mobilnummer (valgfritt)"}</label>
|
|
8938
|
+
<input
|
|
8939
|
+
id="vio-checkout-phone"
|
|
8940
|
+
class="phone-input"
|
|
8941
|
+
type="tel"
|
|
8942
|
+
?required=${this.phoneRequired}
|
|
8943
|
+
inputmode="tel"
|
|
8944
|
+
autocomplete="tel"
|
|
8945
|
+
placeholder=${this.phoneCountry.toUpperCase() === "NO" ? "8 siffer" : "+46 70 123 45 67"}
|
|
8946
|
+
aria-invalid=${this.phoneError ? "true" : "false"}
|
|
8947
|
+
.value=${this.form.phone ?? ""}
|
|
8948
|
+
@input=${(e) => this.onFieldChange("phone", e.target.value)}
|
|
8949
|
+
/>
|
|
8950
|
+
${this.phoneError ? html`<div class="field-error" role="alert">Skriv et gyldig mobilnummer (med landskode, f.eks. +46, hvis det ikke er norsk)</div>` : ""}
|
|
8951
|
+
</div>
|
|
7706
8952
|
<div class="field">
|
|
7707
8953
|
<label>Adresse *</label>
|
|
7708
8954
|
<input
|
|
@@ -7767,22 +9013,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7767
9013
|
</section>
|
|
7768
9014
|
` : ""}
|
|
7769
9015
|
|
|
7770
|
-
${isVipps ? html`
|
|
7771
|
-
<section class="section">
|
|
7772
|
-
<div class="section-label">Kontakt</div>
|
|
7773
|
-
<div class="field">
|
|
7774
|
-
<label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
|
|
7775
|
-
<input
|
|
7776
|
-
type="email"
|
|
7777
|
-
required
|
|
7778
|
-
autocomplete="email"
|
|
7779
|
-
.value=${this.form.email}
|
|
7780
|
-
@input=${(e) => this.onFieldChange("email", e.target.value)}
|
|
7781
|
-
/>
|
|
7782
|
-
</div>
|
|
7783
|
-
</section>
|
|
7784
|
-
` : ""}
|
|
7785
|
-
|
|
7786
9016
|
<section class="section">
|
|
7787
9017
|
<div class="section-label">
|
|
7788
9018
|
${needsOurForm ? "Steg 2" : "Betaling"}
|
|
@@ -7822,7 +9052,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7822
9052
|
<button
|
|
7823
9053
|
class="payment-btn primary complete-cta"
|
|
7824
9054
|
@click=${() => void this.onPay("stripe")}
|
|
7825
|
-
?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9055
|
+
?disabled=${this.stripeLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7826
9056
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7827
9057
|
>
|
|
7828
9058
|
${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
|
|
@@ -7832,7 +9062,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7832
9062
|
<button
|
|
7833
9063
|
class="payment-btn primary complete-cta"
|
|
7834
9064
|
@click=${() => void this.onPay("vipps")}
|
|
7835
|
-
?disabled=${this.vippsLoading}
|
|
9065
|
+
?disabled=${this.vippsLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9066
|
+
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7836
9067
|
>
|
|
7837
9068
|
${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
|
|
7838
9069
|
</button>
|
|
@@ -7841,7 +9072,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7841
9072
|
<button
|
|
7842
9073
|
class="payment-btn primary complete-cta"
|
|
7843
9074
|
@click=${() => void this.onPay("apple-pay")}
|
|
7844
|
-
?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
|
|
9075
|
+
?disabled=${this.applePayInProgress || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7845
9076
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7846
9077
|
>
|
|
7847
9078
|
${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
|
|
@@ -7851,7 +9082,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7851
9082
|
<button
|
|
7852
9083
|
class="payment-btn primary complete-cta"
|
|
7853
9084
|
@click=${this.onCompleteOrder}
|
|
7854
|
-
?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
|
|
9085
|
+
?disabled=${!this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
|
|
7855
9086
|
title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
|
|
7856
9087
|
>
|
|
7857
9088
|
Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
|
|
@@ -7964,7 +9195,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
|
|
|
7964
9195
|
</div>
|
|
7965
9196
|
<div class="order-row">
|
|
7966
9197
|
<span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
|
|
7967
|
-
<span>${this.
|
|
9198
|
+
<span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
|
|
7968
9199
|
</div>
|
|
7969
9200
|
<div class="order-row total">
|
|
7970
9201
|
<span>Totalt</span><span>${this.payTotalLabel()}</span>
|
|
@@ -7985,9 +9216,6 @@ __decorateClass([
|
|
|
7985
9216
|
__decorateClass([
|
|
7986
9217
|
property({ type: String, attribute: "shipping-label" })
|
|
7987
9218
|
], _VioCheckout.prototype, "shippingLabel", 2);
|
|
7988
|
-
__decorateClass([
|
|
7989
|
-
state()
|
|
7990
|
-
], _VioCheckout.prototype, "vippsShipsInApp", 2);
|
|
7991
9219
|
__decorateClass([
|
|
7992
9220
|
property({ type: Boolean, reflect: true })
|
|
7993
9221
|
], _VioCheckout.prototype, "express", 2);
|
|
@@ -8163,5 +9391,5 @@ function registerVioElements() {
|
|
|
8163
9391
|
}
|
|
8164
9392
|
|
|
8165
9393
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
8166
|
-
//# sourceMappingURL=chunk-
|
|
8167
|
-
//# sourceMappingURL=chunk-
|
|
9394
|
+
//# sourceMappingURL=chunk-T5B74YBQ.js.map
|
|
9395
|
+
//# sourceMappingURL=chunk-T5B74YBQ.js.map
|