@salla.sa/twilight-components
Version:
Salla Web Component
425 lines (419 loc) • 21.2 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { r as registerInstance, h, H as Host, a as getElement, F as Fragment } from './index-BHYtfMwX.js';
const sallaMultipleBundleProductCartCss = "";
const SallaMultipleBundleProductCart = class {
constructor(hostRef) {
registerInstance(this, hostRef);
/** The list of sections belonging to a bundle product. */
this.sections = [];
this.itemNumber = '';
}
deleteItem(sectionId, product) {
const form = this.host.closest('form');
if (form) {
const formId = form.getAttribute('id');
if (formId && typeof formId === 'string') {
const itemNumber = formId.match(/item-(\d+)/)?.[1];
this.itemNumber = itemNumber || '';
const selectedAccordion = this.host.querySelector(`#accordion-${product.id}`);
salla.cart
.deleteItem(`${this.itemNumber}?product_id=${product.id}§ion_id=${sectionId}`)
.then(() => selectedAccordion?.remove());
}
}
}
renderRemoveButton(sectionId, product, isText = false) {
return (h("salla-button", { type: "button", shape: isText ? 'btn' : 'icon', fill: isText ? 'outline' : 'solid', size: "small", color: "danger", "aria-label": "Remove from the cart", onClick: () => this.deleteItem(sectionId, product) }, isText ? salla.lang.get('common.elements.delete') : h("i", { class: "sicon-cancel" })));
}
renderAccordionHeader(sectionId, product) {
const hasOptions = product?.options && product?.options?.length > 0; // undefined or empty array
return (h("div", { slot: "html", class: "s-multiple-bundle-product-cart-header-wrapper" }, h("div", { class: `s-multiple-bundle-product-cart-header ${hasOptions ? '' : 's-multiple-bundle-product-cart-header-no-options'}` }, h("div", { class: "s-multiple-bundle-product-cart-header-content" }, h("a", { href: product?.url, class: "s-multiple-bundle-product-cart-header-image-wrapper" }, h("img", { src: product?.image?.url, alt: product?.image?.alt || product?.name, class: "s-multiple-bundle-product-cart-header-image" })), h("div", { class: "s-multiple-bundle-product-cart-header-content-details" }, h("h2", { class: "s-multiple-bundle-product-cart-header-content-details-title" }, h("a", { href: product?.url, class: "s-multiple-bundle-product-cart-header-content-details-title-link" }, product?.name)), h("div", { class: "s-multiple-bundle-product-cart-header-content-details-price" }, h("span", { class: "s-multiple-bundle-product-cart-header-content-details-price-regular" }, h("span", { innerHTML: product?.price ? salla.money(product?.price) : '' })), product?.sale_price > 0 && (h("span", { class: "s-multiple-bundle-product-cart-header-content-details-price-sale" }, h("span", { innerHTML: salla.money(product?.sale_price) })))), product?.quantity_in_group > 0 && product?.quantity !== 0 && (h("p", { class: "s-multiple-bundle-product-cart-header-content-details-quantity" }, h("span", null, salla.lang.get('pages.products.number_of_pieces')), h("span", null, product?.quantity_in_group))))), !hasOptions && (h("div", { class: "s-multiple-bundle-product-cart-header-remove-button" }, this.renderRemoveButton(sectionId, product, false))))));
}
render() {
return (h(Host, { key: 'd11f8075e15cd1379d6754b5ce13c81fa7cf9f03', class: "s-multiple-bundle-product-wrapper" }, h("div", { key: '293351d69f5e12039ecab876832c6164b1235dab', class: "s-multiple-bundle-product-wrapper-sections" }, this.sections.map((section, sectionIndex) => {
return section.products.map(product => {
const bundleContext = {
sectionId: section.id,
sectionIndex: sectionIndex,
productId: product.id,
};
return (h("salla-accordion", { key: product.id, collapsed: false, bordered: true, collapsible: product.options && product.options.length > 0 ? true : false, size: "sm", id: `accordion-${product.id}` }, h("salla-accordion-head", null, this.renderAccordionHeader(String(section.id), product)), product.options && product.options.length > 0 && (h("salla-accordion-body", null, h("salla-product-options", { options: JSON.stringify(product.options), key: `${product.id}-persistent`, "product-id": product.id, "bundle-context": JSON.stringify(bundleContext) }), h("div", { class: "s-multiple-bundle-product-cart-body-remove-button" }, this.renderRemoveButton(String(section.id), product, true))))));
});
}))));
}
get host() { return getElement(this); }
};
SallaMultipleBundleProductCart.style = sallaMultipleBundleProductCartCss;
const sallaMultipleBundleProductDetailsCss = "";
const SallaMultipleBundleProductDetails = class {
constructor(hostRef) {
registerInstance(this, hostRef);
/** The list of sections belonging to a bundle product. */
this.sections = [];
// store selected product IDs per section (can be string or number)
this.selectedProducts = {};
// Event handler reference for cleanup
this.productSelectedHandler = null;
// handle selecting a product (toggle)
this.onSelectProduct = (sectionId, product) => {
const productId = product.id;
const wasSelected = this.selectedProducts[sectionId]?.has(productId) ?? false;
const section = this.sections.find(s => s.id == sectionId);
if (wasSelected && section && this.isProductSelectionLocked(section)) {
return;
}
if (!wasSelected) {
if (!section) {
return;
}
if (this.getEffectiveMax(section) !== 1 && this.isAtSelectionLimit(section)) {
return;
}
}
this.selectedProducts = {
...this.selectedProducts,
[sectionId]: new Set(this.selectedProducts[sectionId] || []),
};
if (wasSelected) {
this.selectedProducts[sectionId].delete(productId);
this.clearProductFormData(productId, sectionId);
this.clearProductModalOptions(productId, sectionId);
}
else {
const effectiveMax = this.getEffectiveMax(section);
if (effectiveMax === 1) {
this.syncSectionSelection(sectionId, productId);
}
else {
this.selectedProducts[sectionId].add(productId);
}
}
// force re-render
this.selectedProducts = { ...this.selectedProducts };
// still dispatch event
salla.event.dispatch('on-bundle-product-selected', {
id: product.id,
name: product.name,
options: product.options,
wasSelected: wasSelected,
isSelected: !wasSelected,
});
};
// ensure product is selected (only add if not already selected)
this.ensureProductSelected = (sectionId, product) => {
this.selectedProducts = {
...this.selectedProducts,
[sectionId]: new Set(this.selectedProducts[sectionId] || []),
};
const productId = product.id;
if (this.selectedProducts[sectionId].has(productId)) {
return;
}
const section = this.sections.find(s => s.id == sectionId);
if (!section) {
return;
}
const effectiveMax = this.getEffectiveMax(section);
if (effectiveMax === 1) {
this.syncSectionSelection(sectionId, productId);
}
else if (effectiveMax > 1 && this.isAtSelectionLimit(section)) {
return;
}
else {
this.selectedProducts[sectionId].add(productId);
}
this.selectedProducts = { ...this.selectedProducts };
salla.event.dispatch('on-bundle-product-selected', {
id: product.id,
name: product.name,
options: product.options,
});
};
// open product options modal
this.onSelectProductOptions = (product, sectionId) => {
const section = this.sections.find(s => s.id == sectionId);
if (!section || !this.canSelectProductInSection(section, product.id)) {
return;
}
const sectionIndex = this.sections.findIndex(s => s.id == sectionId);
const productIndex = section.products?.findIndex(p => p.id == product.id) ?? 0;
const isProductAlreadySelected = this.selectedProducts[sectionId]?.has(product.id) ?? false;
salla.event.dispatch('multiple-bundle-product-modal::open', {
product,
sectionId,
sectionIndex,
productIndex,
isProductAlreadySelected,
});
};
// Event handlers for bundle slider component
this.handleBundleSliderProductSelected = (event) => {
const { product, sectionId } = event.detail;
this.onSelectProduct(sectionId, product);
};
this.handleBundleSliderProductOptionsSelected = (event) => {
const { product, sectionId } = event.detail;
this.onSelectProductOptions(product, sectionId);
};
}
isSingleProductSection(section) {
return (section.products?.length ?? 0) === 1;
}
getObligatoryMin(section) {
const min = section.obligatory_products;
if (min == null) {
return 0;
}
const numericMin = Number(min);
return Number.isNaN(numericMin) ? 0 : numericMin;
}
isProductSelectionLocked(section) {
return this.isSingleProductSection(section) && this.getObligatoryMin(section) === 1;
}
/** Empty max → cap is the number of products in the section. */
getEffectiveMax(section) {
const productCount = section.products?.length ?? 0;
const max = section.max_obligatory_products;
if (max != null && max > 0) {
return Math.min(max, productCount);
}
return productCount;
}
getSelectedCount(sectionId) {
return this.selectedProducts[sectionId]?.size ?? 0;
}
isAtSelectionLimit(section) {
return this.getSelectedCount(section.id) >= this.getEffectiveMax(section);
}
canSelectProductInSection(section, productId) {
if (this.selectedProducts[section.id]?.has(productId)) {
return true;
}
if (this.getEffectiveMax(section) === 1) {
return true;
}
return !this.isAtSelectionLimit(section);
}
async canSelectBundleProduct(sectionId, productId) {
const section = this.sections.find(s => s.id == sectionId);
if (!section) {
return false;
}
return this.canSelectProductInSection(section, productId);
}
queryProductCheckbox(sectionId, productIndex) {
const selector = `input.s-multiple-bundle-product-checkbox[name="bundle[${sectionId}][${productIndex}][id]"]`;
const form = this.host.closest('form');
const fromForm = form?.querySelector(selector);
if (fromForm) {
return fromForm;
}
for (const slider of this.host.querySelectorAll('salla-multiple-bundle-product-slider')) {
const root = slider.shadowRoot ?? slider;
const input = root.querySelector(selector);
if (input) {
return input;
}
}
return null;
}
dispatchBubblingChange(target) {
requestAnimationFrame(() => {
target.dispatchEvent(new window.Event('change', { bubbles: true }));
});
}
/** Uncheck checkbox, remove form inputs, and reset modal state for one product slot. */
clearSectionProductSlot(sectionId, productId, productIndex) {
const checkbox = this.queryProductCheckbox(sectionId, productIndex);
if (checkbox) {
checkbox.checked = false;
}
const form = this.host.closest('form');
if (form) {
const slotPrefix = `bundle[${sectionId}][${productIndex}]`;
Array.from(form.querySelectorAll('input')).forEach((input) => {
const inSlot = input.name?.startsWith(slotPrefix) ?? false;
if (!inSlot) {
return;
}
if (input.type === 'checkbox' && input.name?.endsWith('][id]')) {
input.checked = false;
return;
}
input.remove();
});
}
this.clearProductModalOptions(productId, sectionId);
}
/** max=1: replace section selection and keep DOM/form in sync. */
syncSectionSelection(sectionId, productId) {
const section = this.sections.find(s => s.id == sectionId);
section?.products?.forEach((product, productIndex) => {
if (product.id != productId) {
this.clearSectionProductSlot(sectionId, product.id, productIndex);
}
});
this.selectedProducts[sectionId] = new Set([productId]);
const selectedIndex = section?.products?.findIndex(p => p.id == productId) ?? -1;
const checkbox = selectedIndex >= 0 ? this.queryProductCheckbox(sectionId, selectedIndex) : null;
if (checkbox) {
checkbox.checked = true;
this.dispatchBubblingChange(checkbox);
}
}
autoSelectSingleProductSections() {
if (!this.sections?.length) {
return;
}
const newSelectedProducts = { ...this.selectedProducts };
let updated = false;
const selectedEvents = [];
for (const section of this.sections) {
if (!this.isProductSelectionLocked(section)) {
continue;
}
const product = section.products?.[0];
if (!product || (!product.unlimited_quantity && (product.quantity ?? 0) <= 0)) {
continue;
}
const sectionId = section.id;
const currentSet = new Set(newSelectedProducts[sectionId] || []);
if (currentSet.has(product.id)) {
continue;
}
currentSet.add(product.id);
newSelectedProducts[sectionId] = currentSet;
updated = true;
selectedEvents.push({ product });
}
if (!updated) {
return;
}
this.selectedProducts = newSelectedProducts;
selectedEvents.forEach(({ product }) => {
salla.event.dispatch('on-bundle-product-selected', {
id: product.id,
name: product.name,
options: product.options,
});
});
}
// Clear form data for a specific product in specific section
clearProductFormData(productId, sectionId) {
if (sectionId != null) {
const section = this.sections.find(s => s.id == sectionId);
const productIndex = section?.products?.findIndex(product => product.id == productId) ?? -1;
if (productIndex >= 0) {
this.clearSectionProductSlot(sectionId, productId, productIndex);
return;
}
}
const form = this.host.closest('form');
if (!form) {
return;
}
const inputsToRemove = form.querySelectorAll(`[data-product-id="${productId}"]`);
inputsToRemove.forEach(input => input.remove());
}
// Clear modal options state for a specific product
clearProductModalOptions(productId, sectionId) {
let sectionIndex = null;
let productIndex = null;
if (sectionId != null) {
sectionIndex = this.sections.findIndex(section => section.id == sectionId);
if (sectionIndex > -1) {
const section = this.sections[sectionIndex];
if (section) {
const foundIndex = section.products?.findIndex(product => product.id == productId);
productIndex = typeof foundIndex === 'number' && foundIndex > -1 ? foundIndex : null;
}
}
}
// Emit event to notify modal to reset its state for this product
salla.event.dispatch('multiple-bundle-product-modal::clear-options', {
productId,
sectionId,
sectionIndex,
productIndex,
});
}
getProgressStatus(section) {
const selectedCount = this.getSelectedCount(section.id);
const effectiveMax = this.getEffectiveMax(section);
if (effectiveMax > 0) {
return `${selectedCount}/${effectiveMax}`;
}
return '0';
}
getSectionSelectionNote(section) {
const min = this.getObligatoryMin(section);
const max = Number(section.max_obligatory_products) || 0;
const hasMin = min > 0;
const hasMax = max > 0;
if (!hasMin && !hasMax) {
return null;
}
if (hasMin && hasMax) {
if (min === max) {
return salla.lang.getWithDefault('pages.products.bundle_select_exact', `اختر ${min} منتجات`, { count: min });
}
return salla.lang.getWithDefault('pages.products.bundle_select_range', `اختر من ${min} إلى ${max} منتجات`, { min, max });
}
if (hasMin) {
return salla.lang.getWithDefault('pages.products.bundle_select_min', `اختر على الأقل ${min} منتجات`, { min });
}
return salla.lang.getWithDefault('pages.products.bundle_select_max', `اختر حتى ${max} منتجات`, { max });
}
handleSectionsChange() {
this.autoSelectSingleProductSections();
}
componentWillLoad() {
this.autoSelectSingleProductSections();
}
renderAccordionHeader(section) {
const selectionNote = this.getSectionSelectionNote(section);
return (h(Fragment, null, h("h2", { slot: "title" }, section?.name), selectionNote ? h("span", { slot: "note" }, selectionNote) : null, h("span", { slot: "progress" }, this.getProgressStatus(section))));
}
componentDidLoad() {
// Listen for product selected event from modal
const modal = this.host.querySelector('salla-multiple-bundle-product-options-modal');
if (modal) {
this.productSelectedHandler = (e) => {
const { productId, sectionId, product, fromModal } = e.detail;
if (fromModal) {
// When called from modal, only add to selection if not already selected
this.ensureProductSelected(sectionId, product || { id: productId });
}
else {
// Normal toggle behavior
this.onSelectProduct(sectionId, product || { id: productId });
}
};
modal.addEventListener('productSelected', this.productSelectedHandler);
}
}
disconnectedCallback() {
// Clean up event listener to prevent memory leaks
if (this.productSelectedHandler) {
const modal = this.host.querySelector('salla-multiple-bundle-product-options-modal');
if (modal) {
modal.removeEventListener('productSelected', this.productSelectedHandler);
}
this.productSelectedHandler = null;
}
}
render() {
return (h(Host, { key: '74b123c432e77263cf0ddcf20ab742885c54d5f4', class: "s-multiple-bundle-product-wrapper" }, h("div", { key: '24bef0958a2a7d29bb4485eac08c7e6b8614a00d', class: "s-multiple-bundle-product-wrapper-sections" }, this.sections.map((section, index) => {
return (h("salla-accordion", { key: section.id, collapsed: index === 0 ? false : true }, h("salla-accordion-head", null, this.renderAccordionHeader(section)), h("salla-accordion-body", null, h("salla-multiple-bundle-product-slider", { section: section, sectionIndex: index, selectedProducts: this.selectedProducts, selectionLimit: this.getEffectiveMax(section), isSelectionLocked: this.isProductSelectionLocked(section), onProductSelected: this.handleBundleSliderProductSelected, onProductOptionsSelected: this.handleBundleSliderProductOptionsSelected }))));
})), h("salla-multiple-bundle-product-options-modal", { key: '556b4e51a4b79dfc00371a07296556fb74111524' })));
}
get host() { return getElement(this); }
static get watchers() { return {
"sections": ["handleSectionsChange"]
}; }
};
SallaMultipleBundleProductDetails.style = sallaMultipleBundleProductDetailsCss;
export { SallaMultipleBundleProductCart as salla_multiple_bundle_product_cart, SallaMultipleBundleProductDetails as salla_multiple_bundle_product_details };