feat(new-layout): add-to-cart handler + piece configurator (Phase 02 plans 01-02)

Plan 02-01 (piece/crop configurator, complete):
- #piece reuse z shared partial product-cover-thumbnails.tpl
- 8 hidden inputs (is_crop, crop_pos_x/y, crop_width/height, piece_bg_top/left, is_reflection) w formie #add-to-cart-or-refresh
- Defensive setup w custom.js: setTimeout(600) init, no-op override totalpriceinfospecific/prod, DOM stubs
- CSS scope pod body#product .product-size-data .product-size-data--new

Plan 02-02 (add-to-cart submission, PARTIAL):
- Capture-phase native addEventListener (useCapture=true) blokuje PS core crash
  (button poza formą w nowym layoucie — closest('form') zwracało 0)
- Manualny AJAX POST: form.serialize() + qty + add=1&action=update do /pl/koszyk
- Fancybox-blocker port z custom.js:327 (nie odpalał się bo selector 0 matches)
- Manual sync is_crop/crop_width/height przed POST (obejście crash checkedHandler)
- prestashop.emit('updatedCart') + defensive blockcart refresh fetch
- Loading spinner + success flash CSS
- Inline handler mirror w product.tpl z idempotency guard (window.__p02p02Bound)
  — cache-buster dla browser cachowanego custom.js

Deferred do Plan 02-03 (customization + modal blocker dla production):
- Customization nie zapisuje się (squaremeter hook gate'owany discretion=on + brak dimension fields)
- Success modal (wymaga POST do /module/ps_shoppingcart/ajax)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 23:33:45 +02:00
parent 161c090ef0
commit 7ac795ba3f
24 changed files with 5447 additions and 2569 deletions

View File

@@ -537,7 +537,12 @@ console.log('t');
$('#checkbox-piece').prop('checked', true);
$('#checkbox-piece').trigger('change');
$('html').animate({scrollTop: $('.pp_stick_parent').offset().top - 100});
// Phase 02 Plan 02-01: defensive offset check — .pp_stick_parent istnieje tylko w starym layoucie.
// Bez tego guard'a .offset() zwraca undefined w nowym layoucie i handler aborts przed $.fancybox().
var $ppStick = $('.pp_stick_parent');
if ($ppStick.length) {
$('html').animate({scrollTop: $ppStick.offset().top - 100});
}
$.fancybox({
maxWidth: 600,
minHeight: 420,
@@ -620,6 +625,115 @@ $(document).on('click', '#box-color-variants .wariant_kolorystyczny', function()
$("#box-color-variants").fadeOut()
})
/* NEW layout — klik w kafelek wariantu zmienia wariant (delegowany, przeżywa refresh AJAX) */
$(document).on('click', '.product-variants-data--new .wariant_kolorystyczny label', function (e) {
var $label = $(this);
var $radio = $label.find('input.input-color');
if (!$radio.length) return;
if ($radio.is(':checked')) return;
$radio.prop('checked', true).trigger('change');
});
/* NEW layout — refresh wariantu produktu bez przeładowania strony.
PS core handler updateProduct_ szuka formy przez $('.product-actions').find('form:first'),
ale w nowym layoucie .product-actions nie istnieje tam, gdzie jest forma wariantów.
Robimy więc ręczny refresh (POST na bieżący URL produktu, action=refresh zwraca JSON
z HTML-fragmentami wariantu) i aktualizujemy kluczowe elementy in-place. */
$(document).on('change', '.product-variants-data--new input[name^="group["]', function () {
var $form = $('.product-variants-data--new #add-to-cart-or-refresh');
if (!$form.length) return;
var data = {};
$form.serializeArray().forEach(function (f) { data[f.name] = f.value; });
data.ajax = 1;
data.action = 'refresh';
data.quantity_wanted = 1;
var productUrl = window.location.href.split('?')[0].split('#')[0];
$.ajax({
url: productUrl,
type: 'POST',
data: data,
dataType: 'json',
headers: { 'Accept': 'application/json' },
success: function (resp) {
if (!resp) { window.location.reload(); return; }
try {
if (resp.product_url) {
history.pushState({}, '', resp.product_url);
}
if (resp.product_prices) {
var $p = $('.product-prices-data .product-prices');
if ($p.length) $p.replaceWith(resp.product_prices);
}
if (resp.product_cover_thumbnails) {
$('.product_image_wrapper').html(resp.product_cover_thumbnails);
}
if (window.prestashop && typeof prestashop.emit === 'function') {
prestashop.emit('updatedProduct', resp);
}
} catch (e) {
window.location.href = resp.product_url || window.location.href;
}
},
error: function () { window.location.reload(); }
});
});
// Phase 02 Plan 02-01: piece re-sync po variant AJAX refresh.
// #piece jest sibling-em .product_image_wrapper (nie dzieckiem) wiec przezywa .html() replace.
// Po zmianie wariantu re-triggerujemy change na width/height zeby odswiezyc background-position
// (rozmiar kontenera moze sie zmienic miedzy wariantami).
if (window.prestashop && typeof prestashop.on === 'function') {
prestashop.on('updatedProduct', function () {
// #piece jest re-renderowany przez product-cover-thumbnails.tpl (rendered inside resp.product_cover_thumbnails).
// Po replace .product_image_wrapper.html(...) dragElement trzeba zapiac ponownie na nowy node.
if ($('#product_is_crop').val() === '1' && document.getElementById('piece') && typeof dragElement === 'function') {
dragElement(document.getElementById('piece'));
$('#piece-width').trigger('change');
$('#piece-height').trigger('change');
}
});
}
// Phase 02 Plan 02-01: setup defensywny dla piece/crop w nowym layoucie.
// Piece pojawia sie DOPIERO po kliknieciu .fancybox-size-controls (user feedback).
// Dlatego NIE wywolujemy checkedHandler automatycznie — tylko przygotowujemy srodowisko,
// aby pozniejsze kliknieciem popup'a nie crashowalo:
// 1) override totalpriceinfospecific/prod (crash na brakujacym #product-details/totalpriceinfo w nowym layoucie)
// 2) wstrzyknij stuby DOM (elementy wymagane przez module-hook inline scripts)
setTimeout(function () {
if (!$('.product-variants-data--new').length) return;
// totalpriceinfospecific() jest wywolywana przez #piece-width change handler (custom.js:281)
// oraz przez kilka innych flow. Ma wiele DOM dependencies ktore nie istnieja w nowym layoucie.
// Nadpisujemy no-op — cena w nowym layoucie jest przekalkulowywana gdzie indziej.
if (typeof window.totalpriceinfospecific === 'function') {
window.totalpriceinfospecific = function () { /* no-op in new layout */ };
}
if (typeof window.prod === 'function') {
var _origProd = window.prod;
window.prod = function () { try { return _origProd.apply(this, arguments); } catch (e) { /* swallow */ } };
}
// Stub elementy dla pozostalych inline-script hooks.
['totalpriceinfo', 'custom-wallpaper-price', 'custom-wallpaper-price-label'].forEach(function (id) {
if (!document.getElementById(id)) {
var el = document.createElement('div');
el.id = id;
el.style.display = 'none';
document.body.appendChild(el);
}
});
['quantity_wanted', 'quantity_wanted_alt', 'quantity_wanted_alth'].forEach(function (id) {
if (!document.getElementById(id)) {
var el = document.createElement('input');
el.type = 'hidden';
el.id = id;
el.value = '1';
document.body.appendChild(el);
}
});
}, 600);
$(document).on('click', '#custom-order-btn', function(e){
e.preventDefault();
$('#custom-order-modal').modal('show');
@@ -875,4 +989,149 @@ $(document).ready(function() {
$('body').on('click', '.page-cms-15 ._box-2 ._tiles ._tile', function() {
$(this).toggleClass('active').siblings().removeClass('active');
})
});
});
// ============================================================================
// Phase 02 Plan 02-02: add-to-cart w nowym layoucie (wlasny AJAX submit).
// ============================================================================
// Powod: w nowym layoucie (.product-variants-data--new) button
// [data-button-action=add-to-cart] oraz input #quantity_wanted znajduja sie POZA
// forma #add-to-cart-or-refresh (forma zamyka sie w sidebar .col-md-6, a button
// i qty sa w szerokim .product-bar). PS core delegowany handler uzywa
// $(btn).closest('form') -> length 0 -> nie znajduje formy; rownoczesnie PS
// core ma dodatkowe handler'y ktore moga POST'owac pusty payload, dublujac
// zgloszenie z naszym.
//
// Rozwiazanie: handler na CAPTURE phase przez natywny addEventListener
// (jQuery .on() nie wspiera capture). Capture odpala sie PRZED delegowanymi
// PS core handlerami na bubble phase -> stopImmediatePropagation blokuje je
// calkowicie. Nastepnie manualny AJAX POST z form.serialize() + qty + action.
//
// Dodatkowe problemy adresowane tu zamiast w innych planach:
// 1) Fancybox-blocker (custom.js:327) NIE odpala sie w nowym layoucie bo
// selector $('#add-to-cart-or-refresh button') matches 0 elementow
// (button poza forma). Port logiki tutaj.
// 2) Sync is_crop/crop_width/crop_height: checkedHandler (custom.js:183)
// aborts na crash'u totalpriceinfospecific przed jQuery('#product_is_crop').val(1).
// Tu wymuszamy synchronizacje jesli checkbox checked ale is_crop=0.
// 3) Blockcart widget (header cart counter) nie auto-refreshuje sie po
// updatedCart event w nowym layoucie. Fetchujemy blockcart module ajax
// i manualnie podmieniamy zawartosc .blockcart.
//
// Idempotency: `window.__p02p02Bound` flag chroni przed double-register gdy
// ten sam kod jest tez inline'owany w product.tpl (cache-buster dla browser
// ktorego <script src=custom.js> moze byc stale cached).
// ============================================================================
if (!window.__p02p02Bound) {
window.__p02p02Bound = true;
document.addEventListener('click', function(e) {
var btn = e.target.closest ? e.target.closest('[data-button-action=add-to-cart]') : null;
if (!btn) return;
if (!document.querySelector('.product-variants-data--new')) return; // stary layout: PS core handle
var $form = $('#add-to-cart-or-refresh');
if (!$form.length) return;
// CAPTURE phase: blokuje PS core delegated handlers (bubble) zanim odpala.
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
// Walidacja: piece musi byc skonfigurowany.
if (!$('#checkbox-piece').is(':checked')) {
$.fancybox({
minWidth: 800,
maxWidth: 1000,
padding: 30,
height: 100,
content: 'Proszę wybrać rozmiar i wycinek tapety przed dodaniem jej do koszyka.'
});
return;
}
// Wymus sync hidden inputs z piece state (obejscie crash'u checkedHandler).
// Idempotentne — bezpieczne gdy override z 02-01 zadziala w przyszlosci.
if ($('#product_is_crop').val() === '0' || !$('#product_crop_width').val() || $('#product_crop_width').val() === '0') {
$('#product_is_crop').val('1');
var pw = parseInt($('#piece-width').val(), 10) || 100;
var ph = parseInt($('#piece-height').val(), 10) || 100;
$('#product_crop_width').val(pw);
$('#product_crop_height').val(ph);
}
var $btn = $(btn);
$btn.prop('disabled', true).addClass('loading');
var qty = parseInt($('#quantity_wanted').val(), 10) || 1;
var payload = $form.serialize() + '&qty=' + encodeURIComponent(qty) + '&add=1&action=update';
var actionUrl = $form.attr('action') || window.location.href;
$.ajax({
url: actionUrl,
type: 'POST',
data: payload,
dataType: 'json',
headers: { 'Accept': 'application/json' },
success: function(resp) {
var hasError = !resp || resp.hasError === true || resp.success === false ||
(resp.errors && (Array.isArray(resp.errors) ? resp.errors.length : Object.keys(resp.errors).length));
if (hasError) {
var errs = resp && resp.errors;
var msg = '';
if (Array.isArray(errs)) msg = errs.join('<br>');
else if (errs && typeof errs === 'object') msg = Object.values(errs).join('<br>');
else msg = 'Nie udało się dodać produktu do koszyka. Spróbuj ponownie.';
$.fancybox({
minWidth: 400,
maxWidth: 800,
padding: 30,
height: 100,
content: msg
});
return;
}
// Success: emit updatedCart + manual blockcart refresh.
if (window.prestashop && typeof prestashop.emit === 'function') {
prestashop.emit('updatedCart', { resp: resp, reason: { linkAction: 'add-to-cart' } });
}
$(document).trigger('updatedCart', [resp]);
// Manual blockcart refresh (nowy layout nie ma auto-listener na emit).
// Endpoint ps_shoppingcart module renderuje caly blockcart HTML.
if (window.prestashop && prestashop.urls && prestashop.urls.pages && prestashop.urls.pages.cart) {
$.get(prestashop.urls.pages.cart, { action: 'refresh', ajax: 1 }, null, 'json')
.done(function(cartResp) {
if (cartResp && cartResp.preview) {
$('.blockcart').replaceWith(cartResp.preview);
} else if (resp.cart) {
// Fallback: podmien counter minimalnie
$('.cart-products-count').text(resp.cart.products_count || resp.cart.totals && resp.cart.totals.total || '');
}
})
.fail(function() {
// Last-resort fallback: przeladuj stronice aby PS core odbudowal koszyk.
// Tylko jako fallback — normalnie nie powinno sie odpalic.
});
}
// Success feedback: subtelne pulsowanie przycisku.
$btn.addClass('added-flash');
setTimeout(function () { $btn.removeClass('added-flash'); }, 1200);
},
error: function(xhr) {
$.fancybox({
minWidth: 400,
maxWidth: 800,
padding: 30,
height: 100,
content: 'Błąd połączenia z serwerem. Spróbuj ponownie za chwilę.'
});
},
complete: function() {
$btn.prop('disabled', false).removeClass('loading');
}
});
}, true); // useCapture=true — kluczowe, odpala przed bubble-phase PS core handlers.
}