(function ($){
'use strict';
if(typeof cekonayConfig==='undefined'){
return;
}
class CekonayDimensions {
constructor(){
this.config=cekonayConfig;
this.priceCache={};
this.cartHandles=[];
this.$wrapper=$('#cekonay-dimensions');
this.$typeSelect=$('#cekonay_type');
this.$typeGroup=this.$typeSelect.closest('.form-group');
this.$hauteur=$('#cekonay_hauteur');
this.$largeur=$('#cekonay_largeur');
this.$poigneeSelect=$('#cekonay_poignee_select');
this.$poigneeHidden=$('#cekonay_poignee_hidden');
this.$poigneeSection=$('.poignees-section');
this.$poigneePreview=$('#poignee-preview');
this.$orientationChoice=$('#orientation-choice');
this.$addToCart=$('.single_add_to_cart_button');
this.$poigneeHoverPreview=$();
this.$poigneeModal=$();
this.$poigneeModalImage=$();
this.$poigneeModalTitle=$();
this.$surfaceValue=$('.surface-value');
this.$basePriceValue=$('.base-price-value');
this.$poigneePriceValue=$('.poignee-price-value');
this.$poigneePriceLine=$('.price-poignee-line');
this.$calculatedPrice=$('.calculated-price');
this.$priceCard=this.$wrapper.find('.price-display');
this.$priceEmptyHint=this.$wrapper.find('.price-empty-hint');
this.currentType=null;
this.currentPoignee=null;
this.debounceTimer=null;
this.ajaxRequest=null;
this.init();
}
init(){
if(this.$wrapper.length===0) return;
this.ensurePoigneePreviewUi();
this.bindEvents();
this.enforceNumericInputs();
this.toggleAddToCart(false);
this.loadCartHandles();
this.priceSwapped=false;
this.setBreakdownPlaceholder('Ajustez vos dimensions');
}
swapToCalculatedPrice(){
if(this.priceSwapped) return;
const $wooPrice=$('p.price, .summary .price').not('.cekonay-price-display').first();
if($wooPrice.length){
const initialPriceText=$wooPrice.find('.woocommerce-Price-amount').first().text().trim();
$wooPrice.hide();
$('#show_detail').hide();
let $priceDisplay=$('.cekonay-price-display');
if($priceDisplay.length===0){
$priceDisplay=$('<p class="price cekonay-price-display"><span class="woocommerce-Price-amount amount" style="color:#000;font-weight:700;"></span><small class="cekonay-price-hint" aria-live="polite"></small></p>');
$wooPrice.after($priceDisplay);
}
if(initialPriceText){
$priceDisplay.find('.woocommerce-Price-amount').text(initialPriceText);
}
this.priceSwapped=true;
}}
updateCalculatedPriceDisplay(){
const calculatedText=this.$calculatedPrice.text();
if(!calculatedText||calculatedText==='—') return;
this.swapToCalculatedPrice();
const $priceDisplay=$('.cekonay-price-display');
if($priceDisplay.length){
$priceDisplay.removeClass('is-pending');
$priceDisplay.find('.cekonay-price-hint').text('');
$priceDisplay.find('.woocommerce-Price-amount').text(calculatedText);
$priceDisplay.show();
}}
showTopPriceHint(message='Calcul en cours...'){
this.swapToCalculatedPrice();
const $priceDisplay=$('.cekonay-price-display');
if(!$priceDisplay.length) return;
$priceDisplay.addClass('is-pending');
$priceDisplay.find('.cekonay-price-hint').text(message);
}
clearTopPriceHint(){
const $priceDisplay=$('.cekonay-price-display');
if(!$priceDisplay.length) return;
$priceDisplay.removeClass('is-pending');
$priceDisplay.find('.cekonay-price-hint').text('');
}
loadCartHandles(){
$.ajax({
url: this.config.ajax_url,
method: 'POST',
data: {
action: 'cekonay_get_cart_handles'
},
success: (response)=> {
if(response.success&&response.data.poignees){
this.cartHandles=response.data.poignees;
}}
});
}
bindEvents(){
this.$typeSelect.on('change', ()=> this.handleTypeChange());
this.$hauteur.on('input', ()=> this.handleDimensionInput('hauteur'));
this.$largeur.on('input', ()=> this.handleDimensionInput('largeur'));
this.$poigneeSelect.on('change', ()=> this.handlePoigneeChange());
this.$priceCard.on('click', (e)=> this.handleCollapsedPriceCardClick(e));
this.$priceCard.on('keydown', (e)=> {
if(e.key==='Enter'||e.key===' '){
this.handleCollapsedPriceCardClick(e);
}});
$('input[name="cekonay_poignee_orientation"]').on('change', ()=> {
this.updatePoigneeImage();
this.calculatePrice();
});
this.$wrapper.on('mouseenter focusin', '.cekonay-poignee-thumb-button', (e)=> this.handlePoigneePreviewEnter(e));
this.$wrapper.on('mousemove', '.cekonay-poignee-thumb-button', (e)=> this.positionPoigneeHoverPreview(e));
this.$wrapper.on('mouseleave focusout', '.cekonay-poignee-thumb-button', ()=> this.hidePoigneeHoverPreview());
this.$wrapper.on('click', '.cekonay-poignee-thumb-button', (e)=> this.handlePoigneePreviewClick(e));
$(document).on('click', '.cekonay-poignee-modal, .cekonay-poignee-modal__close', (e)=> this.handlePoigneeModalClose(e));
$(document).on('keydown', (e)=> {
if(e.key==='Escape'){
this.closePoigneeModal();
}});
$('form.cart').on('submit', (e)=> this.handleFormSubmit(e));
}
ensurePoigneePreviewUi(){
if(!$('#cekonay-poignee-hover-preview').length){
$('body').append(`
<div id="cekonay-poignee-hover-preview" class="cekonay-poignee-hover-preview" aria-hidden="true">
<div class="cekonay-poignee-hover-preview__frame">
<img src="" alt="">
</div>
</div>
`);
}
if(!$('#cekonay-poignee-modal').length){
$('body').append(`
<div id="cekonay-poignee-modal" class="cekonay-poignee-modal" aria-hidden="true">
<div class="cekonay-poignee-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="cekonay-poignee-modal-title">
<button type="button" class="cekonay-poignee-modal__close" aria-label="Fermer la prévisualisation">×</button>
<div class="cekonay-poignee-modal__media">
<img src="" alt="">
</div>
<p id="cekonay-poignee-modal-title" class="cekonay-poignee-modal__title"></p>
</div>
</div>
`);
}
this.$poigneeHoverPreview=$('#cekonay-poignee-hover-preview');
this.$poigneeModal=$('#cekonay-poignee-modal');
this.$poigneeModalImage=this.$poigneeModal.find('img');
this.$poigneeModalTitle=$('#cekonay-poignee-modal-title');
}
supportsHoverPreview(){
return window.matchMedia&&window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}
getCurrentPoigneePreviewState(){
if(!this.currentPoignee) return null;
let orientation=$('input[name="cekonay_poignee_orientation"]:checked').val()||'horizontal';
if(this.currentPoignee.type==='fixe'){
orientation='horizontal';
}
const hasDedicatedVerticalImage = !!this.currentPoignee.imageVertical;
let imageUrl='';
if(orientation==='vertical'){
imageUrl=this.currentPoignee.imageVertical||this.currentPoignee.imageHorizontal||this.currentPoignee.image||'';
}else{
imageUrl=this.currentPoignee.imageHorizontal||this.currentPoignee.image||this.currentPoignee.imageVertical||'';
}
let rotateDeg=0;
const rotationValue=String(this.currentPoignee.imageVerticalRotation||'none');
if(orientation==='vertical'&&!hasDedicatedVerticalImage&&imageUrl&&['90', '-90', '180'].includes(rotationValue)){
rotateDeg=parseInt(rotationValue, 10);
}
return {
imageUrl: imageUrl,
rotateDeg: rotateDeg,
nom: this.currentPoignee.nom||'',
orientation: orientation
};}
renderPoigneePlaceholder(){
return `<div class="cekonay-poignee-thumb-placeholder" aria-hidden="true">${this.escapeHtml(this.currentPoignee.nom.charAt(0))}</div>`;
}
buildPoigneePreviewButton(previewState){
if(!previewState||!previewState.imageUrl){
return this.renderPoigneePlaceholder();
}
const imageStyle=previewState.rotateDeg!==0 ? ` style="transform:rotate(${previewState.rotateDeg}deg);"`:'';
return `
<button
type="button"
class="cekonay-poignee-thumb-button"
data-preview-src="${this.escapeHtml(previewState.imageUrl)}"
data-preview-rotation="${previewState.rotateDeg}"
data-preview-alt="${this.escapeHtml(previewState.nom)}"
aria-label="Agrandir la prévisualisation de ${this.escapeHtml(previewState.nom)}"
>
<span class="cekonay-poignee-thumb-frame">
<img src="${this.escapeHtml(previewState.imageUrl)}" alt="${this.escapeHtml(previewState.nom)}"${imageStyle}>
</span>
<span class="cekonay-poignee-thumb-hint">Agrandir</span>
</button>
`;
}
setPoigneePreviewImage($img, src, alt, rotateDeg=0){
if(!$img.length) return;
$img.attr({
src: src,
alt: alt||''
});
$img.css('transform', rotateDeg!==0 ? `rotate(${rotateDeg}deg)`:'none');
}
handlePreviewImageError($img){
const $button=$img.closest('.cekonay-poignee-thumb-button');
if($button.length){
$button.replaceWith(this.renderPoigneePlaceholder());
return;
}
const $media=$img.closest('.cekonay-poignee-modal__media, .cekonay-poignee-hover-preview__frame');
$media.addClass('is-error');
$img.removeAttr('src');
}
handlePoigneePreviewEnter(e){
if(!this.supportsHoverPreview()) return;
const $button=$(e.currentTarget);
const src=$button.data('preview-src');
if(!src) return;
const rotateDeg=parseInt($button.data('preview-rotation'), 10)||0;
const alt=$button.data('preview-alt')||'';
const $img=this.$poigneeHoverPreview.find('img');
this.$poigneeHoverPreview.find('.cekonay-poignee-hover-preview__frame').removeClass('is-error');
this.setPoigneePreviewImage($img, src, alt, rotateDeg);
$img.off('error.cekonayHover').on('error.cekonayHover', ()=> this.handlePreviewImageError($img));
this.$poigneeHoverPreview.addClass('is-visible').attr('aria-hidden', 'false');
this.positionPoigneeHoverPreview(e);
}
positionPoigneeHoverPreview(e){
if(!this.supportsHoverPreview()||!this.$poigneeHoverPreview.hasClass('is-visible')) return;
const previewWidth=this.$poigneeHoverPreview.outerWidth()||260;
const previewHeight=this.$poigneeHoverPreview.outerHeight()||260;
const gutter=20;
let left=e.clientX + gutter;
let top=e.clientY + gutter;
if(left + previewWidth > window.innerWidth - 16){
left=e.clientX - previewWidth - gutter;
}
if(top + previewHeight > window.innerHeight - 16){
top=window.innerHeight - previewHeight - 16;
}
left=Math.max(16, left);
top=Math.max(16, top);
this.$poigneeHoverPreview.css({
left: `${left}px`,
top: `${top}px`
});
}
hidePoigneeHoverPreview(){
this.$poigneeHoverPreview.removeClass('is-visible').attr('aria-hidden', 'true');
}
handlePoigneePreviewClick(e){
e.preventDefault();
const $button=$(e.currentTarget);
const src=$button.data('preview-src');
if(!src) return;
this.openPoigneeModal({
src: src,
alt: $button.data('preview-alt')||'',
rotateDeg: parseInt($button.data('preview-rotation'), 10)||0
});
}
openPoigneeModal(preview){
const $media=this.$poigneeModal.find('.cekonay-poignee-modal__media');
$media.removeClass('is-error');
this.hidePoigneeHoverPreview();
this.setPoigneePreviewImage(this.$poigneeModalImage, preview.src, preview.alt, preview.rotateDeg);
this.$poigneeModalImage.off('error.cekonayModal').on('error.cekonayModal', ()=> this.handlePreviewImageError(this.$poigneeModalImage));
this.$poigneeModalTitle.text(preview.alt||'');
this.$poigneeModal.addClass('is-open').attr('aria-hidden', 'false');
$('body').addClass('cekonay-poignee-modal-open');
}
handlePoigneeModalClose(e){
if($(e.target).is('.cekonay-poignee-modal__dialog, .cekonay-poignee-modal__media, .cekonay-poignee-modal__media *')){
return;
}
this.closePoigneeModal();
}
closePoigneeModal(){
this.$poigneeModal.removeClass('is-open').attr('aria-hidden', 'true');
$('body').removeClass('cekonay-poignee-modal-open');
}
handleCollapsedPriceCardClick(e){
if(!this.$priceCard.hasClass('is-empty')) return;
e.preventDefault();
if(!this.currentType){
this.showTypeRequiredHint(true);
return;
}
this.$hauteur.trigger('focus');
}
enforceNumericInputs(){
$('.cekonay-number').on('keypress', function (e){
const allowedKeys=[8, 9, 37, 38, 39, 40, 46];
const key=e.which||e.keyCode;
if(allowedKeys.indexOf(key)!==-1){
return true;
}
if(key < 48||key > 57){
e.preventDefault();
return false;
}});
$('.cekonay-number').on('paste', function (e){
e.preventDefault();
const pasteData=(e.originalEvent||e).clipboardData.getData('text/plain');
const numericOnly=pasteData.replace(/[^0-9]/g, '').substring(0, 4);
$(this).val(numericOnly).trigger('input');
});
$('.cekonay-number').on('input', function (){
let val=$(this).val().replace(/[^0-9]/g, '');
if(val.length > 4){
val=val.substring(0, 4);
}
$(this).val(val);
});
}
handleTypeChange(){
const $selected=this.$typeSelect.find(':selected');
const type=$selected.val();
if(!type){
this.currentType=null;
this.resetInputs();
this.toggleAddToCart(false);
return;
}
this.clearTypeRequiredHint();
this.currentType={
key: type,
hauteur: {
min: parseInt($selected.data('h-min'), 10),
max: parseInt($selected.data('h-max'), 10)
},
largeur: {
min: parseInt($selected.data('l-min'), 10),
max: parseInt($selected.data('l-max'), 10)
}};
this.$hauteur.attr({
'min': this.currentType.hauteur.min,
'max': this.currentType.hauteur.max,
'placeholder': 'En mm'
});
this.$largeur.attr({
'min': this.currentType.largeur.min,
'max': this.currentType.largeur.max,
'placeholder': 'En mm'
});
if(type==='porte'||type==='tiroir'){
this.$poigneeSection.slideDown();
}else{
this.$poigneeSection.slideUp();
this.$poigneeSelect.val('');
this.$poigneePreview.hide();
$('#poignee-image-container').empty();
$('.cekonay-handle-alert').remove();
this.currentPoignee=null;
this.hidePoigneeHoverPreview();
this.closePoigneeModal();
}
this.clearTypeHints();
this.updateTypeHints();
this.updateConversion(this.$hauteur, parseInt(this.$hauteur.val(), 10), 'hauteur');
this.updateConversion(this.$largeur, parseInt(this.$largeur.val(), 10), 'largeur');
this.calculatePrice();
}
handleDimensionInput(type){
const $input=type==='hauteur' ? this.$hauteur:this.$largeur;
const rawValue=($input.val()||'').trim();
const value=parseInt(rawValue, 10);
if(!this.currentType){
if(rawValue!==''){
this.showTypeRequiredHint(true);
}else{
this.clearTypeRequiredHint();
}
this.setBreakdownPlaceholder('Sélectionnez un type');
this.toggleAddToCart(false);
return;
}
this.clearTypeRequiredHint();
this.updateConversion($input, value, type);
clearTimeout(this.debounceTimer);
this.debounceTimer=setTimeout(()=> {
this.validateDimension($input, value, type);
this.updateOrientationVisibility();
this.updateTypeHints();
this.calculatePrice();
}, 300);
}
clearTypeHints(){
$('.cekonay-fileur-info, .cekonay-plinthe-tip, .cekonay-plinthe-rule').remove();
}
showTypeRequiredHint(shouldFocusSelect=false){
if(!this.$typeGroup.length) return;
let $hint=this.$typeGroup.find('.cekonay-type-required-hint');
if(!$hint.length){
$hint=$('<small class="cekonay-type-required-hint" role="status"></small>');
this.$typeGroup.append($hint);
}
$hint.html('Commencez par sélectionner le type (Porte, Façade, Tiroir...),<br>puis saisissez vos dimensions.');
this.$typeSelect.removeClass('error').addClass('guidance').removeAttr('aria-invalid');
if(shouldFocusSelect){
this.$typeSelect.trigger('focus');
}}
clearTypeRequiredHint(){
if(!this.$typeGroup.length) return;
this.$typeGroup.find('.cekonay-type-required-hint').remove();
this.$typeSelect.removeClass('guidance error').removeAttr('aria-invalid');
}
isSyncronLikeContext(){
if(this.config.is_syncron_like) return true;
const source=String(this.config.grille_source||'').toLowerCase();
const nom=String(this.config.grille_nom||'').toLowerCase();
return source.indexOf('syncron')!==-1||nom.indexOf('syncron')!==-1;
}
updateTypeHints(){
this.clearTypeHints();
if(!this.currentType) return;
if(this.currentType.key==='fileur'){
const largeur=parseInt(this.$largeur.val(), 10);
if(!isNaN(largeur)&&largeur > 100){
const infoHtml=`<div class="cekonay-fileur-info" style="background:#fff3cd;border:1px solid #ffc107;border-radius:4px;padding:10px 12px;margin-top:10px;font-size:13px;color:#856404;display:flex;align-items:flex-start;gap:8px;"><span style="font-size:16px;">ℹ️</span><span>Au-delà de 100mm de largeur, les fileurs ne comportent pas de chant sur les côtés.</span></div>`;
this.$largeur.closest('.form-group').append(infoHtml);
}
return;
}
if(this.currentType.key!=='plinthe'){
return;
}
const ruleMessage=(this.config.plinthe_rule_message||'').trim();
const ruleHtml=`<div class="cekonay-plinthe-rule" style="background:#fdf5d9;border:1px solid #e0c86c;border-radius:4px;padding:10px 12px;margin-top:10px;font-size:13px;color:#7a5d00;display:flex;align-items:flex-start;gap:8px;"><span style="font-size:16px;">📏</span><span><strong>Règle plinthe :</strong> ${this.escapeHtml(ruleMessage||'limites selon la grille tarifaire active.')}</span></div>`;
this.$hauteur.closest('.form-group').append(ruleHtml);
if(!this.isSyncronLikeContext()){
return;
}
const fileurLimits=this.config.types_config&&this.config.types_config.fileur ? this.config.types_config.fileur:null;
if(!fileurLimits) return;
const plintheMax=parseInt(this.currentType.largeur.max, 10);
const fileurMaxLongueur=parseInt(fileurLimits.h_max, 10);
const fileurMaxLargeur=parseInt(fileurLimits.l_max, 10);
if(isNaN(plintheMax)||isNaN(fileurMaxLongueur)||isNaN(fileurMaxLargeur)||fileurMaxLongueur <=plintheMax){
return;
}
const infoHtml=`<div class="cekonay-plinthe-tip" style="background:#e9f7ef;border:1px solid #86d7a4;border-radius:4px;padding:10px 12px;margin-top:10px;font-size:13px;color:#155724;display:flex;align-items:flex-start;gap:8px;"><span style="font-size:16px;">💡</span><span><strong>Astuce Syncron :</strong> si la longueur de plinthe est limitée et que le sens du décor n'est pas bloquant pour vous, vous pouvez demander un <strong>fileur</strong> (saisie en <strong>HxL</strong>). Longueur possible jusqu'à <strong>${fileurMaxLongueur} mm</strong> (largeur jusqu'à <strong>${fileurMaxLargeur} mm</strong> selon la grille).</span></div>`;
this.$largeur.closest('.form-group').append(infoHtml);
}
handlePoigneeChange(){
const $selected=this.$poigneeSelect.find(':selected');
const poigneeId=$selected.val();
$('.cekonay-handle-alert').remove();
if(!poigneeId||poigneeId===''||poigneeId==='0'){
this.$poigneePreview.hide();
this.$poigneeHidden.val('');
this.currentPoignee=null;
$('#poignee-image-container').empty();
this.hidePoigneeHoverPreview();
this.closePoigneeModal();
this.calculatePrice();
return;
}
const poigneeType=$selected.data('type');
const isFixe=(poigneeType==='fixe');
this.currentPoignee={
id: poigneeId,
nom: $selected.data('nom'),
type: poigneeType,
prix_fixe: parseFloat($selected.data('prix-fixe')),
prix_mm: parseFloat($selected.data('prix-mm')),
horizontal: parseInt($selected.data('horizontal')),
vertical: parseInt($selected.data('vertical')),
image: $selected.data('image'),
imageHorizontal: $selected.data('image-horizontal')||'',
imageVertical: $selected.data('image-vertical')||'',
imageVerticalRotation: String($selected.data('image-vertical-rotation')||'none')
};
this.$poigneeHidden.val(poigneeId);
if(this.cartHandles.length > 0&&!this.cartHandles.includes(this.currentPoignee.nom)){
const alertHtml=`
<div class="cekonay-handle-alert">
<div style="display: flex; align-items: flex-start;">
<span style="font-weight: bold; margin-right: 6px;">⚠️</span>
<div>
Un type de poignée différent,
<strong>${this.escapeHtml(this.cartHandles.join(', '))}</strong> est présent dans votre panier.
<div style="color: #000; margin-top: 4px;">
Si c'est volontaire, vous pouvez poursuivre.
</div>
</div>
</div>
</div>
`;
this.$poigneeSelect.after(alertHtml);
}
this.updatePoigneeImage();
$('#poignee-nom-display').text(this.currentPoignee.nom);
if(isFixe){
this.$orientationChoice.hide();
this.$poigneePreview.show();
}else{
this.updateOrientationVisibility();
this.$poigneePreview.show();
}
this.calculatePrice();
}
updatePoigneeImage(){
if(!this.currentPoignee) return;
const previewState=this.getCurrentPoigneePreviewState();
const imageHtml=previewState&&previewState.imageUrl
? this.buildPoigneePreviewButton(previewState)
: this.renderPoigneePlaceholder();
$('#poignee-image-container').html(imageHtml);
const $thumb=$('#poignee-image-container').find('img');
if($thumb.length){
$thumb.off('error.cekonayThumb').on('error.cekonayThumb', ()=> this.handlePreviewImageError($thumb));
}}
updateOrientationVisibility(){
if(!this.currentPoignee||!this.currentType){
this.$orientationChoice.hide();
return;
}
if(this.currentPoignee.type==='fixe'){
this.$orientationChoice.hide();
return;
}
if(this.currentType.key==='tiroir'){
this.$orientationChoice.hide();
$('input[name="cekonay_poignee_orientation"][value="horizontal"]').prop('checked', true);
return;
}
if(this.currentType.key==='porte'){
const canHorizontal=this.currentPoignee.horizontal===1;
const canVertical=this.currentPoignee.vertical===1;
if(canHorizontal&&canVertical){
this.$orientationChoice.show();
}else if(canHorizontal){
this.$orientationChoice.hide();
$('input[name="cekonay_poignee_orientation"][value="horizontal"]').prop('checked', true);
}else if(canVertical){
this.$orientationChoice.hide();
$('input[name="cekonay_poignee_orientation"][value="vertical"]').prop('checked', true);
}else{
this.$orientationChoice.hide();
}}
}
validateDimension($input, value, type){
if(!this.currentType||isNaN(value)){
this.clearValidation($input);
return false;
}
const limits=this.currentType[type];
if(value < limits.min||value > limits.max){
this.showError($input);
return false;
}
this.showSuccess($input);
return true;
}
showError($input){
$input.addClass('error').removeClass('valid');
}
showSuccess($input){
$input.addClass('valid').removeClass('error');
}
clearValidation($input){
$input.removeClass('error valid');
$input.closest('.form-group').find('.conversion-cm').removeClass('is-limit-error');
}
getDimensionHint(value, type){
if(isNaN(value)||value===0){
return { text: '', isError: false };}
if(this.currentType&&this.currentType[type]){
const limits=this.currentType[type];
if(value < limits.min){
return { text: `Min ${limits.min} mm`, isError: true };}
if(value > limits.max){
return { text: `Max ${limits.max} mm`, isError: true };}}
const cm=(value / 10).toFixed(1);
return { text: `soit ${cm} cm`, isError: false };}
updateConversion($input, value, type=null){
const $conversion=$input.closest('.form-group').find('.conversion-cm');
const hint=this.getDimensionHint(value, type);
$conversion
.text(hint.text)
.toggleClass('is-limit-error', !!hint.isError);
}
setBreakdownPlaceholder(message='Ajustez vos dimensions'){
this.$surfaceValue.text(message);
this.$basePriceValue.text('—');
this.$poigneePriceLine.hide();
this.$calculatedPrice.text('—');
this.$priceCard.addClass('is-empty');
this.$priceCard.attr({
'role': 'button',
'tabindex': '0',
'aria-expanded': 'false',
'aria-label': 'Ouvrir la saisie des dimensions'
});
if(this.$priceEmptyHint.length){
this.$priceEmptyHint.text((message||'Renseignez vos dimensions') + ' — Cliquez ici pour commencer');
}}
calculatePrice(){
if(!this.currentType){
this.setBreakdownPlaceholder('Sélectionnez un type');
this.toggleAddToCart(false);
this.clearTopPriceHint();
return;
}
const hauteur=parseInt(this.$hauteur.val(), 10);
const largeur=parseInt(this.$largeur.val(), 10);
if(isNaN(hauteur)||isNaN(largeur)){
this.setBreakdownPlaceholder('Ajustez vos dimensions');
this.toggleAddToCart(false);
if(this.priceSwapped){
this.showTopPriceHint('Ajustez vos dimensions pour calculer');
}else{
this.clearTopPriceHint();
}
return;
}
if(!this.validateDimension(this.$hauteur, hauteur, 'hauteur') ||
!this.validateDimension(this.$largeur, largeur, 'largeur')){
this.setBreakdownPlaceholder('Dimensions hors plage');
this.toggleAddToCart(false);
if(this.priceSwapped){
this.showTopPriceHint('Dimensions hors limites, corrigez vos mesures');
}else{
this.clearTopPriceHint();
}
return;
}
const surface=(hauteur / 1000) * (largeur / 1000);
const poigneeId=this.currentPoignee ? this.currentPoignee.id:0;
const orientation=$('input[name="cekonay_poignee_orientation"]:checked').val()||'horizontal';
const cacheKey=`${this.currentType.key}_${hauteur}_${largeur}_${poigneeId}_${orientation}`;
if(this.priceCache[cacheKey]){
const cached=this.priceCache[cacheKey];
this.updatePriceDisplay(surface, cached.prix_base, cached.prix_poignee, cached.prix_total);
this.toggleAddToCart(true);
return;
}
if(this.ajaxRequest){
this.ajaxRequest.abort();
}
this.showTopPriceHint('Calcul en cours...');
this.ajaxRequest=$.ajax({
url: this.config.ajax_url,
method: 'POST',
data: {
action: 'cekonay_calculate_price',
grille_id: this.config.grille_id,
type: this.currentType.key,
hauteur: hauteur,
largeur: largeur,
product_id: this.config.product_id||0,
poignee_id: poigneeId,
orientation: orientation,
nonce: this.config.nonce
},
success: (response)=> {
if(response.success){
const data=response.data;
this.priceCache[cacheKey]={
prix_base: data.prix_base,
prix_poignee: data.prix_poignee,
prix_total: data.prix_total
};
this.updatePriceDisplay(data.surface, data.prix_base, data.prix_poignee, data.prix_total);
this.toggleAddToCart(true);
}else{
this.showNotice(response.data.message||'Erreur de calcul', 'error');
this.toggleAddToCart(false);
}},
error: (xhr, status)=> {
if(status!=='abort'){
if(xhr.status===403){
this.refreshNonceAndRetry();
}else{
this.showNotice('<a href="javascript:location.reload();" style="color:#fff;text-decoration:underline;">Actualiser votre page</a>', 'error');
this.toggleAddToCart(false);
}}
},
complete: ()=> {
this.ajaxRequest=null;
}});
}
updatePriceDisplay(surface, prixBase, prixPoignee, prixTotal){
this.$surfaceValue.text(this.formatNumber(surface) + ' m²');
this.$basePriceValue.text(this.formatPrice(prixBase));
if(prixPoignee&&prixPoignee > 0){
this.$poigneePriceLine.show();
this.$poigneePriceValue.text(this.formatPrice(prixPoignee));
}else{
this.$poigneePriceLine.hide();
}
this.$calculatedPrice.text(this.formatPrice(prixTotal));
this.$priceCard.removeClass('is-empty');
this.$priceCard.removeAttr('role tabindex aria-label');
this.$priceCard.attr('aria-expanded', 'true');
if(this.$priceEmptyHint.length){
this.$priceEmptyHint.text('Renseignez vos dimensions pour afficher le prix');
}
this.$calculatedPrice.addClass('updated');
setTimeout(()=> {
this.$calculatedPrice.removeClass('updated');
}, 300);
this.clearTopPriceHint();
this.updateCalculatedPriceDisplay();
}
formatPrice(price){
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
formatNumber(num){
return new Intl.NumberFormat('fr-FR', {
minimumFractionDigits: 2,
maximumFractionDigits: 3
}).format(num);
}
toggleAddToCart(enable){
if(this.$addToCart.length===0) return;
if(enable){
this.$addToCart.prop('disabled', false).removeClass('disabled');
}else{
this.$addToCart.prop('disabled', true).addClass('disabled');
}}
refreshNonceAndRetry(){
$.ajax({
url: this.config.ajax_url,
method: 'POST',
data: {
action: 'cekonay_refresh_nonce'
},
success: (response)=> {
if(response.success&&response.data){
if(response.data.nonce){
this.config.nonce=response.data.nonce;
}
if(response.data.cart_nonce){
this.config.cart_nonce=response.data.cart_nonce;
}
if(response.data.wc_nonce){
this.config.wc_nonce=response.data.wc_nonce;
}
this.calculatePrice();
}else{
this.showNotice('Impossible de rafraîchir la session', 'error');
this.toggleAddToCart(false);
}},
error: ()=> {
this.showNotice('Erreur de connexion', 'error');
this.toggleAddToCart(false);
}});
}
validateBeforeAddToCart(e){
if(!this.currentType){
e.preventDefault();
this.showNotice('Veuillez sélectionner un type de mobilier', 'error');
return false;
}
const hauteur=parseInt(this.$hauteur.val(), 10);
const largeur=parseInt(this.$largeur.val(), 10);
if(isNaN(hauteur)||isNaN(largeur)){
e.preventDefault();
this.showNotice('Veuillez renseigner les dimensions', 'error');
return false;
}
if(!this.validateDimension(this.$hauteur, hauteur, 'hauteur') ||
!this.validateDimension(this.$largeur, largeur, 'largeur')){
e.preventDefault();
this.showNotice('Les dimensions saisies sont invalides', 'error');
return false;
}
return true;
}
handleFormSubmit(e){
if(!this.validateBeforeAddToCart(e)){
return false;
}
e.preventDefault();
const $form=$(e.target);
const $button=this.$addToCart;
$button.prop('disabled', true).addClass('loading');
let productId=$form.find('input[name="add-to-cart"]').val() ||
$form.find('button[name="add-to-cart"]').val() ||
$form.find('input[name="product_id"]').val();
if(!productId){
productId=$button.val()||$button.data('product_id');
}
let formData=$form.serialize() + '&action=cekonay_add_to_cart_with_calculator';
if(productId){
formData +='&product_id=' + productId;
}
formData=formData.replace(/cekonay_nonce=[^&]*/, '');
const currentNonce=this.config.cart_nonce||this.config.nonce;
formData +='&cekonay_nonce=' + encodeURIComponent(currentNonce);
$.ajax({
type: 'POST',
url: this.config.ajax_url,
data: formData,
success: (response)=> {
$button.prop('disabled', false).removeClass('loading');
if(response&&response.success===false){
const errorMsg=response.data&&response.data.message
? response.data.message
: 'Erreur lors de l\'ajout au panier';
this.showNotice(errorMsg, 'error');
return;
}
if(!response||!response.fragments){
this.showNotice('Erreur lors de l\'ajout au panier', 'error');
return;
}
if(response.fragments){
$.each(response.fragments, function(key, value){
$(key).replaceWith(value);
});
}
$(document.body).trigger('wc_fragments_refreshed');
$(document.body).trigger('added_to_cart', [response.fragments, response.cart_hash, $button]);
this.showNotice('Produit ajouté au panier', 'success');
this.loadCartHandles();
},
error: (xhr, status, error)=> {
$button.prop('disabled', false).removeClass('loading');
this.showNotice('Erreur lors de l\'ajout au panier', 'error');
}});
return false;
}
showNotice(message, type='info'){
const $notice=$(`
<div class="woocommerce-message cekonay-notice ${type}">
${this.escapeHtml(message)}
</div>
`);
this.$wrapper.before($notice);
setTimeout(()=> {
$notice.fadeOut(()=> $notice.remove());
}, 5000);
}
escapeHtml(text){
const map={
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m=> map[m]);
}
resetInputs(){
this.$hauteur.val('').attr({ 'min': '', 'max': '', 'placeholder': '' });
this.$largeur.val('').attr({ 'min': '', 'max': '', 'placeholder': '' });
this.$poigneeSelect.val('');
this.$poigneeHidden.val('');
this.$poigneePreview.hide();
$('#poignee-image-container').empty();
this.$poigneeSection.hide();
$('.cekonay-handle-alert').remove();
this.clearTypeHints();
this.clearTypeRequiredHint();
this.currentPoignee=null;
this.hidePoigneeHoverPreview();
this.closePoigneeModal();
$('.conversion-cm').text('').removeClass('is-limit-error');
this.$hauteur.add(this.$largeur).removeClass('error valid');
this.setBreakdownPlaceholder();
this.clearTopPriceHint();
this.priceCache={};}}
$(document).ready(function (){
new CekonayDimensions();
});
})(jQuery);