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