Aller au contenu principal

Prévisualisation

/** * ============================================ * SUPPORTMARKET – Preview Manager * Script de prévisualisation en temps réel * ============================================ * * Ce script gère la mise à jour dynamique de la prévisualisation * des supports imprimés (cartes de visite, flyers, etc.) basée * sur les champs de personnalisation remplis par l’utilisateur. * * Compatible avec injection via Code Snippets WordPress. */ (function() { ‘use strict’; /* ============================================ CONFIGURATION ============================================ */ const CONFIG = { // Sélecteurs des champs de saisie fields: { name: ‘#field-name’, job: ‘#field-job’, company: ‘#field-company’, phone: ‘#field-phone’, email: ‘#field-email’, address: ‘#field-address’, website: ‘#field-website’, slogan: ‘#field-slogan’ }, // Sélecteurs des éléments de prévisualisation preview: { name: ‘#preview-name’, job: ‘#preview-job’, company: ‘#preview-company’, phone: ‘#preview-phone’, email: ‘#preview-email’, address: ‘#preview-address’, website: ‘#preview-website’, slogan: ‘#preview-slogan’, logo: ‘#preview-logo’ }, // Sélecteurs des options produit options: { format: ‘#format-options’, orientation: ‘#orientation-options’, paper: ‘#paper-options’, print: ‘#print-options’, quantity: ‘#quantity-options’, color: ‘.sm-color-picker’ }, // Valeurs par défaut affichées dans la preview placeholders: { name: ‘Votre nom’, job: ‘Votre fonction’, company: ‘Votre entreprise’, phone: ’06 00 00 00 00′, email: ’email@exemple.fr’, address: ‘Votre adresse’, website: ‘www.exemple.fr’, slogan:  » }, // Délai de debounce pour la mise à jour (ms) debounceDelay: 150 }; /* ============================================ UTILITAIRES ============================================ */ /** * Fonction debounce pour limiter les appels répétés * @param {Function} func – Fonction à exécuter * @param {number} wait – Délai d’attente en ms * @returns {Function} – Fonction debounced */ function debounce(func, wait) { let timeout; return function executedFunction(…args) { const later = () => { clearTimeout(timeout); func(…args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Sélecteur sécurisé avec fallback * @param {string} selector – Sélecteur CSS * @returns {Element|null} – Élément DOM ou null */ function $(selector) { return document.querySelector(selector); } /** * Sélecteur multiple * @param {string} selector – Sélecteur CSS * @returns {NodeList} – Liste des éléments */ function $$(selector) { return document.querySelectorAll(selector); } /** * Échappe les caractères HTML pour éviter les injections XSS * @param {string} text – Texte à échapper * @returns {string} – Texte échappé */ function escapeHtml(text) { if (!text) return  »; const div = document.createElement(‘div’); div.textContent = text; return div.innerHTML; } /** * Formate un numéro de téléphone français * @param {string} phone – Numéro brut * @returns {string} – Numéro formaté */ function formatPhone(phone) { if (!phone) return  »; // Supprime tout sauf les chiffres et le + const cleaned = phone.replace(/[^\d+]/g,  »); // Format français : XX XX XX XX XX if (cleaned.length === 10 && cleaned.startsWith(‘0’)) { return cleaned.replace(/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, ‘$1 $2 $3 $4 $5′); } return phone; } /** * Formate une URL (supprime http:// si présent) * @param {string} url – URL brute * @returns {string} – URL formatée pour affichage */ function formatUrl(url) { if (!url) return  »; return url.replace(/^https?:\/\//,  »).replace(/\/$/,  »); } /* ============================================ GESTIONNAIRE DE PRÉVISUALISATION ============================================ */ const PreviewManager = { // État actuel des données state: { fields: {}, options: { format: ’85×55’, orientation: ‘horizontal’, paper: ‘350g-mat’, print: ‘recto’, quantity: 100, color: ‘#0f172a’ }, logo: null }, /** * Initialise le gestionnaire de prévisualisation */ init: function() { // Vérifie que les éléments nécessaires existent if (!$(‘#preview-frame’)) { console.log(‘SupportMarket Preview: Aucun élément de prévisualisation trouvé.’); return; } console.log(‘SupportMarket Preview: Initialisation…’); // Initialise les écouteurs d’événements this.bindFieldEvents(); this.bindOptionEvents(); this.bindLogoUpload(); this.bindTabSwitcher(); this.bindPreviewToggle(); this.bindTemplateSelector(); // Mise à jour initiale this.updateAllFields(); this.updatePreviewInfo(); console.log(‘SupportMarket Preview: Prêt.’); }, /** * Lie les événements aux champs de texte */ bindFieldEvents: function() { const self = this; // Crée une fonction de mise à jour debounced const debouncedUpdate = debounce(function(fieldName, value) { self.updateField(fieldName, value); }, CONFIG.debounceDelay); // Parcourt tous les champs configurés Object.keys(CONFIG.fields).forEach(function(fieldName) { const input = $(CONFIG.fields[fieldName]); if (input) { // Écoute les événements de saisie input.addEventListener(‘input’, function(e) { debouncedUpdate(fieldName, e.target.value); }); // Écoute aussi le changement (pour autocomplete, etc.) input.addEventListener(‘change’, function(e) { self.updateField(fieldName, e.target.value); }); // Stocke la valeur initiale self.state.fields[fieldName] = input.value ||  »; } }); }, /** * Lie les événements aux boutons d’options */ bindOptionEvents: function() { const self = this; // Gestion des boutons d’options (format, orientation, papier, impression) [‘format’, ‘orientation’, ‘paper’, ‘print’].forEach(function(optionType) { const container = $(CONFIG.options[optionType]); if (container) { container.addEventListener(‘click’, function(e) { const button = e.target.closest(‘.sm-option-btn’); if (button && !button.classList.contains(‘sm-option-btn–active’)) { // Désactive l’ancien bouton actif const currentActive = container.querySelector(‘.sm-option-btn–active’); if (currentActive) { currentActive.classList.remove(‘sm-option-btn–active’); } // Active le nouveau bouton button.classList.add(‘sm-option-btn–active’); // Met à jour l’état et le champ caché const value = button.dataset.value; self.state.options[optionType] = value; const hiddenInput = $(‘#’ + optionType + ‘-input’); if (hiddenInput) { hiddenInput.value = value; } // Met à jour l’affichage self.updatePreviewInfo(); self.updatePreviewFrame(); self.updatePrice(); } }); } }); // Gestion des boutons de quantité const quantityContainer = $(CONFIG.options.quantity); if (quantityContainer) { quantityContainer.addEventListener(‘click’, function(e) { const button = e.target.closest(‘.sm-quantity-btn’); if (button && !button.classList.contains(‘sm-quantity-btn–active’)) { // Désactive l’ancien const currentActive = quantityContainer.querySelector(‘.sm-quantity-btn–active’); if (currentActive) { currentActive.classList.remove(‘sm-quantity-btn–active’); } // Active le nouveau button.classList.add(‘sm-quantity-btn–active’); // Met à jour l’état self.state.options.quantity = parseInt(button.dataset.value, 10); const hiddenInput = $(‘#quantity-input’); if (hiddenInput) { hiddenInput.value = button.dataset.value; } self.updatePrice(); } }); } // Gestion du sélecteur de couleur const colorPicker = $(CONFIG.options.color); if (colorPicker) { colorPicker.addEventListener(‘click’, function(e) { const option = e.target.closest(‘.sm-color-option’); if (option) { const radio = option.querySelector(‘input[type= »radio »]’); const colorInput = option.querySelector(‘input[type= »color »]’); // Désactive tous les autres colorPicker.querySelectorAll(‘.sm-color-option’).forEach(function(opt) { opt.classList.remove(‘sm-color-option–active’); }); // Active celui-ci option.classList.add(‘sm-color-option–active’); if (radio) { radio.checked = true; self.state.options.color = radio.value; self.updatePreviewColor(); } else if (colorInput) { // Ouvre le color picker natif colorInput.click(); } } }); // Gestion du color picker personnalisé const customColorInput = colorPicker.querySelector(‘input[type= »color »]’); if (customColorInput) { customColorInput.addEventListener(‘input’, function(e) { self.state.options.color = e.target.value; self.updatePreviewColor(); }); } } }, /** * Lie les événements pour l’upload de logo */ bindLogoUpload: function() { const self = this; const logoInput = $(‘#logo-upload’); const logoBtn = $(‘#logo-upload-btn’); const logoPreview = $(‘#logo-preview’); if (logoBtn && logoInput) { // Clic sur le bouton ouvre le sélecteur de fichier logoBtn.addEventListener(‘click’, function() { logoInput.click(); }); // Gestion du changement de fichier logoInput.addEventListener(‘change’, function(e) { const file = e.target.files[0]; if (file) { // Vérifie le type de fichier if (!file.type.match(/image\/(png|jpeg|jpg|svg\+xml)/)) { alert(‘Format non supporté. Utilisez PNG, JPG ou SVG.’); return; } // Vérifie la taille (max 5MB) if (file.size > 5 * 1024 * 1024) { alert(‘Fichier trop volumineux. Maximum 5 Mo.’); return; } // Crée une URL pour la prévisualisation const reader = new FileReader(); reader.onload = function(event) { self.state.logo = event.target.result; self.updateLogoPreview(); }; reader.readAsDataURL(file); } }); } }, /** * Lie les événements pour le switcher d’onglets (Modèles / Import) */ bindTabSwitcher: function() { const tabs = $$(‘.sm-source-tab’); tabs.forEach(function(tab) { tab.addEventListener(‘click’, function() { const targetTab = this.dataset.tab; // Désactive tous les onglets tabs.forEach(function(t) { t.classList.remove(‘sm-source-tab–active’); }); // Active l’onglet cliqué this.classList.add(‘sm-source-tab–active’); // Masque tous les contenus $$(‘.sm-source-content’).forEach(function(content) { content.classList.remove(‘sm-source-content–active’); }); // Affiche le contenu correspondant const targetContent = $(‘#tab-‘ + targetTab); if (targetContent) { targetContent.classList.add(‘sm-source-content–active’); } }); }); }, /** * Lie les événements pour le toggle Recto/Verso */ bindPreviewToggle: function() { const self = this; const toggles = $$(‘.sm-preview-toggle’); toggles.forEach(function(toggle) { toggle.addEventListener(‘click’, function() { const face = this.dataset.face; // Désactive tous les toggles toggles.forEach(function(t) { t.classList.remove(‘sm-preview-toggle–active’); }); // Active celui-ci this.classList.add(‘sm-preview-toggle–active’); // Affiche la face correspondante self.showPreviewFace(face); }); }); }, /** * Lie les événements pour la sélection de template */ bindTemplateSelector: function() { const templates = $$(‘.sm-template-card’); templates.forEach(function(template) { template.addEventListener(‘click’, function() { // Désactive tous les templates templates.forEach(function(t) { t.classList.remove(‘sm-template-card–active’); }); // Active celui-ci this.classList.add(‘sm-template-card–active’); // Le radio button est géré automatiquement }); }); }, /** * Met à jour un champ spécifique dans la prévisualisation * @param {string} fieldName – Nom du champ * @param {string} value – Nouvelle valeur */ updateField: function(fieldName, value) { // Stocke la valeur this.state.fields[fieldName] = value; // Récupère l’élément de prévisualisation const previewElement = $(CONFIG.preview[fieldName]); if (previewElement) { // Détermine la valeur à afficher let displayValue = value.trim(); // Applique les formatages spécifiques switch (fieldName) { case ‘phone’: displayValue = formatPhone(displayValue); break; case ‘website’: displayValue = formatUrl(displayValue); break; } // Si vide, affiche le placeholder if (!displayValue) { displayValue = CONFIG.placeholders[fieldName] ||  »; previewElement.classList.add(‘sm-preview-placeholder’); } else { previewElement.classList.remove(‘sm-preview-placeholder’); } // Met à jour le contenu (échappé pour la sécurité) previewElement.textContent = displayValue; // Animation subtile de mise à jour previewElement.classList.add(‘sm-preview-updated’); setTimeout(function() { previewElement.classList.remove(‘sm-preview-updated’); }, 300); } }, /** * Met à jour tous les champs de prévisualisation */ updateAllFields: function() { const self = this; Object.keys(CONFIG.fields).forEach(function(fieldName) { const input = $(CONFIG.fields[fieldName]); const value = input ? input.value :  »; self.updateField(fieldName, value); }); }, /** * Met à jour les informations de prévisualisation (format, papier, etc.) */ updatePreviewInfo: function() { const infoItems = $$(‘.sm-preview-info__item’); if (infoItems.length >= 3) { // Format const formatLabels = { ’85×55′: ’85 × 55 mm’, ’90×50′: ’90 × 50 mm’, ’85×85′: ’85 × 85 mm’, ’55×85′: ’55 × 85 mm’ }; const formatText = infoItems[0].querySelector(‘span:last-child’) || infoItems[0]; if (formatText.tagName === ‘SPAN’) { formatText.textContent = formatLabels[this.state.options.format] || this.state.options.format; } // Papier const paperLabels = { ‘350g-mat’: ‘350g Mat’, ‘350g-brillant’: ‘350g Brillant’, ‘400g-soft-touch’: ‘400g Soft Touch’, ‘350g-recycle’: ‘350g Recyclé’ }; // Mise à jour si l’élément existe // Impression const printLabels = { ‘recto’: ‘Recto seul’, ‘recto-verso’: ‘Recto / Verso’ }; // Mise à jour si l’élément existe } }, /** * Met à jour le cadre de prévisualisation selon le format/orientation */ updatePreviewFrame: function() { const frame = $(‘#preview-frame’); if (!frame) return; const format = this.state.options.format; const orientation = this.state.options.orientation; // Définit les ratios selon le format const ratios = { ’85×55′: { width: 85, height: 55 }, ’90×50′: { width: 90, height: 50 }, ’85×85′: { width: 85, height: 85 }, ’55×85′: { width: 55, height: 85 } }; let ratio = ratios[format] || ratios[’85×55′]; // Inverse si portrait if (orientation === ‘vertical’ && format !== ’85×85′) { const temp = ratio.width; ratio.width = ratio.height; ratio.height = temp; } // Applique le ratio frame.style.aspectRatio = ratio.width + ‘ / ‘ + ratio.height; }, /** * Met à jour la couleur principale dans la prévisualisation */ updatePreviewColor: function() { const color = this.state.options.color; const previewFrame = $(‘#preview-frame’); if (previewFrame) { // Met à jour la variable CSS personnalisée previewFrame.style.setProperty(‘–preview-primary-color’, color); // Met à jour les éléments qui utilisent la couleur const coloredElements = previewFrame.querySelectorAll(‘[data-color= »primary »]’); coloredElements.forEach(function(el) { el.style.color = color; }); } }, /** * Met à jour la prévisualisation du logo */ updateLogoPreview: function() { const logoPreviewSmall = $(‘#logo-preview’); const logoPreviewMain = $(‘#preview-logo’); if (this.state.logo) { // Preview dans le formulaire if (logoPreviewSmall) { logoPreviewSmall.innerHTML = ‘Logo‘; } // Preview dans la carte if (logoPreviewMain) { logoPreviewMain.innerHTML = ‘Logo‘; logoPreviewMain.classList.add(‘sm-preview-logo–filled’); } } }, /** * Affiche la face Recto ou Verso * @param {string} face – ‘recto’ ou ‘verso’ */ showPreviewFace: function(face) { const recto = $(‘#preview-recto’); const verso = $(‘#preview-verso’); if (face === ‘recto’) { if (recto) recto.style.display = ‘flex’; if (verso) verso.style.display = ‘none’; } else { if (recto) recto.style.display = ‘none’; if (verso) verso.style.display = ‘flex’; } }, /** * Met à jour le prix affiché */ updatePrice: function() { const quantityBtn = $(‘.sm-quantity-btn–active’); if (!quantityBtn) return; const basePrice = parseInt(quantityBtn.dataset.price, 10) || 0; const quantity = this.state.options.quantity; // Récupère les modificateurs de prix des options let priceModifier = 0; [‘format’, ‘paper’, ‘print’].forEach(function(optionType) { const activeBtn = $(‘#’ + optionType + ‘-options .sm-option-btn–active’); if (activeBtn && activeBtn.dataset.priceModifier) { priceModifier += parseInt(activeBtn.dataset.priceModifier, 10) || 0; } }); const totalPrice = basePrice + priceModifier; const pricePerUnit = (totalPrice / quantity).toFixed(2); // Met à jour les affichages const priceDisplay = $(‘#price-display’); const pricePerUnitDisplay = $(‘#price-per-unit’); const totalPriceDisplay = $(‘#total-price’); const summaryLine = $(‘.sm-product-summary__line span:last-child’); if (priceDisplay) { priceDisplay.textContent = totalPrice.toFixed(2).replace(‘.’, ‘,’) + ‘ €’; } if (pricePerUnitDisplay) { pricePerUnitDisplay.textContent = pricePerUnit.replace(‘.’, ‘,’) + ‘ € / carte’; } if (totalPriceDisplay) { totalPriceDisplay.textContent = totalPrice.toFixed(2).replace(‘.’, ‘,’) + ‘ €’; } } }; /* ============================================ GESTIONNAIRE D’UPLOAD DE FICHIER ============================================ */ const FileUploadManager = { /** * Initialise le gestionnaire d’upload */ init: function() { const uploadZone = $(‘#upload-zone’); const fileInput = $(‘#file-upload’); if (!uploadZone || !fileInput) return; this.bindDragAndDrop(uploadZone, fileInput); this.bindFileInput(fileInput); }, /** * Lie les événements drag & drop */ bindDragAndDrop: function(zone, input) { const self = this; // Empêche le comportement par défaut [‘dragenter’, ‘dragover’, ‘dragleave’, ‘drop’].forEach(function(eventName) { zone.addEventListener(eventName, function(e) { e.preventDefault(); e.stopPropagation(); }); }); // Effet visuel au survol [‘dragenter’, ‘dragover’].forEach(function(eventName) { zone.addEventListener(eventName, function() { zone.classList.add(‘sm-upload-zone–dragover’); }); }); [‘dragleave’, ‘drop’].forEach(function(eventName) { zone.addEventListener(eventName, function() { zone.classList.remove(‘sm-upload-zone–dragover’); }); }); // Gestion du drop zone.addEventListener(‘drop’, function(e) { const files = e.dataTransfer.files; if (files.length > 0) { self.handleFile(files[0]); } }); // Clic sur la zone ouvre le sélecteur zone.addEventListener(‘click’, function() { input.click(); }); }, /** * Lie l’événement de changement du file input */ bindFileInput: function(input) { const self = this; input.addEventListener(‘change’, function(e) { if (e.target.files.length > 0) { self.handleFile(e.target.files[0]); } }); }, /** * Traite le fichier uploadé */ handleFile: function(file) { // Formats acceptés const acceptedTypes = [ ‘application/pdf’, ‘image/png’, ‘image/jpeg’, ‘image/jpg’, ‘application/postscript’, // AI ‘image/vnd.adobe.photoshop’ // PSD ]; const acceptedExtensions = [‘.pdf’, ‘.png’, ‘.jpg’, ‘.jpeg’, ‘.ai’, ‘.psd’]; const extension = ‘.’ + file.name.split(‘.’).pop().toLowerCase(); // Vérifie le type if (!acceptedTypes.includes(file.type) && !acceptedExtensions.includes(extension)) { alert(‘Format non supporté.\nFormats acceptés : PDF, PNG, JPG, AI, PSD’); return; } // Vérifie la taille (max 50MB) if (file.size > 50 * 1024 * 1024) { alert(‘Fichier trop volumineux.\nTaille maximale : 50 Mo’); return; } // Affiche la preview this.showUploadPreview(file); }, /** * Affiche la prévisualisation du fichier uploadé */ showUploadPreview: function(file) { const uploadZone = $(‘#upload-zone’); const uploadPreview = $(‘#upload-preview’); const filenameEl = $(‘#uploaded-filename’); const filesizeEl = $(‘#uploaded-filesize’); const removeBtn = $(‘#remove-file’); if (uploadZone && uploadPreview) { // Masque la zone de drop uploadZone.style.display = ‘none’; // Affiche la preview uploadPreview.style.display = ‘block’; // Met à jour les infos if (filenameEl) { filenameEl.textContent = file.name; } if (filesizeEl) { const sizeMB = (file.size / (1024 * 1024)).toFixed(2); filesizeEl.textContent = sizeMB + ‘ Mo’; } // Gestion du bouton supprimer if (removeBtn) { removeBtn.addEventListener(‘click’, function() { uploadZone.style.display = ‘flex’; uploadPreview.style.display = ‘none’; // Reset le file input const fileInput = $(‘#file-upload’); if (fileInput) { fileInput.value =  »; } }, { once: true }); } } } }; /* ============================================ INITIALISATION ============================================ */ /** * Initialise tous les modules au chargement du DOM */ function init() { // Vérifie qu’on est sur une page produit if (!$(‘.sm-product-page’) && !$(‘.sm-product-form’)) { return; } // Initialise les gestionnaires PreviewManager.init(); FileUploadManager.init(); } // Attend que le DOM soit prêt if (document.readyState === ‘loading’) { document.addEventListener(‘DOMContentLoaded’, init); } else { init(); } /* ============================================ STYLES CSS DYNAMIQUES ============================================ */ // Injecte les styles pour les animations const style = document.createElement(‘style’); style.textContent = ` /* Animation de mise à jour */ .sm-preview-updated { animation: sm-pulse 0.3s ease; } @keyframes sm-pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } } /* Style pour les placeholders */ .sm-preview-placeholder { opacity: 0.5; font-style: italic; } /* Logo rempli */ .sm-preview-logo–filled { background-color: transparent !important; border: none !important; } `; document.head.appendChild(style); })();