@vio-live/web-sdk 0.33.0 → 0.36.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 +90 -0
- package/README.md +139 -13
- package/dist/chunk-3YFJFDSN.js +3 -0
- package/dist/{chunk-QGJUBVH3.js.map → chunk-3YFJFDSN.js.map} +1 -1
- package/dist/{chunk-ZORYG4W3.js → chunk-6PQFXUOW.js} +294 -40
- package/dist/chunk-6PQFXUOW.js.map +1 -0
- package/dist/{chunk-T5B74YBQ.js → chunk-DEREQRCM.js} +423 -109
- package/dist/chunk-DEREQRCM.js.map +1 -0
- package/dist/core/index.d.ts +65 -3
- package/dist/core/index.js +2 -2
- package/dist/detail-layout-BI556w8p.d.ts +76 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.js +5 -4
- package/dist/react/index.js.map +1 -1
- package/dist/{tokens-B7GVA_qB.d.ts → tokens-BGzSC3vK.d.ts} +133 -13
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-QGJUBVH3.js +0 -3
- package/dist/chunk-T5B74YBQ.js.map +0 -1
- package/dist/chunk-ZORYG4W3.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-
|
|
1
|
+
import { Vio, __decorateClass, primaryImageUrl, displayPrice, resolveDetailLayout, toCampaignId, getGlobalCurrency, copyAppliedLayout, onGlobalDetailLayoutChange, layoutSignature, layoutOriginOf, ensureVippsButton, formatPrice, currentGlobalDescriptionLabel, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-6PQFXUOW.js';
|
|
2
2
|
import { LitElement, css, html, nothing } from 'lit';
|
|
3
3
|
import { property, state, query } from 'lit/decorators.js';
|
|
4
4
|
|
|
@@ -171,6 +171,7 @@ var THEME_KEY_TO_CSS_VAR = {
|
|
|
171
171
|
detailDescriptionPreviewLines: "--vio-detail-description-preview-lines",
|
|
172
172
|
detailHighlightsDisplay: "--vio-detail-highlights-display",
|
|
173
173
|
detailShippingReturnsDisplay: "--vio-detail-shipping-returns-display",
|
|
174
|
+
cartHighlightsDisplay: "--vio-cart-highlights-display",
|
|
174
175
|
detailThumbSize: "--vio-detail-thumb-size",
|
|
175
176
|
detailThumbGap: "--vio-detail-thumb-gap",
|
|
176
177
|
detailThumbRadius: "--vio-detail-thumb-radius",
|
|
@@ -201,7 +202,8 @@ var THEME_KEY_TO_CSS_VAR = {
|
|
|
201
202
|
confirmTitleSize: "--vio-confirm-title-size",
|
|
202
203
|
confirmCheckBg: "--vio-confirm-check-bg",
|
|
203
204
|
redirectBg: "--vio-redirect-bg",
|
|
204
|
-
redirectSpinner: "--vio-redirect-spinner"
|
|
205
|
+
redirectSpinner: "--vio-redirect-spinner",
|
|
206
|
+
vippsCartPayment: "--vio-vipps-cart-payment"
|
|
205
207
|
};
|
|
206
208
|
function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
|
|
207
209
|
if (!target) return;
|
|
@@ -1556,6 +1558,21 @@ function returnCancelled(detail) {
|
|
|
1556
1558
|
emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
|
|
1557
1559
|
}
|
|
1558
1560
|
|
|
1561
|
+
// src/ui/vipps-cart-payment.ts
|
|
1562
|
+
var VIPPS_CART_PAYMENT_VAR = "--vio-vipps-cart-payment";
|
|
1563
|
+
function resolveVippsCartPayment(el, own) {
|
|
1564
|
+
let fromTheme = "";
|
|
1565
|
+
if (!own) {
|
|
1566
|
+
try {
|
|
1567
|
+
fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(el).getPropertyValue(VIPPS_CART_PAYMENT_VAR) : "";
|
|
1568
|
+
} catch {
|
|
1569
|
+
fromTheme = "";
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const value = String(own || fromTheme || "").trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
1573
|
+
return value === "checkout" ? "checkout" : "express";
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1559
1576
|
// src/core/checkout/method-taxonomy.ts
|
|
1560
1577
|
function normalizeMethodName(name) {
|
|
1561
1578
|
return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -2035,12 +2052,6 @@ function unlockPageScroll() {
|
|
|
2035
2052
|
window.scrollTo(0, scrollY);
|
|
2036
2053
|
root.style.scrollBehavior = behavior;
|
|
2037
2054
|
}
|
|
2038
|
-
|
|
2039
|
-
// src/ui/components/vio-product-detail.ts
|
|
2040
|
-
var ADDED_TOAST_MS = 8e3;
|
|
2041
|
-
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
2042
|
-
'<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
|
|
2043
|
-
);
|
|
2044
2055
|
var HIGHLIGHT_ICONS = {
|
|
2045
2056
|
plane: "M21.5 2.5 2.5 10l7 3m12-10.5-12 10.5m12-10.5L17 21l-7.5-8m0 0V20l3-4M5 17l-2.5 2.5M7.5 18.5 6 20",
|
|
2046
2057
|
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",
|
|
@@ -2055,6 +2066,72 @@ var HIGHLIGHT_ICONS = {
|
|
|
2055
2066
|
pin: "M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
|
|
2056
2067
|
clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13.5V12l3 2"
|
|
2057
2068
|
};
|
|
2069
|
+
function renderHighlights(items) {
|
|
2070
|
+
const list = (Array.isArray(items) ? items : []).filter((h) => h && h.title);
|
|
2071
|
+
if (list.length === 0) return nothing;
|
|
2072
|
+
return html`<ul class="highlights" part="highlights">
|
|
2073
|
+
${list.map((h) => {
|
|
2074
|
+
const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
|
|
2075
|
+
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;
|
|
2076
|
+
return html`<li class="hl" part="highlight">
|
|
2077
|
+
${icon}
|
|
2078
|
+
<span class="hl-title">${h.title}</span>
|
|
2079
|
+
${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
|
|
2080
|
+
</li>`;
|
|
2081
|
+
})}
|
|
2082
|
+
</ul>`;
|
|
2083
|
+
}
|
|
2084
|
+
function highlightsStyles(scope) {
|
|
2085
|
+
const display = scope === "detail" ? css`var(--vio-detail-highlights-display, grid)` : css`var(--vio-cart-highlights-display, grid)`;
|
|
2086
|
+
const padding = scope === "detail" ? css`var(--vio-detail-highlights-padding, 16px 0 2px)` : css`var(--vio-cart-highlights-padding, 14px 0 2px)`;
|
|
2087
|
+
return css`
|
|
2088
|
+
/* Like a shop's trust strip: a thin icon, the promise in regular weight,
|
|
2089
|
+
a smaller line under it — nothing bold. */
|
|
2090
|
+
.highlights {
|
|
2091
|
+
list-style: none;
|
|
2092
|
+
margin: 0;
|
|
2093
|
+
padding: ${padding};
|
|
2094
|
+
display: ${display};
|
|
2095
|
+
grid-auto-flow: column;
|
|
2096
|
+
grid-auto-columns: 1fr;
|
|
2097
|
+
gap: 10px;
|
|
2098
|
+
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2099
|
+
}
|
|
2100
|
+
.hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
|
|
2101
|
+
.hl-icon {
|
|
2102
|
+
width: var(--vio-detail-highlight-icon-size, 22px);
|
|
2103
|
+
height: var(--vio-detail-highlight-icon-size, 22px);
|
|
2104
|
+
margin-bottom: 8px;
|
|
2105
|
+
fill: none;
|
|
2106
|
+
stroke: currentColor;
|
|
2107
|
+
stroke-width: 1.2;
|
|
2108
|
+
stroke-linecap: round;
|
|
2109
|
+
stroke-linejoin: round;
|
|
2110
|
+
object-fit: contain;
|
|
2111
|
+
}
|
|
2112
|
+
.hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
|
|
2113
|
+
.hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
|
|
2114
|
+
`;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/ui/components/vio-product-detail.ts
|
|
2118
|
+
var layoutAttribute = {
|
|
2119
|
+
fromAttribute(value) {
|
|
2120
|
+
if (value === null || value.trim() === "") return null;
|
|
2121
|
+
try {
|
|
2122
|
+
return JSON.parse(value);
|
|
2123
|
+
} catch {
|
|
2124
|
+
return value;
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
toAttribute(value) {
|
|
2128
|
+
return value === null || value === void 0 ? null : JSON.stringify(value);
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
var ADDED_TOAST_MS = 8e3;
|
|
2132
|
+
var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
2133
|
+
'<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>'
|
|
2134
|
+
);
|
|
2058
2135
|
function rateIcon(method) {
|
|
2059
2136
|
const m = method.toLowerCase();
|
|
2060
2137
|
if (/postkasse|mailbox|brevkasse/.test(m)) return "mailbox";
|
|
@@ -2087,11 +2164,19 @@ var VioProductDetail = class extends LitElement {
|
|
|
2087
2164
|
this.vioContentId = "";
|
|
2088
2165
|
this.vioContentTitle = "";
|
|
2089
2166
|
this.noAddedToast = false;
|
|
2167
|
+
this.vippsCartPayment = "";
|
|
2090
2168
|
this.gallery = "";
|
|
2091
2169
|
this.descriptionMode = "";
|
|
2170
|
+
this.descriptionLabel = "";
|
|
2092
2171
|
this.highlights = [];
|
|
2093
2172
|
this.deliveryTime = "";
|
|
2094
2173
|
this.carrierLogos = {};
|
|
2174
|
+
this.layout = null;
|
|
2175
|
+
/** The layout the detail renders with now (resolved before each render). */
|
|
2176
|
+
this.layoutNow = resolveDetailLayout(null);
|
|
2177
|
+
/** The product and layout `vio:detail-layout-applied` was last said for. */
|
|
2178
|
+
this.appliedFor = null;
|
|
2179
|
+
this.unsubscribeLayout = null;
|
|
2095
2180
|
this.slotRegistration = new SlotRegistration(this, "detail");
|
|
2096
2181
|
/** What the click that opened the detail carried ('' when nothing), and for which product. */
|
|
2097
2182
|
this.eventSlot = "";
|
|
@@ -2180,6 +2265,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
2180
2265
|
this.show();
|
|
2181
2266
|
void this.fetchProduct();
|
|
2182
2267
|
};
|
|
2268
|
+
/** The cart changed (another tab, the drawer): the line under Vipps counts it again. */
|
|
2269
|
+
this.boundCartChange = () => {
|
|
2270
|
+
if (this.open) this.requestUpdate();
|
|
2271
|
+
};
|
|
2183
2272
|
/** The photo in view is the active one (dots, Apple Pay sheet, events). */
|
|
2184
2273
|
this.onCarouselScroll = () => {
|
|
2185
2274
|
const track = this.carouselTrack();
|
|
@@ -2198,6 +2287,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
2198
2287
|
if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
|
|
2199
2288
|
};
|
|
2200
2289
|
}
|
|
2290
|
+
/**
|
|
2291
|
+
* The layout this detail applies (0.34): `{ id, revision, experimentId?,
|
|
2292
|
+
* variantId?, sections, adjusted, source }` — `source` is `element` (its own
|
|
2293
|
+
* `layout`), `global` (`Vio.configureDetail`) or `default`. A copy.
|
|
2294
|
+
*/
|
|
2295
|
+
get appliedLayout() {
|
|
2296
|
+
return copyAppliedLayout(resolveDetailLayout(this.layout));
|
|
2297
|
+
}
|
|
2201
2298
|
/** The card's context applies to the product it opened — not to one the host opened by hand later. */
|
|
2202
2299
|
get fromCard() {
|
|
2203
2300
|
return this.eventProductId !== "" && this.eventProductId === String(this.productId);
|
|
@@ -2259,7 +2356,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
2259
2356
|
};
|
|
2260
2357
|
}
|
|
2261
2358
|
static {
|
|
2262
|
-
this.styles =
|
|
2359
|
+
this.styles = [
|
|
2360
|
+
css`
|
|
2263
2361
|
:host { display: contents; }
|
|
2264
2362
|
|
|
2265
2363
|
.backdrop {
|
|
@@ -2632,35 +2730,6 @@ var VioProductDetail = class extends LitElement {
|
|
|
2632
2730
|
margin: 0;
|
|
2633
2731
|
letter-spacing: -0.01em;
|
|
2634
2732
|
}
|
|
2635
|
-
/* The brand's promises: one row, an icon over a title and a line of text,
|
|
2636
|
-
centred like a shop's own trust strip. */
|
|
2637
|
-
.highlights {
|
|
2638
|
-
list-style: none;
|
|
2639
|
-
/* --vio-detail-highlights-display: none hides the strip (theme detailHighlightsDisplay). */
|
|
2640
|
-
margin: 0;
|
|
2641
|
-
padding: var(--vio-detail-highlights-padding, 16px 0 2px);
|
|
2642
|
-
display: var(--vio-detail-highlights-display, grid);
|
|
2643
|
-
grid-auto-flow: column;
|
|
2644
|
-
grid-auto-columns: 1fr;
|
|
2645
|
-
gap: 10px;
|
|
2646
|
-
border-top: 1px solid var(--vio-color-border, #e5e5e5);
|
|
2647
|
-
}
|
|
2648
|
-
/* Like a shop's trust strip: a thin icon, the promise in regular weight,
|
|
2649
|
-
a smaller line under it — nothing bold. */
|
|
2650
|
-
.hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
|
|
2651
|
-
.hl-icon {
|
|
2652
|
-
width: var(--vio-detail-highlight-icon-size, 22px);
|
|
2653
|
-
height: var(--vio-detail-highlight-icon-size, 22px);
|
|
2654
|
-
margin-bottom: 8px;
|
|
2655
|
-
fill: none;
|
|
2656
|
-
stroke: currentColor;
|
|
2657
|
-
stroke-width: 1.2;
|
|
2658
|
-
stroke-linecap: round;
|
|
2659
|
-
stroke-linejoin: round;
|
|
2660
|
-
object-fit: contain;
|
|
2661
|
-
}
|
|
2662
|
-
.hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
|
|
2663
|
-
.hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
|
|
2664
2733
|
/* «Frakt & retur»: same fold as «Beskrivelse», with a small list of rates.
|
|
2665
2734
|
--vio-detail-shipping-returns-display: none hides it (theme detailShippingReturnsDisplay). */
|
|
2666
2735
|
.shipping-returns { display: var(--vio-detail-shipping-returns-display, block); }
|
|
@@ -2714,6 +2783,10 @@ var VioProductDetail = class extends LitElement {
|
|
|
2714
2783
|
}
|
|
2715
2784
|
/* The sizes follow the price closely: they are the next thing to do. */
|
|
2716
2785
|
.headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
|
|
2786
|
+
/* A layout with the sizes apart from the buy zone (0.34): they follow the
|
|
2787
|
+
price closely instead, and the buy zone keeps the normal gap. */
|
|
2788
|
+
.headline + .buy-options.options-apart { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
|
|
2789
|
+
.headline + .purchase.options-apart { margin-top: 0; }
|
|
2717
2790
|
.price {
|
|
2718
2791
|
font-size: var(--vio-detail-price-size, 24px);
|
|
2719
2792
|
font-weight: var(--vio-detail-price-weight, 700);
|
|
@@ -3201,10 +3274,15 @@ var VioProductDetail = class extends LitElement {
|
|
|
3201
3274
|
color: var(--vio-color-accent, #c14a3b);
|
|
3202
3275
|
font-size: 14px;
|
|
3203
3276
|
}
|
|
3204
|
-
|
|
3277
|
+
`,
|
|
3278
|
+
highlightsStyles("detail")
|
|
3279
|
+
];
|
|
3205
3280
|
}
|
|
3206
3281
|
connectedCallback() {
|
|
3207
3282
|
super.connectedCallback();
|
|
3283
|
+
Vio.cart.addEventListener("change", this.boundCartChange);
|
|
3284
|
+
this.unsubscribeLayout?.();
|
|
3285
|
+
this.unsubscribeLayout = onGlobalDetailLayoutChange(() => this.requestUpdate());
|
|
3208
3286
|
this.slotRegistration.connect(this.slotAttributes());
|
|
3209
3287
|
void this.loadVippsButton();
|
|
3210
3288
|
document.addEventListener("vio:product-click", this.boundProductClick);
|
|
@@ -3213,8 +3291,11 @@ var VioProductDetail = class extends LitElement {
|
|
|
3213
3291
|
if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
|
|
3214
3292
|
this.addedToastTimer = null;
|
|
3215
3293
|
document.removeEventListener("vio:product-click", this.boundProductClick);
|
|
3294
|
+
Vio.cart.removeEventListener("change", this.boundCartChange);
|
|
3216
3295
|
this.slotRegistration.clear();
|
|
3217
3296
|
this.bottomSheet.disconnect();
|
|
3297
|
+
this.unsubscribeLayout?.();
|
|
3298
|
+
this.unsubscribeLayout = null;
|
|
3218
3299
|
super.disconnectedCallback();
|
|
3219
3300
|
}
|
|
3220
3301
|
updated(changed) {
|
|
@@ -3238,17 +3319,38 @@ var VioProductDetail = class extends LitElement {
|
|
|
3238
3319
|
else this.revealActiveSlide();
|
|
3239
3320
|
}
|
|
3240
3321
|
this.measureLogo();
|
|
3322
|
+
this.announceLayout();
|
|
3241
3323
|
if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
|
|
3242
3324
|
this.slotRegistration.sync(this.slotAttributes());
|
|
3243
3325
|
}
|
|
3244
3326
|
}
|
|
3245
3327
|
willUpdate(changed) {
|
|
3328
|
+
this.layoutNow = resolveDetailLayout(this.layout);
|
|
3246
3329
|
if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
|
|
3247
3330
|
if (changed.has("descriptionMode") || changed.has("open") && this.open) {
|
|
3248
3331
|
const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
|
|
3249
3332
|
this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
|
|
3250
3333
|
}
|
|
3251
3334
|
}
|
|
3335
|
+
/**
|
|
3336
|
+
* `vio:detail-layout-applied` (bubbles, composed), with the applied layout,
|
|
3337
|
+
* each time a product renders with a layout it has not been shown with yet:
|
|
3338
|
+
* every open of a product, and every change of layout while it is open.
|
|
3339
|
+
*/
|
|
3340
|
+
announceLayout() {
|
|
3341
|
+
const product = this.product;
|
|
3342
|
+
if (!product || this.fetchError) return;
|
|
3343
|
+
const signature = layoutSignature(this.layoutNow);
|
|
3344
|
+
if (this.appliedFor && this.appliedFor.product === product && this.appliedFor.signature === signature) return;
|
|
3345
|
+
this.appliedFor = { product, signature };
|
|
3346
|
+
this.dispatchEvent(
|
|
3347
|
+
new CustomEvent("vio:detail-layout-applied", {
|
|
3348
|
+
bubbles: true,
|
|
3349
|
+
composed: true,
|
|
3350
|
+
detail: { ...copyAppliedLayout(this.layoutNow), productId: product.id, sponsorId: this.sponsorId }
|
|
3351
|
+
})
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3252
3354
|
/** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
|
|
3253
3355
|
themeChoice(own, cssVar) {
|
|
3254
3356
|
const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
|
|
@@ -3378,10 +3480,17 @@ var VioProductDetail = class extends LitElement {
|
|
|
3378
3480
|
const price = Number(
|
|
3379
3481
|
this.product.price?.amount_incl_taxes ?? this.product.price?.amount
|
|
3380
3482
|
);
|
|
3483
|
+
const shown = layoutOriginOf(resolveDetailLayout(this.layout));
|
|
3381
3484
|
Vio.analytics.track("view_item", {
|
|
3382
3485
|
context: {
|
|
3383
3486
|
sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
|
|
3384
|
-
...this.attribution()
|
|
3487
|
+
...this.attribution(),
|
|
3488
|
+
...shown.layoutId ? {
|
|
3489
|
+
layoutId: shown.layoutId,
|
|
3490
|
+
layoutRevision: shown.layoutRevision ?? "",
|
|
3491
|
+
experimentId: shown.experimentId,
|
|
3492
|
+
variantId: shown.variantId
|
|
3493
|
+
} : {}
|
|
3385
3494
|
},
|
|
3386
3495
|
commerce: {
|
|
3387
3496
|
items: [
|
|
@@ -3712,7 +3821,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
3712
3821
|
this.selectionPrompt = missing.name;
|
|
3713
3822
|
void this.updateComplete.then(() => {
|
|
3714
3823
|
const group = this.renderRoot.querySelector(".option-group.needs-choice");
|
|
3715
|
-
group?.
|
|
3824
|
+
const apart = !!group?.closest(".options-apart");
|
|
3825
|
+
group?.scrollIntoView?.({ behavior: "smooth", block: apart ? "center" : "nearest" });
|
|
3716
3826
|
});
|
|
3717
3827
|
return false;
|
|
3718
3828
|
}
|
|
@@ -3782,13 +3892,14 @@ var VioProductDetail = class extends LitElement {
|
|
|
3782
3892
|
const variant = this.selectedVariant();
|
|
3783
3893
|
const sponsorName = resolveSponsorIdentity(this.sponsorId)?.name || void 0;
|
|
3784
3894
|
const { campaignId, slot, contentId, componentTemplateId, contentUrl, contentTitle } = this.attribution();
|
|
3895
|
+
const layout = layoutOriginOf(this.layoutNow);
|
|
3785
3896
|
Vio.cart.addProduct({
|
|
3786
3897
|
product: this.product,
|
|
3787
3898
|
sponsorId: this.sponsorId,
|
|
3788
3899
|
sponsorName,
|
|
3789
3900
|
quantity: this.quantity,
|
|
3790
3901
|
variantId: variant?.id,
|
|
3791
|
-
origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle }
|
|
3902
|
+
origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle, ...layout }
|
|
3792
3903
|
});
|
|
3793
3904
|
return variant?.id;
|
|
3794
3905
|
}
|
|
@@ -3807,7 +3918,9 @@ var VioProductDetail = class extends LitElement {
|
|
|
3807
3918
|
sponsorId: this.sponsorId,
|
|
3808
3919
|
quantity: this.quantity,
|
|
3809
3920
|
variantId,
|
|
3810
|
-
...this.attribution()
|
|
3921
|
+
...this.attribution(),
|
|
3922
|
+
// The detail layout of the add (0.34) — `variantId` above is the product's.
|
|
3923
|
+
...this.layoutNow.source !== "default" ? { layout: layoutOriginOf(this.layoutNow) } : {}
|
|
3811
3924
|
}
|
|
3812
3925
|
})
|
|
3813
3926
|
);
|
|
@@ -3920,7 +4033,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
3920
4033
|
if (!this.product || this.availableQuantity <= 0 || this.adding) return;
|
|
3921
4034
|
if (!this.requireSelection()) return;
|
|
3922
4035
|
if (!(this.unitPrice > 0)) return;
|
|
3923
|
-
if (
|
|
4036
|
+
if (this.cartHasLines() && this.vippsCartMode() === "checkout") {
|
|
3924
4037
|
this.addCurrentToCart();
|
|
3925
4038
|
this.close();
|
|
3926
4039
|
this.openCheckoutWithVipps();
|
|
@@ -3932,12 +4045,46 @@ var VioProductDetail = class extends LitElement {
|
|
|
3932
4045
|
void this.startVippsExpressOrCheckout(redirect);
|
|
3933
4046
|
}
|
|
3934
4047
|
/**
|
|
3935
|
-
* The Vipps button will open the app with Express
|
|
3936
|
-
* brand has Express on, and this brand's cart is
|
|
3937
|
-
*
|
|
4048
|
+
* The Vipps button will open the app with Express for this product alone
|
|
4049
|
+
* (see buyWithVipps): the brand has Express on, and this brand's cart is
|
|
4050
|
+
* empty.
|
|
3938
4051
|
*/
|
|
3939
4052
|
vippsGoesStraightToApp() {
|
|
3940
|
-
return this.vippsExpress &&
|
|
4053
|
+
return this.vippsExpress && !this.cartHasLines();
|
|
4054
|
+
}
|
|
4055
|
+
/** This brand's cart already has lines. */
|
|
4056
|
+
cartHasLines() {
|
|
4057
|
+
return (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
|
|
4058
|
+
}
|
|
4059
|
+
/** `vipps-cart-payment`, else the theme's, else `express`. */
|
|
4060
|
+
vippsCartMode() {
|
|
4061
|
+
return resolveVippsCartPayment(this, this.vippsCartPayment);
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* What the Vipps button will pay when the brand's cart already has lines
|
|
4065
|
+
* (`express` mode): every unit in the cart plus this product at the chosen
|
|
4066
|
+
* quantity, and their total, as the cart sums it (shipping is chosen in
|
|
4067
|
+
* Vipps). null with an empty cart, or in `checkout` mode.
|
|
4068
|
+
*/
|
|
4069
|
+
wholeCartToPay() {
|
|
4070
|
+
if (this.vippsCartMode() !== "express") return null;
|
|
4071
|
+
const cart = Vio.cart.getCart(this.sponsorId);
|
|
4072
|
+
const items = cart?.items ?? [];
|
|
4073
|
+
if (items.length === 0) return null;
|
|
4074
|
+
const units = items.reduce((n, i) => n + (Number(i.quantity) || 0), 0) + this.quantity;
|
|
4075
|
+
const total = items.reduce((sum, i) => sum + (Number(i.unitPrice) || 0) * (Number(i.quantity) || 0), 0) + this.unitPrice * this.quantity;
|
|
4076
|
+
return { units, total, currency: cart?.currency || this.currency };
|
|
4077
|
+
}
|
|
4078
|
+
/** The line under the Vipps button: what is paid (a cart with lines) or what happens in the app (Express, empty cart). */
|
|
4079
|
+
renderVippsNote() {
|
|
4080
|
+
const whole = this.wholeCartToPay();
|
|
4081
|
+
if (whole) {
|
|
4082
|
+
return html`<p class="vipps-note vipps-note-cart" part="vipps-note">Betaler hele handlekurven · ${whole.units} varer · ${formatPrice(whole.total, whole.currency)}</p>`;
|
|
4083
|
+
}
|
|
4084
|
+
if (this.vippsGoesStraightToApp()) {
|
|
4085
|
+
return html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>`;
|
|
4086
|
+
}
|
|
4087
|
+
return "";
|
|
3941
4088
|
}
|
|
3942
4089
|
async startVippsExpressOrCheckout(redirect) {
|
|
3943
4090
|
let fallback = true;
|
|
@@ -4012,8 +4159,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
4012
4159
|
* after an await, so it cannot wait for the server's cart), while the
|
|
4013
4160
|
* charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
|
|
4014
4161
|
* what will be charged: an empty cart, and a sheet prepared for this
|
|
4015
|
-
* product × quantity. Otherwise — lines already in the cart
|
|
4016
|
-
*
|
|
4162
|
+
* product × quantity. Otherwise — lines already in the cart, or a quantity
|
|
4163
|
+
* whose sheet is still being prepared —
|
|
4017
4164
|
* the product is added and the checkout opens with Apple Pay preselected:
|
|
4018
4165
|
* every line and the total on screen, and its own Apple Pay button pays
|
|
4019
4166
|
* that total.
|
|
@@ -4113,11 +4260,16 @@ var VioProductDetail = class extends LitElement {
|
|
|
4113
4260
|
* the heading, the text or «Les mer» opens the rest (Angelo, 2026-10-28).
|
|
4114
4261
|
* A description that fits in the preview has nothing to open.
|
|
4115
4262
|
*/
|
|
4263
|
+
/** The description fold's heading: this element's, else the page's, else «Beskrivelse». */
|
|
4264
|
+
descriptionHeading() {
|
|
4265
|
+
const own = this.descriptionLabel.trim();
|
|
4266
|
+
return (own && own.length <= 40 ? own : "") || currentGlobalDescriptionLabel() || "Beskrivelse";
|
|
4267
|
+
}
|
|
4116
4268
|
renderDescription(text) {
|
|
4117
4269
|
const mode = this.resolvedDescriptionMode;
|
|
4118
4270
|
if (mode === "open") {
|
|
4119
4271
|
return html`<div class="description-block description-preview open" part="description">
|
|
4120
|
-
<div class="desc-head"
|
|
4272
|
+
<div class="desc-head">${this.descriptionHeading()}</div>
|
|
4121
4273
|
<div class="description">${text}</div>
|
|
4122
4274
|
</div>`;
|
|
4123
4275
|
}
|
|
@@ -4127,7 +4279,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
4127
4279
|
if (foldable) this.descriptionOpen = !this.descriptionOpen;
|
|
4128
4280
|
};
|
|
4129
4281
|
return html`<div class="description-block description-preview ${open ? "open" : ""} ${foldable ? "clipped" : ""} ${mode}" part="description">
|
|
4130
|
-
<button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}
|
|
4282
|
+
<button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>${this.descriptionHeading()}</button>
|
|
4131
4283
|
${mode === "folded" && !open ? nothing : html`<div class="description ${open ? "" : "collapsed"}" @click=${() => !this.descriptionOpen && toggle()}>${text}</div>`}
|
|
4132
4284
|
${mode === "preview" && this.descriptionClipped ? html`<button class="desc-more" type="button" @click=${toggle}>${this.descriptionOpen ? "Vis mindre" : "Les mer"}</button>` : nothing}
|
|
4133
4285
|
</div>`;
|
|
@@ -4144,19 +4296,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
4144
4296
|
}
|
|
4145
4297
|
renderHighlights() {
|
|
4146
4298
|
const own = (Array.isArray(this.highlights) ? this.highlights : []).filter((h) => h && h.title);
|
|
4147
|
-
|
|
4148
|
-
if (items.length === 0) return nothing;
|
|
4149
|
-
return html`<ul class="highlights" part="highlights">
|
|
4150
|
-
${items.map((h) => {
|
|
4151
|
-
const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
|
|
4152
|
-
const icon = path ? html`<svg class="hl-icon" viewBox="0 0 24 24" aria-hidden="true"><path d=${path}></path></svg>` : h.icon ? html`<img class="hl-icon" src=${h.icon} alt="" />` : nothing;
|
|
4153
|
-
return html`<li class="hl" part="highlight">
|
|
4154
|
-
${icon}
|
|
4155
|
-
<span class="hl-title">${h.title}</span>
|
|
4156
|
-
${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
|
|
4157
|
-
</li>`;
|
|
4158
|
-
})}
|
|
4159
|
-
</ul>`;
|
|
4299
|
+
return renderHighlights(own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? []);
|
|
4160
4300
|
}
|
|
4161
4301
|
/**
|
|
4162
4302
|
* «Frakt & retur», folded like «Beskrivelse»: the delivery options for the
|
|
@@ -4318,9 +4458,6 @@ var VioProductDetail = class extends LitElement {
|
|
|
4318
4458
|
}
|
|
4319
4459
|
if (!this.product) return html``;
|
|
4320
4460
|
const p = this.product;
|
|
4321
|
-
const stock = this.stockLabel();
|
|
4322
|
-
const compare = this.compareAt;
|
|
4323
|
-
const showCompare = compare !== null && compare > this.unitPrice;
|
|
4324
4461
|
const images = this.allImages();
|
|
4325
4462
|
const activeImage = this.activeImage;
|
|
4326
4463
|
return html`
|
|
@@ -4365,24 +4502,110 @@ var VioProductDetail = class extends LitElement {
|
|
|
4365
4502
|
</div>`}
|
|
4366
4503
|
|
|
4367
4504
|
<div class="info" part="info">
|
|
4368
|
-
${
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4505
|
+
${this.renderSections(this.layoutNow.sections)}
|
|
4506
|
+
</div>
|
|
4507
|
+
</div>
|
|
4508
|
+
`;
|
|
4509
|
+
}
|
|
4510
|
+
/**
|
|
4511
|
+
* The info column in the applied layout's order (0.34) — the real DOM in
|
|
4512
|
+
* that order, never CSS `order`, the same on every screen. The default
|
|
4513
|
+
* order renders exactly the column before 0.34: brand, `.headline` (the
|
|
4514
|
+
* title row with the stock, the price row), then `.purchase` — `.buy` with
|
|
4515
|
+
* the options first — and highlights, description, «Frakt & retur» in it.
|
|
4516
|
+
*
|
|
4517
|
+
* A run of `title` / `stock` / `price` is one `.headline`; `title`
|
|
4518
|
+
* right before `stock` share the title row. `options` right before
|
|
4519
|
+
* `purchase` live in `.buy` (a pinned buy zone keeps them); elsewhere they
|
|
4520
|
+
* are a block of their own. Everything after `purchase` goes in
|
|
4521
|
+
* `.purchase`, as the description does by default: a pinned buy zone may
|
|
4522
|
+
* slide over it, never over what comes before.
|
|
4523
|
+
*/
|
|
4524
|
+
renderSections(sections) {
|
|
4525
|
+
const out = [];
|
|
4526
|
+
for (let i = 0; i < sections.length; i++) {
|
|
4527
|
+
const s = sections[i];
|
|
4528
|
+
if (s === "title" || s === "stock" || s === "price") {
|
|
4529
|
+
let j = i;
|
|
4530
|
+
while (j < sections.length && ["title", "stock", "price"].includes(sections[j])) j++;
|
|
4531
|
+
out.push(this.renderHeadline(sections.slice(i, j)));
|
|
4532
|
+
i = j - 1;
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
if (s === "options" && sections[i + 1] === "purchase") continue;
|
|
4536
|
+
if (s === "purchase") {
|
|
4537
|
+
const optionsInBuy = sections[i - 1] === "options";
|
|
4538
|
+
out.push(this.renderPurchase(optionsInBuy, sections.slice(i + 1)));
|
|
4539
|
+
break;
|
|
4540
|
+
}
|
|
4541
|
+
out.push(this.renderSection(s));
|
|
4542
|
+
}
|
|
4543
|
+
return out;
|
|
4544
|
+
}
|
|
4545
|
+
/** A run of title / stock / price: one `.headline`. */
|
|
4546
|
+
renderHeadline(run) {
|
|
4547
|
+
const p = this.product;
|
|
4548
|
+
const stock = this.stockLabel();
|
|
4549
|
+
const compare = this.compareAt;
|
|
4550
|
+
const showCompare = compare !== null && compare > this.unitPrice;
|
|
4551
|
+
const name = html`<h2 class="name" part="title">${p.title}</h2>`;
|
|
4552
|
+
const stockLine = html`<div class="stock ${stock.cls}" part="stock">${stock.text}</div>`;
|
|
4553
|
+
const parts = [];
|
|
4554
|
+
for (let i = 0; i < run.length; i++) {
|
|
4555
|
+
const s = run[i];
|
|
4556
|
+
if (s === "title" && run[i + 1] === "stock") {
|
|
4557
|
+
parts.push(html`<div class="title-row">
|
|
4558
|
+
${name}
|
|
4559
|
+
${stockLine}
|
|
4560
|
+
</div>`);
|
|
4561
|
+
i++;
|
|
4562
|
+
} else if (s === "title") {
|
|
4563
|
+
parts.push(html`<div class="title-row">${name}</div>`);
|
|
4564
|
+
} else if (s === "stock") {
|
|
4565
|
+
parts.push(stockLine);
|
|
4566
|
+
} else {
|
|
4567
|
+
parts.push(html`<div class="price-row">
|
|
4375
4568
|
<span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
|
|
4376
4569
|
${showCompare ? html`<span class="compare-at">${formatPrice(
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
</div
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4570
|
+
compare,
|
|
4571
|
+
p.price.currency_code
|
|
4572
|
+
)}</span>` : ""}
|
|
4573
|
+
</div>`);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
return html`<div class="headline">
|
|
4577
|
+
${parts}
|
|
4578
|
+
</div>`;
|
|
4579
|
+
}
|
|
4580
|
+
/** One section that is not the headline nor the purchase group. */
|
|
4581
|
+
renderSection(section) {
|
|
4582
|
+
const p = this.product;
|
|
4583
|
+
switch (section) {
|
|
4584
|
+
case "brand":
|
|
4585
|
+
return p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : "";
|
|
4586
|
+
case "options":
|
|
4587
|
+
return p.options.length > 0 ? html`<div class="buy-options options-apart" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : "";
|
|
4588
|
+
case "highlights":
|
|
4589
|
+
return this.renderHighlights();
|
|
4590
|
+
case "description":
|
|
4591
|
+
return p.description ? this.renderDescription(this.stripHtml(p.description)) : "";
|
|
4592
|
+
case "shippingReturns":
|
|
4593
|
+
return this.renderShippingReturns();
|
|
4594
|
+
default:
|
|
4595
|
+
return "";
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
/**
|
|
4599
|
+
* The SDK-managed buy group, never split: «Frakt fra…», Vipps / Apple Pay /
|
|
4600
|
+
* the other ways to pay, quantity + «Legg i handlekurv», «Du handler direkte
|
|
4601
|
+
* hos …». The options when they come right before it; then what follows it.
|
|
4602
|
+
*/
|
|
4603
|
+
renderPurchase(optionsInBuy, rest) {
|
|
4604
|
+
const p = this.product;
|
|
4605
|
+
const optionsApart = !optionsInBuy && p.options.length > 0;
|
|
4606
|
+
return html`<div class=${optionsApart ? "purchase options-apart" : "purchase"}>
|
|
4384
4607
|
<div class="buy" part="buy">
|
|
4385
|
-
${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
|
|
4608
|
+
${optionsInBuy && p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
|
|
4386
4609
|
${this.renderDeliveryNote()}
|
|
4387
4610
|
${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
|
|
4388
4611
|
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
|
|
@@ -4411,7 +4634,7 @@ var VioProductDetail = class extends LitElement {
|
|
|
4411
4634
|
<span class="vipps-badge">vipps</span>
|
|
4412
4635
|
</button>
|
|
4413
4636
|
` : ""}
|
|
4414
|
-
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps")
|
|
4637
|
+
${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.renderVippsNote() : ""}
|
|
4415
4638
|
|
|
4416
4639
|
<div class="qty-cta-row" part="quantity-row">
|
|
4417
4640
|
<div class="qty-stepper">
|
|
@@ -4499,14 +4722,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
4499
4722
|
${this.renderSellerNote()}
|
|
4500
4723
|
</div>
|
|
4501
4724
|
|
|
4502
|
-
${this.
|
|
4503
|
-
|
|
4504
|
-
${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
|
|
4505
|
-
${this.renderShippingReturns()}
|
|
4506
|
-
</div>
|
|
4507
|
-
</div>
|
|
4508
|
-
</div>
|
|
4509
|
-
`;
|
|
4725
|
+
${this.renderSections(rest)}
|
|
4726
|
+
</div>`;
|
|
4510
4727
|
}
|
|
4511
4728
|
renderOptionPickers(options) {
|
|
4512
4729
|
if (options.length === 0) return "";
|
|
@@ -4547,8 +4764,8 @@ var VioProductDetail = class extends LitElement {
|
|
|
4547
4764
|
})}`;
|
|
4548
4765
|
}
|
|
4549
4766
|
/** Best-effort HTML strip for description. */
|
|
4550
|
-
stripHtml(
|
|
4551
|
-
return
|
|
4767
|
+
stripHtml(html8) {
|
|
4768
|
+
return html8.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
4552
4769
|
}
|
|
4553
4770
|
};
|
|
4554
4771
|
__decorateClass([
|
|
@@ -4590,12 +4807,18 @@ __decorateClass([
|
|
|
4590
4807
|
__decorateClass([
|
|
4591
4808
|
property({ type: Boolean, attribute: "no-added-toast" })
|
|
4592
4809
|
], VioProductDetail.prototype, "noAddedToast", 2);
|
|
4810
|
+
__decorateClass([
|
|
4811
|
+
property({ type: String, attribute: "vipps-cart-payment" })
|
|
4812
|
+
], VioProductDetail.prototype, "vippsCartPayment", 2);
|
|
4593
4813
|
__decorateClass([
|
|
4594
4814
|
property({ type: String })
|
|
4595
4815
|
], VioProductDetail.prototype, "gallery", 2);
|
|
4596
4816
|
__decorateClass([
|
|
4597
4817
|
property({ type: String, attribute: "description-mode" })
|
|
4598
4818
|
], VioProductDetail.prototype, "descriptionMode", 2);
|
|
4819
|
+
__decorateClass([
|
|
4820
|
+
property({ type: String, attribute: "description-label" })
|
|
4821
|
+
], VioProductDetail.prototype, "descriptionLabel", 2);
|
|
4599
4822
|
__decorateClass([
|
|
4600
4823
|
property({ type: Array })
|
|
4601
4824
|
], VioProductDetail.prototype, "highlights", 2);
|
|
@@ -4605,6 +4828,9 @@ __decorateClass([
|
|
|
4605
4828
|
__decorateClass([
|
|
4606
4829
|
property({ type: Object, attribute: "carrier-logos" })
|
|
4607
4830
|
], VioProductDetail.prototype, "carrierLogos", 2);
|
|
4831
|
+
__decorateClass([
|
|
4832
|
+
property({ attribute: "layout", converter: layoutAttribute })
|
|
4833
|
+
], VioProductDetail.prototype, "layout", 2);
|
|
4608
4834
|
__decorateClass([
|
|
4609
4835
|
state()
|
|
4610
4836
|
], VioProductDetail.prototype, "product", 2);
|
|
@@ -4724,6 +4950,12 @@ function cartShipping(items, currency) {
|
|
|
4724
4950
|
}
|
|
4725
4951
|
return null;
|
|
4726
4952
|
}
|
|
4953
|
+
function cartShippingIsFrom(items, currency) {
|
|
4954
|
+
if (items.some((item) => ratePrice(item.shipping, currency) !== null)) return false;
|
|
4955
|
+
return items.some(
|
|
4956
|
+
(item) => (item.availableShippings ?? []).filter((r) => ratePrice(r, currency) !== null).length > 1
|
|
4957
|
+
);
|
|
4958
|
+
}
|
|
4727
4959
|
var VioCart = class extends LitElement {
|
|
4728
4960
|
constructor() {
|
|
4729
4961
|
super(...arguments);
|
|
@@ -4731,12 +4963,15 @@ var VioCart = class extends LitElement {
|
|
|
4731
4963
|
this.heading = "Handlekurv";
|
|
4732
4964
|
this.emptyLabel = "Handlekurven er tom";
|
|
4733
4965
|
this.subtotalLabel = "Sum";
|
|
4966
|
+
this.vippsCartPayment = "";
|
|
4734
4967
|
this.carts = /* @__PURE__ */ new Map();
|
|
4735
4968
|
this.itemCount = 0;
|
|
4736
4969
|
this.applePayAvailable = false;
|
|
4737
4970
|
this.availableMethods = null;
|
|
4738
4971
|
this.methodsLoading = false;
|
|
4739
4972
|
this.vippsOfficial = false;
|
|
4973
|
+
/** A Vipps payment is being started from here: one at a time. */
|
|
4974
|
+
this.vippsStarting = false;
|
|
4740
4975
|
/** Whether the method lookup has been attempted — see boundOnCartChange. */
|
|
4741
4976
|
this.paymentMethodsAttempted = false;
|
|
4742
4977
|
this.confirmedOrder = null;
|
|
@@ -4772,7 +5007,8 @@ var VioCart = class extends LitElement {
|
|
|
4772
5007
|
};
|
|
4773
5008
|
}
|
|
4774
5009
|
static {
|
|
4775
|
-
this.styles =
|
|
5010
|
+
this.styles = [
|
|
5011
|
+
css`
|
|
4776
5012
|
:host { display: contents; }
|
|
4777
5013
|
|
|
4778
5014
|
.backdrop {
|
|
@@ -4991,6 +5227,12 @@ var VioCart = class extends LitElement {
|
|
|
4991
5227
|
font-size: 14px;
|
|
4992
5228
|
}
|
|
4993
5229
|
.total-row { font-weight: 700; font-size: 16px; margin-top: 0; }
|
|
5230
|
+
/* Where the shipping method is chosen: in Vipps, or in the checkout. */
|
|
5231
|
+
.shipping-hint {
|
|
5232
|
+
margin: calc(var(--vio-space-md, 16px) * -0.5) 0 var(--vio-space-md, 16px);
|
|
5233
|
+
font-size: 12px;
|
|
5234
|
+
color: var(--vio-color-text-secondary, #666);
|
|
5235
|
+
}
|
|
4994
5236
|
.subtotal-value { font-weight: 700; font-size: 16px; }
|
|
4995
5237
|
|
|
4996
5238
|
/* Apple Pay express — pay the whole cart directly. */
|
|
@@ -5148,7 +5390,9 @@ var VioCart = class extends LitElement {
|
|
|
5148
5390
|
font-family: var(--vio-font-sans, -apple-system, sans-serif);
|
|
5149
5391
|
}
|
|
5150
5392
|
.confirm-close:hover { background: #222; }
|
|
5151
|
-
|
|
5393
|
+
`,
|
|
5394
|
+
highlightsStyles("cart")
|
|
5395
|
+
];
|
|
5152
5396
|
}
|
|
5153
5397
|
connectedCallback() {
|
|
5154
5398
|
super.connectedCallback();
|
|
@@ -5312,7 +5556,6 @@ var VioCart = class extends LitElement {
|
|
|
5312
5556
|
})
|
|
5313
5557
|
);
|
|
5314
5558
|
}
|
|
5315
|
-
/** Vipps — opens the checkout with Vipps preselected. */
|
|
5316
5559
|
/**
|
|
5317
5560
|
* Open the checkout without preselecting a method.
|
|
5318
5561
|
*
|
|
@@ -5334,21 +5577,73 @@ var VioCart = class extends LitElement {
|
|
|
5334
5577
|
);
|
|
5335
5578
|
}
|
|
5336
5579
|
/**
|
|
5337
|
-
* Vipps from the cart:
|
|
5338
|
-
*
|
|
5339
|
-
*
|
|
5340
|
-
*
|
|
5341
|
-
*
|
|
5580
|
+
* Vipps from the cart (2026-10-29: «el botón negro va con el nuestro y el
|
|
5581
|
+
* botón de Vipps paga el carrito entero»): Vipps Express for the sponsor's
|
|
5582
|
+
* WHOLE cart — the same Express as the detail's «Kjøp nå med Vipps»: the
|
|
5583
|
+
* hand-over overlay, `vio:payment-redirect-*` with source `'cart'`, the
|
|
5584
|
+
* return and its confirmation. «Til kassen» is the way into our checkout.
|
|
5585
|
+
* When the brand has Express off, or this cart cannot have it (several
|
|
5586
|
+
* suppliers, no rate), the checkout opens with Vipps preselected.
|
|
5587
|
+
*
|
|
5588
|
+
* `vipps-cart-payment="checkout"` keeps the 0.31–0.34 behaviour: the
|
|
5589
|
+
* checkout opens with Vipps preselected, the standard payment.
|
|
5342
5590
|
*/
|
|
5343
5591
|
onVipps() {
|
|
5344
5592
|
if (!this.payable) return;
|
|
5345
|
-
|
|
5346
|
-
|
|
5593
|
+
if (resolveVippsCartPayment(this, this.vippsCartPayment) === "checkout") {
|
|
5594
|
+
const firstSponsorId = [...this.carts.keys()][0];
|
|
5595
|
+
if (firstSponsorId === void 0) return;
|
|
5596
|
+
this.openCheckoutWithVipps(firstSponsorId);
|
|
5597
|
+
return;
|
|
5598
|
+
}
|
|
5599
|
+
if (this.vippsStarting) return;
|
|
5600
|
+
const sponsorId = this.vippsSponsorId();
|
|
5601
|
+
if (sponsorId === void 0) return;
|
|
5602
|
+
this.vippsStarting = true;
|
|
5603
|
+
const redirect = beginPaymentRedirect({ method: "vipps", sponsorId, source: "cart" });
|
|
5604
|
+
void this.startVippsExpressOrCheckout(sponsorId, redirect).finally(() => {
|
|
5605
|
+
this.vippsStarting = false;
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* The one sponsor whose cart the Vipps button pays: the first with
|
|
5610
|
+
* something to pay for. Express is built on THAT sponsor's backend cart
|
|
5611
|
+
* only — another sponsor's lines are never in it.
|
|
5612
|
+
*/
|
|
5613
|
+
vippsSponsorId() {
|
|
5614
|
+
for (const cart of this.carts.values()) {
|
|
5615
|
+
const total = cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
5616
|
+
if (cart.items.length > 0 && total > 0) return cart.sponsorId;
|
|
5617
|
+
}
|
|
5618
|
+
return void 0;
|
|
5619
|
+
}
|
|
5620
|
+
async startVippsExpressOrCheckout(sponsorId, redirect) {
|
|
5621
|
+
let fallback = true;
|
|
5622
|
+
try {
|
|
5623
|
+
await Vio.cart.waitForCartMutations(sponsorId);
|
|
5624
|
+
if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
|
|
5625
|
+
redirect.abort("express-off");
|
|
5626
|
+
} else {
|
|
5627
|
+
const res = await Vio.checkout.startVippsExpress(sponsorId);
|
|
5628
|
+
fallback = !res?.payment_url;
|
|
5629
|
+
if (fallback) redirect.abort("not-started");
|
|
5630
|
+
}
|
|
5631
|
+
} catch (err) {
|
|
5632
|
+
if (typeof console !== "undefined") {
|
|
5633
|
+
console.warn("[VioCart] Vipps Express not started:", err);
|
|
5634
|
+
}
|
|
5635
|
+
redirect.fail(err instanceof Error ? err.message : String(err));
|
|
5636
|
+
}
|
|
5637
|
+
if (!fallback) return;
|
|
5638
|
+
this.openCheckoutWithVipps(sponsorId);
|
|
5639
|
+
}
|
|
5640
|
+
/** Our checkout with Vipps preselected: the host opens it on `vio:checkout-open`. */
|
|
5641
|
+
openCheckoutWithVipps(sponsorId) {
|
|
5347
5642
|
this.dispatchEvent(
|
|
5348
5643
|
new CustomEvent("vio:checkout-open", {
|
|
5349
5644
|
bubbles: true,
|
|
5350
5645
|
composed: true,
|
|
5351
|
-
detail: { sponsorId
|
|
5646
|
+
detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
|
|
5352
5647
|
})
|
|
5353
5648
|
);
|
|
5354
5649
|
}
|
|
@@ -5558,23 +5853,39 @@ var VioCart = class extends LitElement {
|
|
|
5558
5853
|
<span class="vipps-logo">vipps</span>
|
|
5559
5854
|
</button>
|
|
5560
5855
|
` : ""}
|
|
5856
|
+
${this.renderBrandHighlights()}
|
|
5561
5857
|
</div>
|
|
5562
5858
|
`}
|
|
5563
5859
|
</aside>
|
|
5564
5860
|
`;
|
|
5565
5861
|
}
|
|
5862
|
+
/**
|
|
5863
|
+
* The selling brand's promises under the ways to pay, as in the product
|
|
5864
|
+
* detail (one article = one brand: the cart's brand). Hidden with the theme's
|
|
5865
|
+
* `cartHighlightsDisplay: 'none'`; nothing when the brand has none.
|
|
5866
|
+
*/
|
|
5867
|
+
renderBrandHighlights() {
|
|
5868
|
+
const sponsorId = [...this.carts.keys()].find((id) => (this.carts.get(id)?.items.length ?? 0) > 0);
|
|
5869
|
+
if (sponsorId === void 0) return "";
|
|
5870
|
+
return renderHighlights(resolveSponsorIdentity(sponsorId)?.highlights ?? []);
|
|
5871
|
+
}
|
|
5566
5872
|
/** «Frakt» (the chosen or cheapest rate) and «Totalt» with it; «Beregnes i kassen» when unknown. */
|
|
5567
5873
|
renderShippingRows() {
|
|
5568
5874
|
const shipping = this.shippingAcrossSponsors;
|
|
5875
|
+
const from = shipping !== null && shipping > 0 && [...this.carts.values()].some(
|
|
5876
|
+
(cart) => cart.items.length > 0 && cartShippingIsFrom(cart.items, cart.currency)
|
|
5877
|
+
);
|
|
5878
|
+
const fra = from ? "fra " : "";
|
|
5569
5879
|
return html`
|
|
5570
5880
|
<div class="shipping-row" part="cart-shipping">
|
|
5571
5881
|
<span class="subtotal-label">Frakt</span>
|
|
5572
|
-
<span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : formatPrice(shipping, this.primaryCurrency)}</span>
|
|
5882
|
+
<span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : `${fra}${formatPrice(shipping, this.primaryCurrency)}`}</span>
|
|
5573
5883
|
</div>
|
|
5574
5884
|
${shipping !== null ? html`<div class="total-row" part="cart-total">
|
|
5575
5885
|
<span>Totalt</span>
|
|
5576
|
-
<span>${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
|
|
5886
|
+
<span>${fra}${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
|
|
5577
5887
|
</div>` : ""}
|
|
5888
|
+
<p class="shipping-hint" part="cart-shipping-hint">Frakt velger du i Vipps eller i neste steg.</p>
|
|
5578
5889
|
`;
|
|
5579
5890
|
}
|
|
5580
5891
|
renderConfirmation() {
|
|
@@ -5616,6 +5927,9 @@ __decorateClass([
|
|
|
5616
5927
|
__decorateClass([
|
|
5617
5928
|
property({ type: String, attribute: "subtotal-label" })
|
|
5618
5929
|
], VioCart.prototype, "subtotalLabel", 2);
|
|
5930
|
+
__decorateClass([
|
|
5931
|
+
property({ type: String, attribute: "vipps-cart-payment" })
|
|
5932
|
+
], VioCart.prototype, "vippsCartPayment", 2);
|
|
5619
5933
|
__decorateClass([
|
|
5620
5934
|
state()
|
|
5621
5935
|
], VioCart.prototype, "carts", 2);
|
|
@@ -9391,5 +9705,5 @@ function registerVioElements() {
|
|
|
9391
9705
|
}
|
|
9392
9706
|
|
|
9393
9707
|
export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
|
|
9394
|
-
//# sourceMappingURL=chunk-
|
|
9395
|
-
//# sourceMappingURL=chunk-
|
|
9708
|
+
//# sourceMappingURL=chunk-DEREQRCM.js.map
|
|
9709
|
+
//# sourceMappingURL=chunk-DEREQRCM.js.map
|