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 = ‘
‘;
}
// Preview dans la carte
if (logoPreviewMain) {
logoPreviewMain.innerHTML = ‘
‘;
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);
})();