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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
}
|
||||
Reference in New Issue
Block a user