Aucun produit disponible pour le moment
Restez à l'écoute ! D'autres produits seront affichés ici au fur et à mesure qu'ils seront ajoutés.
(function () { console.info('[stickerconfigurator] configurator-164.js v2.1.9 — fix TDZ mesure hauteur'); const roots = document.querySelectorAll('#sc-configurator'); roots.forEach((item, index) => { if (index > 0) { const shell = item.closest('.sc-page-shell'); (shell || item).remove(); } }); const fallbackConfig = { fonts: [ { name: 'Arial', value: 'Arial, Helvetica, sans-serif', category: 'sans' }, { name: 'Impact', value: 'Impact, Haettenschweiler, sans-serif', category: 'display' } ], colors: [ { name: 'Noir', value: '#111111' }, { name: 'Rouge', value: '#d92727' } ], materials: [ { name: 'Vinyle brillant', value: '1', price: 0 } ], priceRules: [ { min: 0, max: 0.1, price: 100 }, { min: 0.11, max: 0.2, price: 100 } ], logos: [], options: { enableTextMode: true, enableLogoMode: false, enableStickerType: false, enableMultiline: false, enableStencil: true, enableMirror: true, enableInside: true, minWidth: 5, maxWidth: 300, minHeight: 2, maxHeight: 120 } }; function getGlobalConfig() { return window.stickerConfiguratorConfig || null; } function needsDarkContrast(color) { const value = String(color || '').trim(); const normalized = value.toLowerCase(); const shortHex = value.match(/^#([0-9a-f]{3})$/i); const fullHex = value.match(/^#([0-9a-f]{6})$/i); const rgb = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); let red; let green; let blue; if (normalized === 'white' || normalized === 'blanc') { return true; } if (shortHex) { red = parseInt(shortHex[1][0] + shortHex[1][0], 16); green = parseInt(shortHex[1][1] + shortHex[1][1], 16); blue = parseInt(shortHex[1][2] + shortHex[1][2], 16); } else if (fullHex) { red = parseInt(fullHex[1].slice(0, 2), 16); green = parseInt(fullHex[1].slice(2, 4), 16); blue = parseInt(fullHex[1].slice(4, 6), 16); } else if (rgb) { red = Number(rgb[1]); green = Number(rgb[2]); blue = Number(rgb[3]); } else { return false; } return (red * 0.299 + green * 0.587 + blue * 0.114) >= 205; } function applyPreviewContrast(element, color) { if (element) { element.classList.toggle('sc-dark-contrast', needsDarkContrast(color)); } } function pageLooksLikeProduct() { return document.body.id === 'product' || document.body.classList.contains('page-product') || !!document.querySelector('input[name="id_product"]'); } function detectProductId() { const inputValue = Number(document.querySelector('input[name="id_product"]')?.value || 0); if (inputValue) { return inputValue; } const classMatch = document.body.className.match(/product-id-(\d+)/); if (classMatch) { return Number(classMatch[1]); } return Number(window.stickerConfiguratorProductId || 0); } function autoMountConfigurator() { if (!pageLooksLikeProduct()) { return; } const target = findConfiguratorMountTarget(); if (!target) { return; } const shell = document.createElement('div'); shell.className = 'sc-page-shell sc-page-shell-auto'; shell.innerHTML = ` `; const data = shell.querySelector('#sc-config-data'); data.textContent = JSON.stringify(getGlobalConfig() || fallbackConfig); insertConfiguratorShell(shell); } function findConfiguratorMountTarget() { return document.querySelector('.cc-configurator-wrapper') || document.querySelector('.cc-preview-area') || document.querySelector('.tabs') || document.querySelector('.product-container') || document.querySelector('#main') || document.querySelector('main'); } function insertConfiguratorShell(shell) { const themeWrapper = document.querySelector('.cc-configurator-wrapper'); if (themeWrapper) { themeWrapper.replaceChildren(shell); return; } const themePreview = document.querySelector('.cc-preview-area'); if (themePreview) { themePreview.replaceChildren(shell); return; } const tabs = document.querySelector('.tabs'); if (tabs && tabs.parentElement) { tabs.insertAdjacentElement('beforebegin', shell); return; } const productContainer = document.querySelector('.product-container'); if (productContainer && productContainer.parentElement) { productContainer.insertAdjacentElement('afterend', shell); return; } const main = document.querySelector('#main') || document.querySelector('main'); if (main) { main.append(shell); } } function relocateConfigurator() { if (!pageLooksLikeProduct()) { return; } const currentRoot = document.querySelector('#sc-configurator'); if (!currentRoot) { return; } const shell = currentRoot.closest('.sc-page-shell') || currentRoot; const tabs = document.querySelector('.tabs'); if (tabs && tabs.parentElement && shell.nextElementSibling !== tabs) { tabs.insertAdjacentElement('beforebegin', shell); } } function tidyProductPageAroundConfigurator() { if (!pageLooksLikeProduct()) { return; } const quantity = document.querySelector('#quantity_wanted'); const nativeQty = quantity ? quantity.closest('.qty') : document.querySelector('.product-quantity .qty'); if (nativeQty) { nativeQty.style.display = 'none'; nativeQty.setAttribute('data-sc-hidden-native-qty', '1'); } document.querySelectorAll('.product-buybox, .product-actions, #add-to-cart-or-refresh, .product-add-to-cart, .wishlist-button-add').forEach((buyElement) => { buyElement.style.display = 'none'; buyElement.setAttribute('data-sc-hidden-native-buybox', '1'); }); document.querySelectorAll('.product-customization, .js-product-customization').forEach((customization) => { customization.style.display = 'none'; customization.setAttribute('data-sc-hidden-native-customization', '1'); }); document.querySelectorAll('.product-variants, .js-product-variants').forEach((variants) => { if (!variants.textContent.trim()) { variants.style.display = 'none'; variants.setAttribute('data-sc-hidden-empty-variants', '1'); } }); document.querySelectorAll('.social-sharing').forEach((sharing) => { sharing.style.display = 'none'; sharing.setAttribute('data-sc-hidden-social-sharing', '1'); }); document.querySelectorAll('.images-container .product-cover').forEach((cover) => { cover.style.display = 'none'; cover.setAttribute('data-sc-hidden-native-cover', '1'); }); const reassurance = document.querySelector('.blockreassurance_product'); const shell = document.querySelector('.sc-page-shell'); const tabs = document.querySelector('.tabs'); if (reassurance && shell && tabs && reassurance.nextElementSibling !== tabs) { reassurance.classList.add('sc-reassurance-moved'); tabs.insertAdjacentElement('beforebegin', reassurance); } } function moveDeliveryBlockToCheckout() { const target = document.querySelector('.sc-checkout .sc-delivery-hook'); if (!target || target.dataset.scDeliveryReady === '1') { return; } const candidates = Array.from(document.body.querySelectorAll('div, section, article, p, span')).filter((element) => { if (element.closest('.sc-checkout') || element.closest('script, style')) { return false; } const text = (element.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase(); return text.includes('commandez maintenant') && text.includes('livraison') && text.length < 350; }); const source = candidates.sort((a, b) => { return (a.textContent || '').length - (b.textContent || '').length; })[0]; if (!source) { return; } source.style.display = ''; target.innerHTML = ''; target.appendChild(source); target.dataset.scDeliveryReady = '1'; } let root = document.querySelector('#sc-configurator'); if (!root) { autoMountConfigurator(); root = document.querySelector('#sc-configurator'); } relocateConfigurator(); tidyProductPageAroundConfigurator(); moveDeliveryBlockToCheckout(); root = document.querySelector('#sc-configurator'); const cartPreview = document.querySelector('[data-sc-cart-preview]'); function readConfig() { const globalConfig = getGlobalConfig(); try { const parsed = root ? JSON.parse(root.querySelector('#sc-config-data').textContent) : globalConfig; return { fonts: parsed.fonts && parsed.fonts.length ? parsed.fonts : fallbackConfig.fonts, colors: parsed.colors && parsed.colors.length ? parsed.colors : fallbackConfig.colors, materials: parsed.materials && parsed.materials.length ? parsed.materials : fallbackConfig.materials, priceRules: parsed.priceRules && parsed.priceRules.length ? parsed.priceRules : fallbackConfig.priceRules, logos: parsed.logos && parsed.logos.length ? parsed.logos : fallbackConfig.logos, options: { enableTextMode: !parsed.options || parsed.options.enableTextMode !== false, enableLogoMode: !!(parsed.options && parsed.options.enableLogoMode), enableStickerType: !!(parsed.options && parsed.options.enableStickerType), enableMultiline: !!(parsed.options && parsed.options.enableMultiline), enableStencil: !parsed.options || parsed.options.enableStencil !== false, enableMirror: !parsed.options || parsed.options.enableMirror !== false, enableInside: !parsed.options || parsed.options.enableInside !== false } }; } catch (error) { return fallbackConfig; } } function formatPrice(value) { return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value); } function renderCartPreview() { if (!cartPreview) { return; } if (cartPreview.dataset.serverConfig === '1') { return; } const raw = window.localStorage.getItem('stickerConfiguratorLastConfig'); const empty = cartPreview.querySelector('.sc-cart-preview-empty'); const content = cartPreview.querySelector('.sc-cart-preview-content'); const art = cartPreview.querySelector('.sc-cart-preview-art'); const summary = cartPreview.querySelector('.sc-cart-preview-summary'); if (!raw) { return; } try { const config = JSON.parse(raw); empty.hidden = true; content.hidden = false; art.style.color = config.color; applyPreviewContrast(art, config.color); art.innerHTML = config.mode === 'logo' && config.logoSvg ? config.logoSvg : `${config.text}`; summary.textContent = `${config.mode === 'logo' ? 'Logo SVG' : config.text} - ${config.width} x ${config.height} cm - ${config.colorName} - ${config.materialName} - ${config.quantity} ex.`; } catch (error) { window.localStorage.removeItem('stickerConfiguratorLastConfig'); } } function getProductId() { const value = Number(root.getAttribute('data-product-id')); return Number.isFinite(value) ? value : 0; } function getProductAttributeId() { const input = document.querySelector('input[name="id_product_attribute"]'); const value = Number(input ? input.value : 0); return Number.isFinite(value) ? value : 0; } async function saveConfiguration() { const productId = getProductId(); const globalSaveUrl = typeof stickerConfiguratorSaveUrl !== 'undefined' ? stickerConfiguratorSaveUrl : ''; const globalToken = typeof stickerConfiguratorToken !== 'undefined' ? stickerConfiguratorToken : ''; const saveUrl = root.dataset.saveUrl || globalSaveUrl || window.stickerConfiguratorSaveUrl || `${window.location.origin}/module/stickerconfigurator/save`; const saveToken = root.dataset.saveToken || globalToken || window.stickerConfiguratorToken; if (!productId || !saveUrl || !hiddenConfig.value) { throw new Error('La configuration ne peut pas être enregistrée.'); } const body = new URLSearchParams(); body.set('token', saveToken || (window.prestashop && prestashop.static_token ? prestashop.static_token : '')); body.set('id_product', String(productId)); body.set('id_product_attribute', String(getProductAttributeId())); body.set('configuration', hiddenConfig.value); const response = await window.fetch(saveUrl, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, body: body.toString() }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.success || !result.id_customization) { throw new Error(result.error || 'La configuration ne peut pas être enregistrée.'); } return result; } async function addProductToCart(savedConfiguration) { const productId = getProductId(); if (!productId || !window.prestashop || !prestashop.urls || !prestashop.urls.pages || !prestashop.urls.pages.cart) { return false; } const url = new URL(prestashop.urls.pages.cart, window.location.origin); url.searchParams.set('add', '1'); url.searchParams.set('action', 'update'); url.searchParams.set('ajax', '1'); url.searchParams.set('id_product', String(productId)); url.searchParams.set('id_product_attribute', String(getProductAttributeId())); url.searchParams.set('qty', String(savedConfiguration.quantity)); url.searchParams.set('id_customization', String(savedConfiguration.id_customization)); if (prestashop.static_token) { url.searchParams.set('token', prestashop.static_token); } const response = await window.fetch(url.toString(), { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const cartResponse = await response.clone().json().catch(() => ({})); if (!response.ok || cartResponse.hasError || (cartResponse.errors && cartResponse.errors.length)) { return false; } if (prestashop.emit) { prestashop.emit('updateCart', { reason: { idProduct: productId, linkAction: 'add-to-cart' }, resp: cartResponse }); } return true; } function fitCartPreviewVisual(visual) { if (!visual || visual.querySelector('svg')) { return; } visual.style.fontSize = '14px'; window.requestAnimationFrame(() => { let size = 14; while (visual.scrollWidth > visual.clientWidth && size > 5) { size -= 1; visual.style.fontSize = `${size}px`; } }); } function attachCartPreviewsToProductLines() { const staging = document.querySelector('.sc-cart-preview-staging'); const cacheKey = 'stickerConfiguratorCartPreviews'; let previews = staging ? Array.from(staging.querySelectorAll('[data-sc-cart-line-preview]')) : []; const checkoutTotalHt = staging ? staging.querySelector('.sc-checkout-total-ht') : null; const checkoutTotals = document.querySelector('body#checkout .cart-summary-totals'); if (checkoutTotalHt && checkoutTotals && !checkoutTotals.querySelector('.sc-checkout-total-ht')) { checkoutTotalHt.hidden = false; checkoutTotals.insertAdjacentElement('afterbegin', checkoutTotalHt); } if (previews.length) { try { window.sessionStorage.setItem(cacheKey, JSON.stringify(previews.map((preview) => preview.outerHTML))); } catch (error) { // The server-rendered preview remains available when storage is disabled. } } else { try { const cached = JSON.parse(window.sessionStorage.getItem(cacheKey) || '[]'); previews = cached.map((html) => { const template = document.createElement('template'); template.innerHTML = html; return template.content.firstElementChild; }).filter(Boolean); } catch (error) { previews = []; } } const productIds = [...new Set(previews.map((preview) => preview.dataset.productId).filter((id) => /^\d+$/.test(id || '')))]; if (productIds.length) { let guard = document.querySelector('#sc-cart-image-guard'); if (!guard) { guard = document.createElement('style'); guard.id = 'sc-cart-image-guard'; document.head.append(guard); } guard.textContent = productIds.map((id) => `.cart-item:has([data-id-product="${id}"]) .product-image > picture, .cart-item:has([data-id_product="${id}"]) .product-image > picture { visibility: hidden !important; }` ).join('\n'); } previews.forEach((preview) => { const productId = preview.dataset.productId; const customizationId = preview.dataset.customizationId; let marker = null; if (customizationId && customizationId !== '0') { marker = document.querySelector(`.cart-item [data-id-customization="${customizationId}"], .cart-item [data-id_customization="${customizationId}"]`); } if (!marker && productId) { marker = document.querySelector(`.cart-item [data-id-product="${productId}"], .cart-item [data-id_product="${productId}"]`); } const row = marker ? marker.closest('.cart-item') : null; const checkoutMarker = !row && productId ? document.querySelector(`#cart-summary-product-list li.media a[href*="/${productId}-"]`) : null; const checkoutRow = checkoutMarker ? checkoutMarker.closest('li.media') : null; const line = row || checkoutRow; const target = row ? row.querySelector('.product-line-grid-body') : checkoutRow ? checkoutRow.querySelector('.media-body') : null; const imageTarget = row ? row.querySelector('.product-line-grid-left .product-image') : checkoutRow ? checkoutRow.querySelector('.media-left a') : null; const visual = preview.querySelector('.sc-cart-preview-art'); const details = preview.querySelector('.sc-cart-preview-summary'); const cartKey = customizationId && customizationId !== '0' ? customizationId : productId; const existing = line ? line.querySelector(`[data-sc-custom-cart="${cartKey}"]`) : null; if (line) { line.querySelectorAll('.customizations').forEach((customizations) => customizations.remove()); line.querySelectorAll('a[data-target^="#product-customizations-modal-"]').forEach((link) => { const line = link.closest('.product-line-info'); (line || link).remove(); }); } if (target && imageTarget && visual && details && !existing) { visual.classList.add('sc-cart-product-visual'); visual.dataset.scCustomCart = cartKey; visual.style.fontFamily = preview.dataset.fontFamily || ''; applyPreviewContrast(visual, preview.dataset.color || preview.dataset.colorName || visual.style.color); details.classList.add('sc-cart-product-summary'); details.dataset.scCustomCart = cartKey; imageTarget.replaceChildren(visual); fitCartPreviewVisual(visual); target.append(details); if (checkoutRow) { checkoutRow.classList.add('sc-checkout-configured-product'); } preview.remove(); } else if (existing) { preview.remove(); } }); if (staging && !staging.querySelector('[data-sc-cart-line-preview]')) { staging.remove(); } } renderCartPreview(); attachCartPreviewsToProductLines(); if (window.prestashop && prestashop.on) { prestashop.on('updatedCart', () => { window.setTimeout(attachCartPreviewsToProductLines, 0); window.setTimeout(attachCartPreviewsToProductLines, 50); window.setTimeout(attachCartPreviewsToProductLines, 300); }); } const cartOverview = document.querySelector('.cart-overview, .cart-container'); if (cartOverview && window.MutationObserver) { let cartPreviewTimer = 0; new MutationObserver(() => { window.clearTimeout(cartPreviewTimer); cartPreviewTimer = window.setTimeout(attachCartPreviewsToProductLines, 0); }).observe(cartOverview, { childList: true, subtree: true }); } if (!root) { return; } const config = readConfig(); const state = { mode: 'text', text: 'Votre texte', font: config.fonts[0].name, fontCategory: 'all', color: (config.colors.find((color) => color.name.toLowerCase() === 'noir') || config.colors[0]).value, colorName: (config.colors.find((color) => color.name.toLowerCase() === 'noir') || config.colors[0]).name, material: config.materials[0].name, materialMultiplier: parseMultiplier(config.materials[0].value), logoName: config.logos[0] ? config.logos[0].name : '', logoSvg: config.logos[0] ? config.logos[0].svg : '', width: 40, height: 12, baseWidth: 40, baseHeight: 12, quantity: 1, keepRatio: true, multiline: false, stencil: false, mirror: false, inside: false, stretchX: 100, rotation: 0, zoom: 1, naturalWidthCm: 40, offsetX: 0, offsetY: 0 }; const modeSettingKeys = [ 'text', 'font', 'fontCategory', 'color', 'colorName', 'material', 'materialMultiplier', 'logoName', 'logoSvg', 'width', 'height', 'baseWidth', 'baseHeight', 'keepRatio', 'multiline', 'stencil', 'mirror', 'inside', 'stretchX', 'rotation', 'zoom', 'naturalWidthCm', 'offsetX', 'offsetY' ]; const modeSettings = { text: {}, logo: {} }; const preview = root.querySelector('#sc-sticker-preview'); const logoPreview = root.querySelector('#sc-logo-preview'); const fontGrid = root.querySelector('#sc-font-grid'); const logoGrid = root.querySelector('#sc-logo-grid'); const colorGrid = root.querySelector('#sc-color-grid'); const materialGrid = root.querySelector('#sc-material-grid'); const materialSelect = root.querySelector('#sc-material'); const textInput = root.querySelector('#sc-text-input'); const widthInput = root.querySelector('#sc-width'); const heightInput = root.querySelector('#sc-height'); const quantityInput = root.querySelector('#sc-quantity'); const quantityMinus = root.querySelector('#sc-quantity-minus'); const quantityPlus = root.querySelector('#sc-quantity-plus'); const stretchInput = root.querySelector('#sc-stretch-x'); const rotationInput = root.querySelector('#sc-rotation'); const price = root.querySelector('#sc-price'); let measureCanvasCtx = null; const HEIGHT_CALIBRATION = 1.011; const summaryText = root.querySelector('#sc-summary-text') || document.querySelector('#sc-summary-text'); const summarySize = root.querySelector('#sc-summary-size') || document.querySelector('#sc-summary-size'); const summaryFont = root.querySelector('#sc-summary-font') || document.querySelector('#sc-summary-font'); const summaryColor = root.querySelector('#sc-summary-color') || document.querySelector('#sc-summary-color'); const summaryMaterial = root.querySelector('#sc-summary-material') || document.querySelector('#sc-summary-material'); const summaryOptions = root.querySelector('#sc-summary-options') || document.querySelector('#sc-summary-options'); const summaryQuantity = root.querySelector('#sc-summary-quantity') || document.querySelector('#sc-summary-quantity'); const previewSize = root.querySelector('#sc-preview-size'); const previewFinish = root.querySelector('#sc-preview-finish'); const realSizeLabel = root.querySelector('#sc-real-size-label'); const previewSurface = root.querySelector('.sc-preview-surface'); const guideTop = root.querySelector('.sc-guide-top'); const guideBottom = root.querySelector('.sc-guide-bottom'); const dimensionLabel = root.querySelector('#sc-dimension-label'); let secondaryDimensionLabel = root.querySelector('#sc-secondary-dimension-label'); let totalDimensionLabel = root.querySelector('#sc-total-dimension-label'); const totalSizeReminder = root.querySelector('#sc-total-size-reminder'); const hiddenConfig = root.querySelector('#sc-configuration-json'); const dialog = document.querySelector('#sc-cart-dialog'); const summary = document.querySelector('#sc-cart-summary'); let fontPickerButton = null; let textEditorButton = null; const dimensionLimits = { minWidth: Number(config.options.minWidth) || 5, maxWidth: Number(config.options.maxWidth) || 300, minHeight: Number(config.options.minHeight) || 2, maxHeight: Number(config.options.maxHeight) || 120 }; function copyModeSettings() { return modeSettingKeys.reduce((settings, key) => { settings[key] = state[key]; return settings; }, {}); } function saveCurrentModeSettings() { modeSettings[state.mode] = copyModeSettings(); } function applyModeSettings(mode) { Object.assign(state, modeSettings[mode] || {}); state.mode = mode; textInput.value = state.text; textInput.rows = state.multiline ? 4 : 2; syncSizeInputs(); if (stretchInput) { stretchInput.value = state.stretchX; } if (rotationInput) { rotationInput.value = state.rotation; } materialSelect.value = state.material; [ ['#sc-keep-ratio', 'keepRatio'], ['#sc-multiline', 'multiline'], ['#sc-stencil', 'stencil'], ['#sc-mirror', 'mirror'], ['#sc-inside', 'inside'] ].forEach(([selector, key]) => { const input = root.querySelector(selector); if (input) { input.checked = !!state[key]; } }); if (fontPickerButton) { fontPickerButton.textContent = `Police : ${state.font}`; } updateTextEditorButton(); } function switchMode(mode) { if (mode === state.mode) { return; } saveCurrentModeSettings(); applyModeSettings(mode); closeOpenConfiguratorDialogs(); renderFonts(); renderLogos(); renderColors(); renderMaterials(); renderMode(); render(); } modeSettings.text = copyModeSettings(); modeSettings.logo = copyModeSettings(); modeSettings.logo.width = modeSettings.logo.height; modeSettings.logo.baseWidth = modeSettings.logo.baseHeight; syncSizeInputs(); function getLargeImageUrl(image) { return image.dataset.imageLargeSrc || image.dataset.largeSrc || image.getAttribute('data-image-large-src') || image.getAttribute('data-large-src') || image.src.replace('-small_default/', '-large_default/').replace('-home_default/', '-large_default/').replace('-medium_default/', '-large_default/'); } function openProductImagePopup(image) { let galleryDialog = document.querySelector('#sc-gallery-dialog'); if (!galleryDialog) { galleryDialog = document.createElement('dialog'); galleryDialog.id = 'sc-gallery-dialog'; galleryDialog.className = 'sc-gallery-dialog'; galleryDialog.innerHTML = '
Aucun logo SVG configuré.
'; return; } config.logos.forEach((logo) => { const button = document.createElement('button'); button.type = 'button'; button.className = `sc-logo-card${logo.name === state.logoName ? ' active' : ''}`; button.innerHTML = `${logo.svg}${logo.name}`; button.addEventListener('click', () => { state.logoName = logo.name; state.logoSvg = logo.svg; render(); renderLogos(); }); logoGrid.append(button); }); } function renderColors() { colorGrid.innerHTML = ''; config.colors.forEach((color) => { const button = document.createElement('button'); button.type = 'button'; button.className = `sc-swatch${color.value === state.color ? ' active' : ''}`; button.style.setProperty('--swatch', color.value); const colorExtra = Math.max(0, Number(color.price) || 0); const colorLabel = colorExtra > 0 ? `${color.name} (+${formatPrice(colorExtra)})` : color.name; button.title = colorLabel; button.setAttribute('aria-label', colorLabel); button.addEventListener('click', () => { state.color = color.value; state.colorName = color.name; render(); renderColors(); }); colorGrid.append(button); }); } function renderMaterials() { materialSelect.innerHTML = ''; if (materialGrid) { materialGrid.innerHTML = ''; } config.materials.forEach((material) => { const option = document.createElement('option'); option.value = material.name; option.textContent = material.name; option.dataset.multiplier = material.value; materialSelect.append(option); if (materialGrid) { const button = document.createElement('button'); button.type = 'button'; button.className = `sc-material-card${material.name === state.material ? ' active' : ''}`; const materialExtra = Math.max(0, Number(material.price) || 0); const extraLabel = materialExtra > 0 ? ` + ${formatPrice(materialExtra)}` : ''; button.innerHTML = `${material.name}${extraLabel ? `${extraLabel.trim()}` : ''}`; button.addEventListener('click', () => setMaterial(material.name, material.value)); materialGrid.append(button); } }); materialSelect.value = state.material; } function renderMode() { applyFeatureVisibility(); const isLogo = state.mode === 'logo'; const showTextPreview = isModeAvailable('text'); const showLogoPreview = isModeAvailable('logo'); root.querySelectorAll('.sc-text-panel').forEach((panel) => { panel.hidden = isLogo; }); root.querySelector('.sc-logo-panel').hidden = !isLogo; preview.hidden = !showTextPreview; logoPreview.hidden = !showLogoPreview; root.querySelector('.sc-sticker-shadow')?.classList.toggle('sc-combined-preview', showTextPreview && showLogoPreview); root.querySelectorAll('.sc-mode-tabs button').forEach((button) => { button.classList.toggle('active', button.dataset.mode === state.mode); }); } function applyFeatureVisibility() { const allowTextMode = config.options.enableTextMode !== false; const allowLogoMode = !!config.options.enableLogoMode; const availableModes = [allowTextMode ? 'text' : null, allowLogoMode ? 'logo' : null].filter(Boolean); const allowStickerType = !!config.options.enableStickerType && availableModes.length > 1; const allowMultiline = !!config.options.enableMultiline; const allowedSpecials = { stencil: config.options.enableStencil !== false, mirror: config.options.enableMirror !== false, inside: config.options.enableInside !== false }; const modeStep = root.querySelector('.sc-mode-tabs')?.closest('.sc-step'); const multilineOption = root.querySelector('#sc-multiline')?.closest('.sc-check-row'); const forcedMode = availableModes.includes(state.mode) ? state.mode : (availableModes[0] || 'text'); if (modeStep) { modeStep.hidden = !allowStickerType; } if (!allowStickerType) { if (state.mode !== forcedMode) { saveCurrentModeSettings(); applyModeSettings(forcedMode); } else { state.mode = forcedMode; } root.querySelectorAll('.sc-mode-tabs button').forEach((button) => { button.classList.toggle('active', button.dataset.mode === state.mode); }); } root.querySelectorAll('.sc-mode-tabs button').forEach((button) => { button.hidden = !availableModes.includes(button.dataset.mode); }); if (multilineOption) { multilineOption.hidden = !allowMultiline || state.mode !== 'text'; } if (!allowMultiline) { state.multiline = false; const multilineInput = root.querySelector('#sc-multiline'); if (multilineInput) { multilineInput.checked = false; } textInput.rows = 2; if (textInput.value.includes('\n')) { textInput.value = textInput.value.replace(/\s*\n\s*/g, ' '); state.text = textInput.value; updateTextEditorButton(); } } [ ['#sc-stencil', 'stencil'], ['#sc-mirror', 'mirror'], ['#sc-inside', 'inside'] ].forEach(([selector, key]) => { const input = root.querySelector(selector); const row = input ? input.closest('.sc-check-row') : null; const enabled = allowedSpecials[key] !== false; if (row) { row.hidden = !enabled; } if (!enabled) { state[key] = false; if (input) { input.checked = false; } } }); } function render() { const textSettings = modeSnapshot('text'); const logoSettings = modeSnapshot('logo'); const font = selectedFontFor(textSettings); const activeFont = selectedFontFor(state); const displayText = (textSettings.text || '').trim() || 'Votre texte'; const maxFontSize = window.matchMedia('(max-width: 680px)').matches ? 40 : 96; const fontSize = Math.max(34, Math.min(maxFontSize, textSettings.height * (textSettings.multiline ? 4.1 : 5.3) * textSettings.zoom)); const logoSize = Math.max(34, Math.min(maxFontSize, logoSettings.height * 5.55 * logoSettings.zoom)); preview.textContent = displayText; const pickerPreview = document.querySelector('#sc-font-picker-preview'); if (pickerPreview) { pickerPreview.textContent = (state.text || '').trim() || 'Votre texte'; pickerPreview.style.fontFamily = activeFont.value; pickerPreview.style.color = state.color; applyPreviewContrast(pickerPreview, state.color); } const textEditorPreview = document.querySelector('#sc-text-editor-preview'); if (textEditorPreview) { textEditorPreview.textContent = (state.text || '').trim() || 'Votre texte'; textEditorPreview.style.fontFamily = activeFont.value; textEditorPreview.style.color = state.color; applyPreviewContrast(textEditorPreview, state.color); } preview.style.fontFamily = font.value; preview.style.color = textSettings.color; preview.style.fontSize = `${fontSize}px`; preview.style.setProperty('--sc-preview-font-size', `${fontSize}px`); preview.style.transform = getTransformFor(textSettings); preview.classList.toggle('sc-stencil', !!textSettings.stencil); preview.classList.toggle('sc-multiline', !!textSettings.multiline); logoPreview.innerHTML = logoSettings.logoSvg || ''; logoPreview.style.color = logoSettings.color || state.color; logoPreview.style.transform = getTransformFor(logoSettings); logoPreview.style.width = `${logoSize}px`; logoPreview.style.height = `${logoSize}px`; previewSurface.classList.toggle( 'sc-dark-contrast', (isModeAvailable('text') && needsDarkContrast(textSettings.color)) || (isModeAvailable('logo') && needsDarkContrast(logoSettings.color || state.color)) ); preview.classList.toggle('is-selected-live-item', state.mode === 'text'); logoPreview.classList.toggle('is-selected-live-item', state.mode === 'logo'); if (isModeAvailable('text') && isModeAvailable('logo')) { previewSize.textContent = `Texte ${formatCentimeters(renderedWidthCmFor(textSettings, 'text'))} x ${formatCentimeters(calibratedHeightCmFor(textSettings, 'text'))} cm | Logo ${formatCentimeters(renderedWidthCmFor(logoSettings, 'logo'))} x ${formatCentimeters(calibratedHeightCmFor(logoSettings, 'logo'))} cm`; previewFinish.textContent = `Texte ${textSettings.material} - ${textSettings.colorName} | Logo ${logoSettings.material} - ${logoSettings.colorName}`; } else { previewSize.textContent = `${formatCentimeters(renderedWidthCm())} x ${formatCentimeters(calibratedHeightCmFor(state, state.mode))} cm`; previewFinish.textContent = `${state.material} - ${state.colorName}`; } realSizeLabel.textContent = `Hauteur réelle : ${formatCentimeters(calibratedHeightCmFor(state, state.mode))} cm`; syncSizeInputs(); price.textContent = formatPrice(calculatePrice()); updateCheckoutSummary(); updateHiddenConfig(); scheduleDimensionUpdate(); } root.querySelectorAll('.sc-mode-tabs button').forEach((button) => { button.addEventListener('click', () => { if (button.hidden) { return; } switchMode(button.dataset.mode); }); }); root.querySelectorAll('.sc-specials button').forEach((button) => { button.addEventListener('click', () => { const start = textInput.selectionStart; const end = textInput.selectionEnd; const char = button.textContent; textInput.value = `${textInput.value.slice(0, start)}${char}${textInput.value.slice(end)}`; textInput.focus(); textInput.selectionStart = textInput.selectionEnd = start + char.length; state.text = textInput.value; updateTextEditorButton(); render(); }); }); root.querySelectorAll('.sc-font-tabs button').forEach((button) => { button.addEventListener('click', () => { root.querySelectorAll('.sc-font-tabs button').forEach((tab) => tab.classList.remove('active')); button.classList.add('active'); state.fontCategory = button.dataset.filter; renderFonts(); }); }); root.querySelectorAll('[data-qty]').forEach((button) => { button.addEventListener('click', () => { root.querySelectorAll('[data-qty]').forEach((qty) => qty.classList.remove('active')); button.classList.add('active'); state.quantity = Number(button.dataset.qty); quantityInput.value = state.quantity; render(); }); }); function syncTextInput() { state.text = textInput.value; updateTextEditorButton(); render(); } root.addEventListener('input', (event) => { if (event.target && event.target.id === 'sc-text-input') { syncTextInput(); } }, true); root.addEventListener('beforeinput', (event) => { if (event.target && event.target.id === 'sc-text-input') { window.setTimeout(syncTextInput, 0); } }, true); textInput.addEventListener('input', syncTextInput); textInput.addEventListener('keyup', syncTextInput); textInput.addEventListener('change', syncTextInput); textInput.addEventListener('blur', syncTextInput); textInput.addEventListener('compositionend', syncTextInput); textInput.addEventListener('paste', () => window.setTimeout(syncTextInput, 0)); bindPreviewDrag(preview, 'text'); bindPreviewDrag(logoPreview, 'logo'); materialSelect.addEventListener('change', (event) => { const selected = event.target.selectedOptions[0]; if (selected) { setMaterial(selected.value, selected.dataset.multiplier); } }); widthInput.addEventListener('input', () => commitWidthInput(false)); widthInput.addEventListener('change', () => commitWidthInput(true)); widthInput.addEventListener('blur', () => commitWidthInput(true)); heightInput.addEventListener('input', () => commitHeightInput(false)); heightInput.addEventListener('change', () => commitHeightInput(true)); heightInput.addEventListener('blur', () => commitHeightInput(true)); quantityInput.addEventListener('input', () => { state.quantity = Math.max(1, Number(quantityInput.value) || 1); root.querySelectorAll('[data-qty]').forEach((qty) => { qty.classList.toggle('active', Number(qty.dataset.qty) === state.quantity); }); render(); }); function stepQuantity(delta) { state.quantity = Math.min(999, Math.max(1, (Number(state.quantity) || 1) + delta)); quantityInput.value = state.quantity; root.querySelectorAll('[data-qty]').forEach((qty) => { qty.classList.toggle('active', Number(qty.dataset.qty) === state.quantity); }); render(); } if (quantityMinus) { quantityMinus.addEventListener('click', () => stepQuantity(-1)); } if (quantityPlus) { quantityPlus.addEventListener('click', () => stepQuantity(1)); } if (stretchInput) { stretchInput.addEventListener('input', () => { state.stretchX = Number(stretchInput.value) || 100; render(); }); } if (rotationInput) { rotationInput.addEventListener('input', () => { state.rotation = Number(rotationInput.value) || 0; render(); }); } const resetRatioButton = root.querySelector('#sc-reset-ratio'); if (resetRatioButton) { resetRatioButton.addEventListener('click', () => { state.width = clampWidth(state.baseWidth); state.height = clampHeight(state.baseHeight); state.stretchX = 100; state.rotation = 0; state.offsetX = 0; state.offsetY = 0; widthInput.value = state.width; heightInput.value = state.height; if (stretchInput) { stretchInput.value = state.stretchX; } if (rotationInput) { rotationInput.value = state.rotation; } render(); }); } [ ['#sc-keep-ratio', 'keepRatio'], ['#sc-multiline', 'multiline'], ['#sc-stencil', 'stencil'], ['#sc-mirror', 'mirror'], ['#sc-inside', 'inside'] ].forEach(([selector, key]) => { const optionInput = root.querySelector(selector); if (!optionInput) { return; } optionInput.addEventListener('change', (event) => { state[key] = event.target.checked; if (key === 'multiline') { textInput.rows = state.multiline ? 4 : 2; if (state.multiline && !textInput.value.includes('\n')) { textInput.value = textInput.value.replace(' ', '\n'); state.text = textInput.value; } } render(); }); }); root.querySelector('#sc-zoom-in').addEventListener('click', () => { state.zoom = Math.min(1.4, state.zoom + 0.1); render(); }); root.querySelector('#sc-zoom-out').addEventListener('click', () => { state.zoom = Math.max(0.7, state.zoom - 0.1); render(); }); root.querySelector('#sc-config-form').addEventListener('submit', async (event) => { event.preventDefault(); updateHiddenConfig(); window.localStorage.setItem('stickerConfiguratorLastConfig', hiddenConfig.value); const submitButton = event.submitter; if (submitButton) { submitButton.disabled = true; } let added = false; let errorMessage = ''; try { const savedConfiguration = await saveConfiguration(); added = savedConfiguration.added === true; if (added && window.prestashop && prestashop.emit) { prestashop.emit('updateCart', { reason: { idProduct: getProductId(), idCustomization: savedConfiguration.id_customization, linkAction: 'add-to-cart' }, resp: savedConfiguration }); } } catch (error) { errorMessage = error instanceof Error ? error.message : 'Une erreur est survenue.'; } finally { if (submitButton) { submitButton.disabled = false; } } summary.textContent = `${state.quantity} sticker(s), ${formatCentimeters(renderedWidthCm())} x ${formatCentimeters(calibratedHeightCmFor(state, state.mode))} cm, ${state.mode === 'logo' ? state.logoName : `texte ${state.font}`}, ${state.colorName.toLowerCase()}, ${state.material}. Total estimé : ${formatPrice(calculatePrice())}.${added ? ' Produit ajouté au panier avec sa personnalisation.' : ` Ajout impossible. ${errorMessage}`}`; dialog.showModal(); }); stabilizeProductLayout(); window.addEventListener('load', stabilizeProductLayout); window.addEventListener('load', moveDeliveryBlockToCheckout); [250, 800, 1600, 3000].forEach((delay) => { window.setTimeout(stabilizeProductLayout, delay); window.setTimeout(moveDeliveryBlockToCheckout, delay); }); if (window.MutationObserver) { const observer = new MutationObserver(() => { window.clearTimeout(observer.scTimer); observer.scTimer = window.setTimeout(stabilizeProductLayout, 80); }); observer.observe(document.body, { childList: true, subtree: true }); } renderFonts(); setupMobileFontPicker(); setupTextEditor(); setupMobileEditingBehavior(); renderLogos(); renderColors(); renderMaterials(); renderMode(); render(); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(function () { _fontHeightRef = null; Object.keys(_fontHeightCache).forEach(function (key) { delete _fontHeightCache[key]; }); render(); }); } })();