@salla.sa/twilight-components
Version:
Salla Web Component
958 lines (947 loc) • 130 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
'use strict';
var index = require('./index-uoA36zqH.js');
var cart = require('./cart-s-x1Fshk.js');
var Helper = require('./Helper-CU4Xuiki.js');
var bellRing = require('./bell-ring-BfKPinNo.js');
var interfaces = require('./interfaces-Bh7W0bEU.js');
var check = require('./check-BnVBPQNp.js');
var camera = require('./camera-DytepEoK.js');
require('./anime.es-BqW8JHZi.js');
var WalletIcon = `<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>full-wallet</title>
<path d="M29 12h-26c-0.668-0.008-1.284-0.226-1.787-0.59l0.009 0.006c-0.744-0.552-1.222-1.428-1.222-2.416 0-1.657 1.343-3 2.999-3h6c0.552 0 1 0.448 1 1s-0.448 1-1 1v0h-6c-0.552 0-1 0.448-1 1 0 0.326 0.156 0.616 0.397 0.798l0.002 0.002c0.167 0.12 0.374 0.194 0.599 0.2l0.001 0h26c0.552 0 1 0.448 1 1s-0.448 1-1 1v0zM27 12c-0.552 0-1-0.448-1-1v0-3h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1v0h4c0.552 0 1 0.448 1 1v0 4c0 0.552-0.448 1-1 1v0zM29 30h-26c-1.657 0-3-1.343-3-3v0-18c0-0.552 0.448-1 1-1s1 0.448 1 1v0 18c0 0.552 0.448 1 1 1v0h25v-5c0-0.552 0.448-1 1-1s1 0.448 1 1v0 6c0 0.552-0.448 1-1 1v0zM29 18c-0.552 0-1-0.448-1-1v0-6c0-0.552 0.448-1 1-1s1 0.448 1 1v0 6c0 0.552-0.448 1-1 1v0zM31 24h-7c-2.209 0-4-1.791-4-4s1.791-4 4-4v0h7c0.552 0 1 0.448 1 1v0 6c0 0.552-0.448 1-1 1v0zM24 18c-1.105 0-2 0.895-2 2s0.895 2 2 2v0h6v-4zM25 12c-0.001 0-0.001 0-0.002 0-0.389 0-0.726-0.222-0.891-0.546l-0.003-0.006-3.552-7.106-2.306 1.152c-0.13 0.066-0.284 0.105-0.447 0.105-0.552 0-1-0.448-1-1 0-0.39 0.223-0.727 0.548-0.892l0.006-0.003 3.2-1.6c0.13-0.067 0.284-0.106 0.447-0.106 0.39 0 0.727 0.223 0.892 0.548l0.003 0.006 4 8c0.067 0.13 0.106 0.285 0.106 0.448 0 0.552-0.448 1-1 1v0zM21 12c-0.001 0-0.001 0-0.002 0-0.389 0-0.726-0.222-0.891-0.546l-0.003-0.006-3.552-7.106-15.104 7.552c-0.13 0.066-0.284 0.105-0.447 0.105-0.552 0-1-0.448-1-1 0-0.39 0.223-0.727 0.548-0.892l0.006-0.003 16-8c0.13-0.067 0.284-0.106 0.447-0.106 0.39 0 0.727 0.223 0.892 0.548l0.003 0.006 4 8c0.067 0.13 0.106 0.285 0.106 0.448 0 0.552-0.448 1-1 1-0.001 0-0.001 0-0.002 0h0z"></path>
</svg>
`;
const sallaAddProductButtonCss = ":host{display:block}salla-add-product-button[width=wide]{width:100%}";
const SallaAddProductButton = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
this.success = index.createEvent(this, "success");
this.failed = index.createEvent(this, "failed");
this.hostAttributes = {};
/**
* Has Pre Order
*/
this.hasPreOrder = false;
/**
* Product Status.Defaults to `sale`
*/
this.productStatus = 'sale';
/**
* Product type. Defaults to `product`
*/
this.productType = 'product';
this.selectedOptions = [];
this.buyNowText = salla.lang.get('pages.products.buy_now');
/** Use matchMedia instead of window.innerWidth to avoid forced reflow (Lighthouse/PageSpeed) */
this.isDesktopViewport = typeof window !== 'undefined' ? window.matchMedia('(min-width: 768px)').matches : true;
this.viewportMediaQuery = null;
this.onViewportChange = (e) => {
this.isDesktopViewport = e.matches;
this.btn?.setText((e.matches && !!this.passedLabel) ? this.passedLabel : this.getLabel());
};
salla.lang.onLoaded(() => {
this.buyNowText = salla.lang.get('pages.products.buy_now');
});
}
getLabel() {
if (this.productStatus === 'sale' && this.supportStickyBar && !this.isDesktopViewport && this.showQuickBuy && this.isApplePayActive) {
return cart.PendingOrdersIcon;
}
if (this.hasPreOrder) {
return salla.lang.get('pages.products.pre_order_now');
}
if (this.productStatus === 'sale' && this.productType === 'booking') {
return salla.lang.get('pages.cart.book_now');
}
if (this.productStatus === 'sale') {
return salla.lang.get('pages.cart.add_to_cart');
}
if (this.productType !== 'donating') {
return salla.lang.get('pages.products.out_of_stock');
}
// donating
return salla.lang.get('pages.products.donation_exceed');
}
addProductToCart(event) {
if (this.productType === 'booking') {
event.preventDefault();
return this.addBookingProduct();
}
// we want to ignore the click action when the type of button is submit a form
if (this.hostAttributes.type === 'submit') {
return false;
}
event.preventDefault();
this.btn?.disable();
// Validate product options explicitly on Add to Cart click
const optionsEl = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
if (optionsEl && typeof optionsEl.validateAndScroll === 'function') {
return optionsEl.validateAndScroll()
.then(async (isValid) => {
// Call validate hook
const ctx = { isValid, component: this };
await salla.hooks.call('salla-add-product-button', 'validate', ctx);
if (!ctx.isValid) {
this.btn?.enable();
return;
}
return this._proceedAddToCart();
})
.catch(() => {
this.btn?.enable();
});
}
// Fallback if options component not found
return this._proceedAddToCart();
}
async _proceedAddToCart() {
/**
* by default the quick add is just an alias for add item function
* but its work only when the id is the only value is passed via the object
* so we will filter the object entities to remove null and zero values in case we don't want the normal add item action
*/
const parent = this.host.closest('salla-products-list, salla-products-slider');
const data = Object.entries({
id: this.productId,
donation_amount: this.donatingAmount,
quantity: this.quantity,
endpoint: 'quickAdd',
source: Helper.Helper.getProductsSource(parent?.getAttribute('source'))
}).reduce((a, [k, v]) => (v ? (a[k] = v, a) : a), {});
// Use the potentially modified data
return salla.cart.addItem(data)
.then(async (response) => {
this.selectedOptions = [];
this.btn?.enable();
this.success.emit(response);
return response;
})
.catch(error => { this.failed.emit(error); this.btn?.enable(); });
}
addBookingProduct() {
if (salla.config.isGuest()) {
salla.auth.api.setAfterLoginEvent('booking::add', this.productId);
salla.event.dispatch('login::open');
return;
}
return salla.booking.add(this.productId)
.then(resp => this.success.emit(resp))
.catch(error => this.failed.emit(error));
}
getBtnAttributes() {
for (let i = 0; i < this.host.attributes.length; i++) {
if (!['id', 'class'].includes(this.host.attributes[i].name)) {
this.hostAttributes[this.host.attributes[i].name] = this.host.attributes[i].value;
}
}
return this.hostAttributes;
}
getQuickBuyBtnAttributes() {
return {
...this.getBtnAttributes(),
type: this.supportStickyBar && !this.isDesktopViewport ? 'plain' : this.productType == 'donating' ? 'donate' : 'buy'
};
}
miniCheckoutWidget() {
let storeId = salla.config.get('store.id');
if (!storeId) {
return;
}
return index.h("salla-mini-checkout-widget", { language: salla.lang.locale, "store-id": storeId, config: { user: salla.config.get('user'), failure_url: window.location.href }, products: [this.productId], api: salla.config.get('store.api'), outline: true, "form-selector": "form.product-form", class: "s-add-product-button-mini-checkout" }, index.h("div", { slot: "widget-label", class: "s-add-product-button-mini-checkout-content" }, index.h("span", { innerHTML: WalletIcon }), this.buyNowText));
}
componentWillLoad() {
return salla.onReady()
.then(() => {
// this to fix not added hydrated class to html element after components loaded
document.documentElement.classList.add('hydrated');
this.showQuickBuy = this.quickBuy
&& salla.config.get('store.settings.buy_now')
&& this.productStatus == 'sale'
&& this.productType !== 'booking';
this.isApplePayActive = window.ApplePaySession?.canMakePayments()
&& salla.config.get('store.settings.payments')?.includes('apple_pay')
&& salla.config.get('store.settings.is_salla_gateway', false);
this.passedLabel = this.host.innerHTML.replace('<!---->', '').trim();
if (!!this.passedLabel && this.isDesktopViewport) {
return this.btn?.setText(this.passedLabel);
}
if (this.host.getAttribute('type') === 'submit' && this.supportStickyBar) {
this.viewportMediaQuery = window.matchMedia('(min-width: 768px)');
this.viewportMediaQuery.addEventListener('change', this.onViewportChange);
}
});
}
render() {
//TODO:: find a better fix, this is a patch for issue that duplicates the buttons more than twice @see the screenshot inside this folder
if (this.host.closest('.swiper-slide')?.classList.contains('swiper-slide-duplicate')) {
return '';
}
if (this.hasSubscribedOptions) {
return index.h(index.Host, null, index.h("salla-product-availability", { ...this.getBtnAttributes(), "is-subscribed": true }, index.h("span", { class: "s-hidden" }, index.h("slot", null))));
}
if ((this.productStatus === 'out-and-notify' && this.channels) || this.hasOutOfStockOption) {
return index.h(index.Host, null, index.h("salla-product-availability", { ...this.getBtnAttributes() }, index.h("span", { class: "s-hidden" }, index.h("slot", null))));
}
return index.h(index.Host, { class: {
's-add-product-button-with-quick-buy': this.showQuickBuy,
's-add-product-button-with-sticky-bar': this.supportStickyBar,
's-add-product-button-with-apple-pay': this.showQuickBuy && this.isApplePayActive
} }, index.h("div", { class: {
's-add-product-button-main': this.showQuickBuy,
'w-full': !document.getElementById('fast-checkout-js') || ['financial_support', 'donating'].includes(this.productType) // This is a temporary fix until all themes fully support the fast checkout -- To be removed later
} }, index.h("salla-button", { color: this.productStatus === 'sale' ? 'primary' : 'light', type: "button", fill: this.productStatus === 'sale' ? 'solid' : 'outline', ref: el => this.btn = el, onClick: event => this.addProductToCart(event), disabled: this.productStatus !== 'sale', ...this.getBtnAttributes(), "loader-position": "center" }, index.h("slot", null)), this.showQuickBuy && !!document.getElementById('fast-checkout-js') && !['financial_support', 'donating'].includes(this.productType) ? this.miniCheckoutWidget() : ''), this.showQuickBuy && this.isApplePayActive ? index.h("salla-quick-buy", { ...this.getQuickBuyBtnAttributes() }) : '');
}
async componentDidLoad() {
// Register this component for hooks system, already on it logic to run componentDidLoad trigger
await Salla.hooks.registerComponent('salla-add-product-button', this);
if (!this.notifyOptionsAvailability) {
return;
}
salla.event.on('product-options::change', async (data) => {
if (!['thumbnail', 'color', 'single-option'].includes(data.option.type)) {
return;
}
this.hasSubscribedOptions = false;
this.selectedOptions = await document.querySelector(`salla-product-options[product-id="${this.productId}"]`)?.getSelectedOptions();
this.hasOutOfStockOption = await document.querySelector(`salla-product-options[product-id="${this.productId}"]`)?.hasOutOfStockOption();
let subscribedDetails = salla.storage.get(`product-${this.productId}-subscribed-options`);
if (!subscribedDetails && !this.subscribedOptions || !this.hasOutOfStockOption) {
return;
}
if (salla.config.isGuest()) {
const parsedSubscribedDetails = subscribedDetails ? subscribedDetails.map(ids => ids.split(',').map(id => parseInt(id))) : [];
this.hasSubscribedOptions = parsedSubscribedDetails.length > 0 && parsedSubscribedDetails.some(ids => ids.every(id => this.selectedOptions.some(option => option.id === id)));
}
else {
this.hasSubscribedOptions = this.subscribedOptions && this.subscribedOptions !== 'null' && this.subscribedOptions !== '[]' ? JSON.parse(this.subscribedOptions).some(ids => ids.every(id => this.selectedOptions.some(option => option.id === id))) : false;
}
});
}
componentDidRender() {
//if label not passed, get label
if (!!this.passedLabel && (!this.supportStickyBar || this.isDesktopViewport)) {
// if passed label, set it
this.btn?.setText(this.passedLabel);
return;
}
this.btn?.setText(this.getLabel());
salla.lang.onLoaded(() => this.btn?.setText(this.getLabel()));
}
disconnectedCallback() {
this.viewportMediaQuery?.removeEventListener('change', this.onViewportChange);
}
get host() { return index.getElement(this); }
};
SallaAddProductButton.style = sallaAddProductButtonCss;
const sallaBoughtTogetherCss = ":host{display:block}.s-bought-together-checkbox:checked~.s-bought-together-checkmark{background-color:var(--color-primary, #2ECC71);border-color:var(--color-primary, #2ECC71)}.s-bought-together-checkbox:checked~.s-bought-together-checkmark::after{content:\"\";display:block;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg);margin-top:-2px}";
// Read-only recommendations key confirmed safe for client-side use by the API team — no write access or sensitive scope.
const XSELL_API_KEY = 'VOL5WaZu2YROp0RplCZAr1RplhL9FFGQ';
const SallaBoughtTogether = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
/**
* Maximum number of recommendations to fetch. Max is 4.
*/
this.limit = 3;
this.recommendations = [];
this.selectedIds = new Set();
this.isLoading = true;
this.canRender = true;
this.isAdding = false;
this.title = salla.lang.get('pages.products.bought_together_title');
this.subtitle = salla.lang.get('pages.products.bought_together_subtitle');
this.buyTogetherFor = salla.lang.get('pages.products.buy_together_for');
this.resolvedProductId = 0;
salla.lang.onLoaded(() => {
this.title = salla.lang.get('pages.products.bought_together_title');
this.subtitle = salla.lang.get('pages.products.bought_together_subtitle');
this.buyTogetherFor = salla.lang.get('pages.products.buy_together_for');
});
}
componentWillLoad() {
salla.onReady()
.then(() => {
if (!salla.config.get('store.settings.product.bought_together')) {
this.canRender = false;
return;
}
const id = salla.config.get('page.id');
if (!id) {
this.canRender = false;
return;
}
this.resolvedProductId = id;
return salla.api.request('https://api.salla.dev/1/indexes/*/recommendations', {
requests: [{
indexName: 'products',
model: 'xsell-v1',
priceThreshold: 2,
objectID: String(id),
maxRecommendations: this.limit,
}]
}, 'post', { headers: { 'X-Algolia-Api-Key': XSELL_API_KEY, 'X-Query-Enrichment': 'false', 'X-source': 'store' } });
})
.then((response) => {
if (!response)
return;
const hits = response?.results?.[0]?.hits ?? [];
if (!hits.length) {
this.canRender = false;
return;
}
const ids = [String(this.resolvedProductId), ...hits.map(h => h.objectID)];
return salla.product.fetch({ source: 'selected', source_value: ids });
})
.then((response) => {
if (!response)
return;
const products = response?.data ?? [];
if (!products.length) {
this.canRender = false;
return;
}
const mainProduct = products.find(p => String(p.id) === String(this.resolvedProductId));
const rest = products.filter(p => String(p.id) !== String(this.resolvedProductId));
if (!rest.length) {
this.canRender = false;
return;
}
this.recommendations = mainProduct ? [mainProduct, ...rest] : rest;
this.selectedIds = new Set(this.recommendations.map((p) => p.id));
})
.catch(() => {
this.canRender = false;
})
.finally(() => {
this.isLoading = false;
});
}
toggleProduct(id) {
const next = new Set(this.selectedIds);
next.has(id) ? next.delete(id) : next.add(id);
this.selectedIds = next;
}
get totalPrice() {
const total = this.recommendations
.filter(p => this.selectedIds.has(p.id))
// p.price is a plain float in the API response (e.g. 1150.58) — the TS type (any[]) is misleading.
.reduce((sum, p) => sum + (p.price ?? 0), 0);
const decimals = salla.config.get('store.currency.decimals') ?? 2;
const factor = Math.pow(10, decimals);
return salla.money(Math.round(total * factor) / factor);
}
async addSelectedToCart() {
if (this.isAdding || !this.selectedIds.size)
return;
this.isAdding = true;
try {
// Intentional: quickAdd is used for all items including the main product without options — business decision by product team.
const results = await Promise.allSettled(Array.from(this.selectedIds).map(id => salla.cart.quickAdd(id)));
if (results.some(r => r.status === 'rejected')) {
salla.error(salla.lang.get('common.messages.error'));
}
}
finally {
this.isAdding = false;
}
}
getSkeletonView() {
return (index.h(index.Host, { class: "s-bought-together-entry" }, index.h("div", { class: "s-bought-together-skeleton" }, index.h("div", { class: "s-bought-together-skeleton-header" }, index.h("salla-skeleton", { height: "16px", width: "35%" }), index.h("salla-skeleton", { height: "10px", width: "60%" })), Array(4).fill(null).map((_, i) => (index.h("div", { key: i, class: "s-bought-together-skeleton-item" }, index.h("salla-skeleton", { height: "22px", width: "22px" }), index.h("salla-skeleton", { height: "48px", width: "48px" }), index.h("div", { class: "s-bought-together-skeleton-info" }, index.h("salla-skeleton", { height: "12px", width: "55%", style: { marginBottom: '3px' } }), index.h("salla-skeleton", { height: "10px", width: "25%" }))))), index.h("salla-skeleton", { height: "44px", width: "100%" }))));
}
render() {
if (!this.canRender)
return null;
if (this.isLoading)
return this.getSkeletonView();
if (!this.recommendations.length)
return null;
return (index.h(index.Host, { class: "s-bought-together-entry" }, index.h("div", { class: "s-bought-together-header" }, index.h("h3", { class: "s-bought-together-title" }, this.title), index.h("p", { class: "s-bought-together-subtitle" }, this.subtitle)), index.h("div", { class: "s-bought-together-list" }, this.recommendations.map((product) => (index.h("div", { key: product.id, class: `s-bought-together-item${!this.selectedIds.has(product.id) ? ' s-bought-together-item--unchecked' : ''}` }, index.h("label", { class: "s-bought-together-checkbox-label" }, index.h("input", { type: "checkbox", class: "s-bought-together-checkbox", checked: this.selectedIds.has(product.id), onChange: () => this.toggleProduct(product.id) }), index.h("span", { class: "s-bought-together-checkmark" })), index.h("a", { class: "s-bought-together-item-link", href: product.url }, index.h("img", { class: "s-bought-together-item-img", src: product.image?.url || salla.url.cdn('images/s-empty.png'), alt: product.image?.alt || product.name, loading: "lazy", decoding: "async", onError: e => {
e.currentTarget.onerror = null;
e.currentTarget.src = salla.url.cdn('images/s-empty.png');
} }), index.h("span", { class: "s-bought-together-item-name" }, product.name)), index.h("span", { class: "s-bought-together-item-price", innerHTML: product.price != null ? salla.money(product.price) : '' }))))), index.h("salla-button", { class: "s-bought-together-btn", disabled: !this.selectedIds.size || this.isAdding, loading: this.isAdding, onClick: () => this.addSelectedToCart() }, index.h("span", { innerHTML: `${this.buyTogetherFor} ${this.totalPrice}` }))));
}
};
SallaBoughtTogether.style = sallaBoughtTogetherCss;
const sallaProductAvailabilityCss = "";
const SallaProductAvailability = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
this.isUser = salla.config.isUser();
this.translationLoaded = false;
/**
* Listen to product options availability.
*/
this.notifyOptionsAvailability = false;
/**
* is current user already subscribed
*/
this.isSubscribed = false;
this.handleSubmitOptions = async () => {
let payload = { id: this.productId };
if (!this.notifyOptionsAvailability) {
return payload;
}
let optionsElement = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
let options = Object.values(await optionsElement?.getSelectedOptionsData() || {});
//if all options not selected, show message && throw exception
if (options.length && !await optionsElement?.reportValidity()) {
let errorMessage = salla.lang.get('common.messages.required_fields');
salla.error(errorMessage);
throw errorMessage;
}
payload.options = [];
options.forEach(option => {
//inject numbers only, without zeros
if (option && !isNaN(option)) {
payload.options.push(Number(option));
}
});
return payload;
};
// helpers
this.typing = (e) => {
const error = e.target.nextElementSibling;
e.target.classList.remove('s-has-error');
error?.classList.contains('s-product-availability-error-msg') && (error.innerText = '');
e.keyCode === 13 && this.submit();
};
salla.lang.onLoaded(() => {
this.translationLoaded = true;
this.title_ = this.host.title || salla.lang.get('pages.products.notify_availability_title');
this.modal?.setTitle(this.title_);
});
if (!this.productId) {
this.productId = salla.config.get('page.id');
}
if (this.isUser)
return;
this.channelsWatcher(this.channels);
this.title_ = this.host.title || salla.lang.get('pages.products.notify_availability_title');
this.host.removeAttribute('title');
//todo:: fix this to cover options too
this.isVisitorSubscribed = !this.notifyOptionsAvailability ? salla.storage.get(`product-${this.productId}-subscribed`) : '';
}
channelsWatcher(newValue) {
this.channels_ = !!newValue ? newValue.split(',') : [];
}
openModel() {
this.handleSubmitOptions().then(isSuccess => isSuccess ? this.modal.open() : null);
}
async submit() {
let payload = await this.handleSubmitOptions();
if (this.isUser) {
return salla.api.product.availabilitySubscribe(payload)
.then(() => this.isSubscribed = true);
}
if (this.channels_.includes('sms')) {
let { phone, countryCode } = await this.mobileInput.getValues();
payload['country_code'] = countryCode;
payload['phone'] = phone;
}
if (this.channels_.includes('email')) {
this.email.value !== '' && (payload['email'] = this.email.value);
}
await this.validateform();
return this.btn.load()
.then(() => this.btn.disable())
.then(() => salla.api.product.availabilitySubscribe(payload))
.then(() => {
if (!this.notifyOptionsAvailability) {
salla.storage.set(`product-${this.productId}-subscribed`, true);
this.isSubscribed = true;
return;
}
if (payload.options.length) {
let options = salla.storage.get(`product-${this.productId}-subscribed-options`) || [];
let selectedOptionsString = payload.options.join(',');
if (!options.includes(selectedOptionsString)) {
options.push(selectedOptionsString);
salla.storage.set(`product-${this.productId}-subscribed-options`, options);
this.isSubscribed = true;
}
else {
salla.log('already subscribed to this options');
}
}
})
.then(() => this.btn.stop())
.then(() => this.modal.close())
.catch(() => this.btn.stop() && this.btn.enable());
}
async validateform() {
try {
if (this.channels_.includes('email')) {
const isEmailValid = Helper.Helper.isValidEmail(this.email.value);
if (isEmailValid)
return;
!isEmailValid && this.validateField(this.email, salla.lang.get('common.elements.email_is_valid'));
}
if (this.channels_.includes('sms')) {
const isPhoneValid = await this.mobileInput.isValid();
if (isPhoneValid)
return;
}
}
catch (error) {
throw ('Please insert required fields');
}
}
validateField(field, errorMsg) {
field.classList.add('s-has-error');
field.nextElementSibling['innerText'] = '* ' + errorMsg;
}
render() {
return (index.h(index.Host, { key: 'aa627ba618b159913ad91efba99e57c14dd27832', class: "s-product-availability-wrap" }, this.isSubscribed || this.isVisitorSubscribed
? index.h("div", { class: "s-product-availability-subscribed" }, index.h("span", { innerHTML: bellRing.BellRing, class: "s-product-availability-subs-icon" }), salla.lang.get('pages.products.notify_availability_success'))
:
index.h("salla-button", { width: "wide", onClick: () => this.isUser ? this.submit() : this.openModel() }, salla.lang.get('pages.products.notify_availability')), this.isUser || this.isSubscribed || this.isVisitorSubscribed ? '' : this.renderModal()));
}
renderModal() {
return (index.h("salla-modal", { ref: modal => this.modal = modal, "modal-title": this.title_, subTitle: salla.lang.get('pages.products.notify_availability_subtitle'), width: "sm" }, index.h("span", { slot: 'icon', class: "s-product-availability-header-icon", innerHTML: bellRing.BellRing }), index.h("div", { class: "s-product-availability-body" }, this.channels_.includes('email') ? [
index.h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.email')),
index.h("input", { class: "s-product-availability-input", onKeyDown: e => this.typing(e), placeholder: salla.lang.get('common.elements.email_placeholder') || 'your@email.com', ref: el => this.email = el, type: "email" }),
index.h("span", { class: "s-product-availability-error-msg" })
] : '', this.channels_.includes('sms') ? [
index.h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.mobile')),
index.h("salla-tel-input", { ref: el => this.mobileInput = el, onKeyDown: e => this.typing(e) })
] : ''), index.h("div", { slot: "footer", class: "s-product-availability-footer" }, index.h("salla-button", { class: "modal-cancel-btn", width: "wide", color: "light", fill: "outline", onClick: () => this.modal.close() }, salla.lang.get('common.elements.cancel')), index.h("salla-button", { class: "submit-btn", "loader-position": 'center', width: "wide", ref: btn => this.btn = btn, onClick: () => this.submit() }, salla.lang.get('common.elements.submit')))));
}
get host() { return index.getElement(this); }
static get watchers() { return {
"channels": ["channelsWatcher"]
}; }
};
SallaProductAvailability.style = sallaProductAvailabilityCss;
var FileIcon = `<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>file-upload</title>
<path d="M21.333 24c0.341 0 0.683-0.131 0.943-0.391 0.521-0.521 0.521-1.364 0-1.885l-5.333-5.333c-0.123-0.123-0.271-0.22-0.433-0.288-0.327-0.135-0.693-0.135-1.019 0-0.163 0.068-0.311 0.165-0.433 0.288l-5.333 5.333c-0.521 0.521-0.521 1.364 0 1.885s1.364 0.521 1.885 0l3.057-3.057v10.115c0 0.736 0.597 1.333 1.333 1.333s1.333-0.597 1.333-1.333v-10.115l3.057 3.057c0.26 0.26 0.601 0.391 0.943 0.391zM28.943 9.724l-9.333-9.333c-0.249-0.251-0.589-0.391-0.943-0.391h-12c-2.205 0-4 1.795-4 4v24c0 2.205 1.795 4 4 4h4c0.736 0 1.333-0.597 1.333-1.333s-0.597-1.333-1.333-1.333h-4c-0.735 0-1.333-0.599-1.333-1.333v-24c0-0.735 0.599-1.333 1.333-1.333h11.448l8.552 8.552v16.781c0 0.735-0.599 1.333-1.333 1.333h-4c-0.736 0-1.333 0.597-1.333 1.333s0.597 1.333 1.333 1.333h4c2.205 0 4-1.795 4-4v-17.333c0-0.353-0.14-0.693-0.391-0.943z"></path>
</svg>
`;
const sallaProductOptionsCss = "";
const SallaProductOptions = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
this.changed = index.createEvent(this, "changed");
this.fileTypes = {
pdf: 'application/pdf',
png: 'image/png',
jpg: 'image/jpeg',
word: 'application/doc,application/ms-doc,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
exl: 'application/excel,application/vnd.ms-excel,application/x-excel,application/x-msexcel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
txt: 'text/plain',
};
this.outOfStockText = "";
this.donationAmount = salla.lang.get('pages.products.donation_amount');
this.selectDonationAmount = salla.lang.getWithDefault('pages.products.select_donation_amount', 'تحديد مبلغ التبرع');
this.selectAmount = salla.lang.getWithDefault('pages.products.select_amount', 'اختر المبلغ');
this.isCustomDonation = false;
this.selectedOptions = [];
this.canDisabled = false;
this.disableCardValue = true;
this.availableDigitalCardValues = [];
this.userInitiatedValidation = false;
this.isCartMode = false;
this.outSkus = [];
/**
* Avoid selection of previous default or selected card value option
* when switching between digital card country options for the 1st time
*/
this.ignoreDefaultCardValue = false;
/**
* The id of the product to which the options are going to be fetched for.
*/
this.productId = salla.config.get('page.id');
this.handleDonationOptions = (event, detail, type, _option) => {
// Donating-amount input only: onInput updates price and dispatches native change for form price watcher.
if (detail === 'custom' && type === 'input') {
const inputTarget = event.target;
salla.helpers.inputDigitsOnly(inputTarget);
salla.event.emit('product-options::donation-changed', {
id: this.productId,
price: inputTarget.value
});
inputTarget.dispatchEvent(new window.Event('change', { bubbles: true }));
return;
}
// Tab (radio) change: get value from the radio that fired.
const value = type === 'option' ? String(event.target?.value ?? '') : '';
if (type === 'option' && detail === 'custom') {
// "Custom" tab: stop propagation so form does not see change until user types in donating-amount input.
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
this.isCustomDonation = value === 'custom';
if (this.donationInput) {
if (value === 'custom') {
this.donationInput.value = '';
this.donationInput.focus();
}
else {
this.donationInput.value = value;
}
if (detail === 'custom') {
return;
}
salla.event.emit('product-options::donation-changed', {
id: this.productId,
price: value
});
}
};
this.hideLabel = (option) => {
if (option.type === interfaces.DisplayType.DONATION && (option.donation && !option.donation.can_donate)) {
return true;
}
return false;
};
this.getExpireDonationMessage = (option) => {
if (!option.donation) {
return;
}
const completed = option.donation.target_amount <= option.donation.collected_amount;
return index.h("div", { class: { "s-product-options-donation-message": true, "s-product-options-donation-completed": completed, "s-product-options-donation-expired": !completed } }, index.h("p", null, option.donation.target_message), index.h("span", { innerHTML: completed ? salla.money(option.donation.target_amount) : '' }));
};
salla.lang.onLoaded(() => {
this.outOfStockText = salla.lang.get("pages.products.out_of_stock");
this.donationAmount = salla.lang.get('pages.products.donation_amount');
this.selectDonationAmount = salla.lang.getWithDefault('pages.products.select_donation_amount', 'تحديد مبلغ التبرع');
this.selectAmount = salla.lang.getWithDefault('pages.products.select_amount', 'اختر المبلغ');
});
if (this.options) {
try {
this.setOptionsData(Array.isArray(this.options) ? this.options : JSON.parse(this.options));
return;
}
catch (e) {
salla.log('Bad json passed via options prop');
}
}
if (!Array.isArray(this.optionsData)) {
salla.log('Options is not an array[] ---> ', this.optionsData);
this.setOptionsData([]);
}
if (this.productId && !salla.url.is_page('cart')) {
salla.api.product.getDetails(this.productId, ['options']).then(resp => this.setOptionsData(resp.data.options));
}
}
/**
* Sets the options data for the product
* @param optionsData - Array of product options
*/
async setOptionsData(optionsData) {
this.optionsData = optionsData;
const that = this;
this.optionsData[0]?.details?.forEach(function (detail) {
Object.entries(detail.skus_availability || {})
.filter(sku => !sku[1])
.map(sku => that.outSkus.push(Number(sku[0])));
});
}
/**
* Get the current options state, including selected values/details.
*/
async getOptionsData() {
return this.optionsData;
}
/**
* Get the id's of the selected options.
* */
async getSelectedOptionsData() {
const selectedOptions = {};
const formData = this.host.getElementSallaData();
// Check if bundleContext is defined as a prop on the component before accessing it
const contextData = (typeof this.bundleContext !== 'undefined') ? this.bundleContext : null;
formData.forEach((value, key) => {
if (contextData) {
// Handle bundle naming convention: bundle[sectionId][index][options][optionId]
if (key.startsWith('bundle[') && key.includes('[options][')) {
const optionId = key.split('[options][')[1].replace(']', '');
selectedOptions[optionId] = value;
}
}
else {
// Handle standard naming convention: options[optionId]
if (key.startsWith('options[')) {
selectedOptions[key.replace('options[', '').replace(']', '')] = value;
}
}
});
return selectedOptions;
}
/**
* Report options form validity.
* */
async reportValidity() {
const requiredElements = this.host.querySelectorAll('[required]');
let pass = true;
for (let i = 0; i < requiredElements.length; i++) {
const el = requiredElements[i];
if (el.disabled || el.closest('.hidden')) {
continue;
}
//if there is only one invalid option, return false
if ('reportValidity' in el && !el.reportValidity()) {
pass = false;
}
}
return pass;
}
/**
* Return true if there is any out of stock options are selected and vise versa.
* */
async hasOutOfStockOption() {
return this.selectedOptions.some(option => option.is_out) || (this.selectedSkus?.length && this.selectedSkus?.every(sku => this.outSkus.includes(sku)));
}
/**
* Get selected options.
* */
async getSelectedOptions() {
return this.selectedOptions;
}
/**
* Get a specific option by its id.
* */
async getOption(option_id) {
return this.optionsData.find(option => option.id === option_id);
}
// @ts-ignore
invalidHandler(event, option) {
const closestProductOption = event.target.closest('.s-product-options-option');
if (!closestProductOption.classList.contains('s-product-options-option-error')) {
closestProductOption.classList.add('s-product-options-option-error');
}
if ((this.userInitiatedValidation || salla.helpers.isIOSDevice()) && !salla.url.is_page('cart')) {
const firstInvalidElement = this.host.querySelector('.s-product-options-option-error');
if (firstInvalidElement === closestProductOption) {
this.scrollToElement(closestProductOption);
}
}
}
scrollToElement(element) {
if (!element)
return;
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
changedHandler(event, option, fireChangeEvent = true) {
const data = {
event: event,
option: option,
detail: null,
productId: this.productId
};
if (option.details) {
const detail = option.details.find((detail) => {
return Number(detail.id) === Number(event.target.value);
});
data.detail = detail;
}
if (option.type === 'country') {
this.handleCountryOptionChange(event, data.detail);
}
const optionElement = event.target.closest('.s-product-options-option');
if (event.target.value ||
((option.type === interfaces.DisplayType.FILE || option.type === interfaces.DisplayType.IMAGE) && event.type === 'added') ||
(option.type === interfaces.DisplayType.MAP && event.type === 'selected' && (event.target.lat && event.target.lng))) {
setTimeout(() => {
optionElement.classList.remove('s-product-options-option-error');
}, 200);
}
if (option.type === interfaces.DisplayType.DONATION) {
salla.event.emit('product-options::donation-changed', {
id: this.productId,
price: event.target.value
});
}
this.setSelectedSkus();
this.handleRequiredMultipleOptions(option);
const index = this.selectedOptions.findIndex(opt => opt.option_id === data.option.id);
if (data.option.type === interfaces.DisplayType.MULTIPLE_OPTIONS) {
// Handle multiple selections
const detailIndex = this.selectedOptions.findIndex(opt => opt.option_id === data.option.id && opt?.id === data.detail?.id);
if (detailIndex > -1) {
// If the option is already selected, remove it (unselect)
this.selectedOptions.splice(detailIndex, 1);
}
else {
// If the option is not selected, add it to the selectedOptions array
this.selectedOptions.push({ ...data.detail, option_id: data.option.id });
}
}
else {
// Handle single selection
if (!data.detail || Object.keys(data.detail).length === 0) {
// If there is no value for the single-select, remove it from the selectedOptions array
if (index > -1) {
this.selectedOptions.splice(index, 1);
}
}
else {
// If a value exists, update or add the selection
if (index > -1) {
// Replace the existing selection with the new one
this.selectedOptions[index] = { ...data.detail, option_id: data.option.id };
}
else {
// If no selection exists for this input, add the new selection
this.selectedOptions.push({ ...data.detail, option_id: data.option.id });
}
}
}
// Update optionsData directly
this.optionsData = this.optionsData.map(opt => {
if (opt.id === data.option.id) {
return {
...opt,
details: opt.details.map(detail => ({
...detail,
is_selected: data.option.type === interfaces.DisplayType.MULTIPLE_OPTIONS
? this.selectedOptions.some(selected => selected.id === detail.id)
: Number(detail.id) === Number(data.detail?.id),
value: data.detail?.value
}))
};
}
return opt;
});
this.pruneInvisibleConditionalSelections();
this.setSelectedSkus();
requestAnimationFrame(() => {
this.optionsData
?.filter(o => o.type === interfaces.DisplayType.MULTIPLE_OPTIONS && o.required)
.forEach(o => this.handleRequiredMultipleOptions(o));
});
// Emit the event only if fireChangeEvent is true
if (fireChangeEvent) {
this.changed.emit(data);
salla.event.emit('product-options::change', data);
}
}
/**
* loop throw all selected details, then get common sku, if it's only one, means we selected all of them;
*/
setSelectedSkus() {
this.selectedSkus = this.selectedOptions.map(detail => Object.keys(detail.skus_availability || {}))
.reduce((p, c) => p.filter(e => c.includes(e)), []) // Initialize accumulator as an empty array
.map(sku => Number(sku));
}
handleRequiredMultipleOptions(option) {
if (option.type !== interfaces.DisplayType.MULTIPLE_OPTIONS || !option.required) {
return;
}
const optionContainer = this.host.querySelector(`[data-option-id="${option.id}"]`);
if (!optionContainer) {
return;
}
const hasChecked = optionContainer.querySelectorAll('input:checked').length;
optionContainer.querySelectorAll('input').forEach(input => input.toggleAttribute('required', !hasChecked));
}
pruneInvisibleConditionalSelections() {
const passes = Math.max(1, this.optionsData.length);
for (let p = 0; p < passes; p++) {
let clearedSomething = false;
for (const opt of this.optionsData) {
const vc = opt.visibility_condition;
if (!vc) {
continue;
}
const selectedAtParent = this.selectedOptions
.filter(s => s.option_id === vc.option)
.map(s => String(s.id));
const targetSelected = selectedAtParent.includes(String(vc.value));
const shouldShow = vc.operator === '=' ? targetSelected : !targetSelected;
if (shouldShow) {
continue;
}
if (this.clearStaleOption(opt.id)) {
clearedSomething = true;
}
}
if (!clearedSomething) {
break;
}
}
}
clearStaleOption(optionId) {
const opt = this.optionsData.find(o => o.id === optionId);
if (!opt) {
return false;
}
const beforeLen = this.selectedOptions.length;
this.selectedOptions = this.selectedOptions.filter(s => s.option_id !== optionId);
const removedFromSelected = this.selectedOptions.length !== beforeLen;
const hasValue = opt.value != null && String(opt.value).length > 0;
const hadSelectedDetail = opt.details?.some(d => d.is_selected) ?? false;
if (!removedFromSelected && !hasValue && !hadSelectedDetail) {
return false;
}
this.optionsData = this.optionsData.map(o => o.id !== optionId
? o
: {
...o,
value: undefined,
details: o.details?.map(d => ({ ...d, is_selected: false })) ?? o.details,
});
return true;
}
getLatLng(value, type) {
return value
? value.split(',')[type === 'lat' ? 0 : 1]
: '';
}
getDisplayForType(option) {
if (this[`${option.type}Option`]) {
return this[`${option.type}Option`](option);
}
if (option.type === interfaces.DisplayType.COLOR_PICKER) {
return this.colorPickerOption(option);
}
if (option.type === interfaces.DisplayType.MULTIPLE_OPTIONS) {
return this.multipleOptions(option);
}
if (option.type === interfaces.DisplayType.SINGLE_OPTION) {
return this.singleOption(option);
}
// Handle radio type as single option for bundle products
if (option.type === interfaces.DisplayType.RADIO) {
return this.radioOption(option);
}
if (option.type === interfaces.DisplayType.DIGITAL_CARD_VALUE) {
return this.digitalCardValuesOption(option);
}
if (option.type === interfaces.DisplayType.COUNTRY) {
return this.countryOption(option);
}
if (option.type === interfaces.DisplayType.BOOKING && salla.url.is_page("cart")) {
return index.h("salla-booking-field", { onInvalidInput: (e) => this.invalidHandler(e, option), option: option, productId: option.value });
}
salla.log(`Couldn't find options type(${option.type})😢`);
return '';
}
getOptionShownWhen(option) {
return option.visibility_condition
? { "data-show-when": `options[${option.visibility_condition.option}] ${option.visibility_condition.operator} ${option.visibility_condition.value}` }
: {};
}
getAvailableDigitalCardSKUs(detail) {
const digitalCardOption = this.optionsData.find(({ type }) => type === 'digital-code-value');
if (!digitalCardOption)
throw new Error('product-options:: No digital card options found');
const outofStockSKUs = Object.keys(detail.skus_availability).filter(key => detail.skus_availability[key] === false);
this.availableDigitalCardValues = digitalCardOption.details.filter((op) => {
return !Object.keys(op.skus_availability).filter(SKU_key => outofStockSKUs.includes(SKU_key)).length;
});
}
handleCountryOptionChange(event, detail) {
event.stopImmediatePropagation();
this.ignoreDefaultCardValue = true;
const currentCardValue = this.host.querySelector("input[data-code-value]:checked");
if (currentCardValue)
currentCardValue.checked = false;
const digitalCardOption = this.optionsData.find(({ type }) => type ===