UNPKG

@salla.sa/twilight-components

Version:
428 lines (421 loc) 21.4 kB
/*! * Crafted with ❤ by Salla */ 'use strict'; var index = require('./index-uoA36zqH.js'); const sallaMultipleBundleProductCartCss = ""; const SallaMultipleBundleProductCart = class { constructor(hostRef) { index.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}&section_id=${sectionId}`) .then(() => selectedAccordion?.remove()); } } } renderRemoveButton(sectionId, product, isText = false) { return (index.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') : index.h("i", { class: "sicon-cancel" }))); } renderAccordionHeader(sectionId, product) { const hasOptions = product?.options && product?.options?.length > 0; // undefined or empty array return (index.h("div", { slot: "html", class: "s-multiple-bundle-product-cart-header-wrapper" }, index.h("div", { class: `s-multiple-bundle-product-cart-header ${hasOptions ? '' : 's-multiple-bundle-product-cart-header-no-options'}` }, index.h("div", { class: "s-multiple-bundle-product-cart-header-content" }, index.h("a", { href: product?.url, class: "s-multiple-bundle-product-cart-header-image-wrapper" }, index.h("img", { src: product?.image?.url, alt: product?.image?.alt || product?.name, class: "s-multiple-bundle-product-cart-header-image" })), index.h("div", { class: "s-multiple-bundle-product-cart-header-content-details" }, index.h("h2", { class: "s-multiple-bundle-product-cart-header-content-details-title" }, index.h("a", { href: product?.url, class: "s-multiple-bundle-product-cart-header-content-details-title-link" }, product?.name)), index.h("div", { class: "s-multiple-bundle-product-cart-header-content-details-price" }, index.h("span", { class: "s-multiple-bundle-product-cart-header-content-details-price-regular" }, index.h("span", { innerHTML: product?.price ? salla.money(product?.price) : '' })), product?.sale_price > 0 && (index.h("span", { class: "s-multiple-bundle-product-cart-header-content-details-price-sale" }, index.h("span", { innerHTML: salla.money(product?.sale_price) })))), product?.quantity_in_group > 0 && product?.quantity !== 0 && (index.h("p", { class: "s-multiple-bundle-product-cart-header-content-details-quantity" }, index.h("span", null, salla.lang.get('pages.products.number_of_pieces')), index.h("span", null, product?.quantity_in_group))))), !hasOptions && (index.h("div", { class: "s-multiple-bundle-product-cart-header-remove-button" }, this.renderRemoveButton(sectionId, product, false)))))); } render() { return (index.h(index.Host, { key: 'd11f8075e15cd1379d6754b5ce13c81fa7cf9f03', class: "s-multiple-bundle-product-wrapper" }, index.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 (index.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}` }, index.h("salla-accordion-head", null, this.renderAccordionHeader(String(section.id), product)), product.options && product.options.length > 0 && (index.h("salla-accordion-body", null, index.h("salla-product-options", { options: JSON.stringify(product.options), key: `${product.id}-persistent`, "product-id": product.id, "bundle-context": JSON.stringify(bundleContext) }), index.h("div", { class: "s-multiple-bundle-product-cart-body-remove-button" }, this.renderRemoveButton(String(section.id), product, true)))))); }); })))); } get host() { return index.getElement(this); } }; SallaMultipleBundleProductCart.style = sallaMultipleBundleProductCartCss; const sallaMultipleBundleProductDetailsCss = ""; const SallaMultipleBundleProductDetails = class { constructor(hostRef) { index.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 (index.h(index.Fragment, null, index.h("h2", { slot: "title" }, section?.name), selectionNote ? index.h("span", { slot: "note" }, selectionNote) : null, index.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 (index.h(index.Host, { key: '74b123c432e77263cf0ddcf20ab742885c54d5f4', class: "s-multiple-bundle-product-wrapper" }, index.h("div", { key: '24bef0958a2a7d29bb4485eac08c7e6b8614a00d', class: "s-multiple-bundle-product-wrapper-sections" }, this.sections.map((section, index$1) => { return (index.h("salla-accordion", { key: section.id, collapsed: index$1 === 0 ? false : true }, index.h("salla-accordion-head", null, this.renderAccordionHeader(section)), index.h("salla-accordion-body", null, index.h("salla-multiple-bundle-product-slider", { section: section, sectionIndex: index$1, selectedProducts: this.selectedProducts, selectionLimit: this.getEffectiveMax(section), isSelectionLocked: this.isProductSelectionLocked(section), onProductSelected: this.handleBundleSliderProductSelected, onProductOptionsSelected: this.handleBundleSliderProductOptionsSelected })))); })), index.h("salla-multiple-bundle-product-options-modal", { key: '556b4e51a4b79dfc00371a07296556fb74111524' }))); } get host() { return index.getElement(this); } static get watchers() { return { "sections": ["handleSectionsChange"] }; } }; SallaMultipleBundleProductDetails.style = sallaMultipleBundleProductDetailsCss; exports.salla_multiple_bundle_product_cart = SallaMultipleBundleProductCart; exports.salla_multiple_bundle_product_details = SallaMultipleBundleProductDetails;