From 1f12fae2811dc4a9446085f2ff822cfccdd8c4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Sun, 1 Oct 2023 23:31:19 +0200 Subject: [PATCH 1/9] quick view refacto --- _dev/js/theme/components/quickview.js | 53 +++++++------- .../product/handler/quickViewClickHandler.js | 22 ++++++ .../core/product/handler/quickViewHandler.js | 33 +++++++++ _dev/js/theme/core/product/index.js | 11 +-- .../theme/core/product/productController.js | 20 ++++++ .../js/theme/core/product/productQuickView.js | 20 ------ .../core/product/request/quickViewRequest.js | 72 +++++++++++++++++++ 7 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 _dev/js/theme/core/product/handler/quickViewClickHandler.js create mode 100644 _dev/js/theme/core/product/handler/quickViewHandler.js create mode 100644 _dev/js/theme/core/product/productController.js delete mode 100644 _dev/js/theme/core/product/productQuickView.js create mode 100644 _dev/js/theme/core/product/request/quickViewRequest.js diff --git a/_dev/js/theme/components/quickview.js b/_dev/js/theme/components/quickview.js index 41c037cd..e6c49f14 100644 --- a/_dev/js/theme/components/quickview.js +++ b/_dev/js/theme/components/quickview.js @@ -1,32 +1,31 @@ import $ from 'jquery'; import prestashop from 'prestashop'; +import DOMReady from '../utils/DOMReady'; +import parseToHtml from '../utils/parseToHtml'; -$(() => { - prestashop.on('clickQuickView', (elm) => { - const data = { - action: 'quickview', - id_product: elm.dataset.idProduct, - id_product_attribute: elm.dataset.idProductAttribute, - }; - $.post(prestashop.urls.pages.product, data, null, 'json') - .then((resp) => { - const $body = $('body'); - $('body').append(resp.quickview_html); - const productModal = $( - `#quickview-modal-${resp.product.id}-${resp.product.id_product_attribute}`, - ); - productModal.modal('show'); - $body.addClass('js-quickview-open'); - productModal.on('hidden.bs.modal', () => { - productModal.remove(); - $body.removeClass('js-quickview-open'); - }); - }) - .fail((resp) => { - prestashop.emit('handleError', { - eventType: 'clickQuickView', - resp, - }); - }); +/** + * Handle open quick view + */ +const handleQuickViewOpen = ({ + resp, +}) => { + const body = document.querySelector('body'); + body.append(parseToHtml(resp.quickview_html)); + + // TO DO REMOVE JQUERY + const productModal = $( + `#quickview-modal-${resp.product.id}-${resp.product.id_product_attribute}`, + ); + productModal.modal('show'); + + body.classList.add('js-quickview-open'); + + productModal.on('hidden.bs.modal', () => { + productModal.remove(); + body.classList.remove('js-quickview-open') }); +}; + +DOMReady(() => { + prestashop.on('clickViewOpen', handleQuickViewOpen); }); diff --git a/_dev/js/theme/core/product/handler/quickViewClickHandler.js b/_dev/js/theme/core/product/handler/quickViewClickHandler.js new file mode 100644 index 00000000..1ed2dc34 --- /dev/null +++ b/_dev/js/theme/core/product/handler/quickViewClickHandler.js @@ -0,0 +1,22 @@ +import prestashop from 'prestashop'; + +/** + * Quick view btn click handler + * SideEffect: emit event on prestashop object clickViewOpen + * @param event {Event} - click event + */ +const quickViewClickHandler = async (event) => { + event.preventDefault(); + const miniature = event.target.closest(prestashop.selectors.product.miniature); + const dataset = miniature?.dataset || {}; + const idProduct = dataset?.idProduct || 0; + const idProductAttribute = dataset?.idProductAttribute || 0; + + prestashop.emit('clickQuickView', { + dataset, + idProduct, + idProductAttribute, + }); +}; + +export default quickViewClickHandler; diff --git a/_dev/js/theme/core/product/handler/quickViewHandler.js b/_dev/js/theme/core/product/handler/quickViewHandler.js new file mode 100644 index 00000000..0f42edc2 --- /dev/null +++ b/_dev/js/theme/core/product/handler/quickViewHandler.js @@ -0,0 +1,33 @@ +import prestashop from 'prestashop'; +import quickViewRequest from '../request/quickViewRequest'; + +/** + * Quick view handler + * SideEffect: emit event on prestashop object clickViewOpen + * @param idProduct + * @param idProductAttribute + */ +const quickViewHandler = async (idProduct, idProductAttribute) => { + const payload = { + id_product: Number.parseInt(idProduct, 10), + id_product_attribute: Number.parseInt(idProductAttribute, 0), + }; + + const { getRequest } = quickViewRequest(payload); + + try { + const resp = await getRequest(); + + prestashop.emit('clickViewOpen', { + reason: 'openQuickView', + resp, + }); + } catch (e) { + prestashop.emit('handleError', { + eventType: 'clickQuickView', + resp: {}, + }); + } +}; + +export default quickViewHandler; diff --git a/_dev/js/theme/core/product/index.js b/_dev/js/theme/core/product/index.js index 90db081e..e70bbd2f 100644 --- a/_dev/js/theme/core/product/index.js +++ b/_dev/js/theme/core/product/index.js @@ -1,7 +1,10 @@ import updateProduct from '@js/theme/core/product/updateProduct'; -import productQuickView from '@js/theme/core/product/productQuickView'; +import productController from "./productController"; +import DOMReady from "../../utils/DOMReady"; -$(() => { +const { init } = productController(); + +DOMReady(() => { updateProduct(); - productQuickView(); -}); + init(); +}) diff --git a/_dev/js/theme/core/product/productController.js b/_dev/js/theme/core/product/productController.js new file mode 100644 index 00000000..f7161839 --- /dev/null +++ b/_dev/js/theme/core/product/productController.js @@ -0,0 +1,20 @@ +import prestashop from 'prestashop'; +import useEvent from '../../components/event/useEvent'; +import quickViewClickHandler from './handler/quickViewClickHandler'; +import quickViewHandler from './handler/quickViewHandler'; + +const { on } = useEvent(); + +const productController = () => { + const init = () => { + on(document, 'click', prestashop.selectors.listing.quickview, quickViewClickHandler); + + prestashop.on('clickQuickView', ({ idProduct, idProductAttribute }) => quickViewHandler(idProduct, idProductAttribute)); + }; + + return { + init, + }; +}; + +export default productController; diff --git a/_dev/js/theme/core/product/productQuickView.js b/_dev/js/theme/core/product/productQuickView.js deleted file mode 100644 index e309dbae..00000000 --- a/_dev/js/theme/core/product/productQuickView.js +++ /dev/null @@ -1,20 +0,0 @@ -import prestashop from 'prestashop'; -import useEvent from '@js/theme/components/event/useEvent'; - -const { on } = useEvent(); - -const handleQuickView = (event) => { - event.preventDefault(); - const miniature = event.target.closest(prestashop.selectors.product.miniature); - const dataset = miniature?.dataset || {}; - - prestashop.emit('clickQuickView', { - dataset, - }); -}; - -const productQuickView = () => { - on(document, 'click', prestashop.selectors.listing.quickview, handleQuickView); -}; - -export default productQuickView; diff --git a/_dev/js/theme/core/product/request/quickViewRequest.js b/_dev/js/theme/core/product/request/quickViewRequest.js new file mode 100644 index 00000000..b048e99e --- /dev/null +++ b/_dev/js/theme/core/product/request/quickViewRequest.js @@ -0,0 +1,72 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string} quickview_html - html content of quick view + * @property {object} product - product front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.id_product {number} - id_product to show - Required + * @param payload.id_product_attribute {number} - id_product to show - optional, default 0 + * @param payload.ajax {number} - optional, default 1 + * @param payload.action {string} - optional + * @example + * const payload = { + * id_product: 1, // Required + * id_product_attribute: 1, // Optional - default 0 + * }; + * + * const { getRequest } = quickViewRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const quickViewRequest = (payload) => { + const payloadToSend = { + action: 'quickview', + ajax: 1, + id_product_attribute: 0, + ...payload, + }; + + const payloadDefinition = { + action: { + type: 'string', + required: true, + }, + id_product: { + type: 'int', + required: true, + }, + id_product_attribute: { + type: 'int', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.product, payloadToSend); + + return { + getRequest, + }; +}; + +export default quickViewRequest; From 60247db6b2e488818c9c4adcd1935b656dbb5922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Sun, 1 Oct 2023 23:34:12 +0200 Subject: [PATCH 2/9] lint fix --- _dev/js/theme/components/quickview.js | 2 +- _dev/js/theme/core/product/handler/quickViewHandler.js | 2 +- _dev/js/theme/core/product/handler/updateProductHandler.js | 5 +++++ _dev/js/theme/core/product/handler/updatedProductHandler.js | 5 +++++ _dev/js/theme/core/product/index.js | 6 +++--- _dev/js/theme/core/product/request/updateProductRequest.js | 5 +++++ 6 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 _dev/js/theme/core/product/handler/updateProductHandler.js create mode 100644 _dev/js/theme/core/product/handler/updatedProductHandler.js create mode 100644 _dev/js/theme/core/product/request/updateProductRequest.js diff --git a/_dev/js/theme/components/quickview.js b/_dev/js/theme/components/quickview.js index e6c49f14..f9d5baa6 100644 --- a/_dev/js/theme/components/quickview.js +++ b/_dev/js/theme/components/quickview.js @@ -22,7 +22,7 @@ const handleQuickViewOpen = ({ productModal.on('hidden.bs.modal', () => { productModal.remove(); - body.classList.remove('js-quickview-open') + body.classList.remove('js-quickview-open'); }); }; diff --git a/_dev/js/theme/core/product/handler/quickViewHandler.js b/_dev/js/theme/core/product/handler/quickViewHandler.js index 0f42edc2..24eee7c4 100644 --- a/_dev/js/theme/core/product/handler/quickViewHandler.js +++ b/_dev/js/theme/core/product/handler/quickViewHandler.js @@ -10,7 +10,7 @@ import quickViewRequest from '../request/quickViewRequest'; const quickViewHandler = async (idProduct, idProductAttribute) => { const payload = { id_product: Number.parseInt(idProduct, 10), - id_product_attribute: Number.parseInt(idProductAttribute, 0), + id_product_attribute: Number.parseInt(idProductAttribute, 10), }; const { getRequest } = quickViewRequest(payload); diff --git a/_dev/js/theme/core/product/handler/updateProductHandler.js b/_dev/js/theme/core/product/handler/updateProductHandler.js new file mode 100644 index 00000000..7f72809c --- /dev/null +++ b/_dev/js/theme/core/product/handler/updateProductHandler.js @@ -0,0 +1,5 @@ +const updateProductHandler = () => { + +}; + +export default updateProductHandler; diff --git a/_dev/js/theme/core/product/handler/updatedProductHandler.js b/_dev/js/theme/core/product/handler/updatedProductHandler.js new file mode 100644 index 00000000..c293cb96 --- /dev/null +++ b/_dev/js/theme/core/product/handler/updatedProductHandler.js @@ -0,0 +1,5 @@ +const updatedProductHandler = () => { + +}; + +export default updatedProductHandler; diff --git a/_dev/js/theme/core/product/index.js b/_dev/js/theme/core/product/index.js index e70bbd2f..d03ff335 100644 --- a/_dev/js/theme/core/product/index.js +++ b/_dev/js/theme/core/product/index.js @@ -1,10 +1,10 @@ import updateProduct from '@js/theme/core/product/updateProduct'; -import productController from "./productController"; -import DOMReady from "../../utils/DOMReady"; +import productController from './productController'; +import DOMReady from '../../utils/DOMReady'; const { init } = productController(); DOMReady(() => { updateProduct(); init(); -}) +}); diff --git a/_dev/js/theme/core/product/request/updateProductRequest.js b/_dev/js/theme/core/product/request/updateProductRequest.js new file mode 100644 index 00000000..1d6a021a --- /dev/null +++ b/_dev/js/theme/core/product/request/updateProductRequest.js @@ -0,0 +1,5 @@ +const updateProductRequest = () => { + +}; + +export default updateProductRequest; From bf8d4021b20e9dd0bb89f3aecce4ee1be890f546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 01:26:21 +0200 Subject: [PATCH 3/9] product page events refacto part 1 --- .../components/http/useDefaultHttpRequest.js | 5 +- .../http/useHttpPayloadDefinition.js | 2 +- .../handler/product/productPopStateHandler.js | 36 ++++ .../product/productUpdateErrorHandler.js | 11 ++ .../{ => product}/updateProductHandler.js | 0 .../handler/product/updatedProductHandler.js | 43 +++++ .../{ => quickView}/quickViewClickHandler.js | 0 .../{ => quickView}/quickViewHandler.js | 2 +- .../product/handler/updatedProductHandler.js | 5 - .../persister/productFormDataPersister.js | 38 ++++ .../theme/core/product/productController.js | 21 ++- .../request/product/updateProductRequest.js | 121 +++++++++++++ .../{ => quickView}/quickViewRequest.js | 8 +- .../product/request/updateProductRequest.js | 5 - .../core/product/store/productStateStore.js | 70 ++++++++ _dev/js/theme/core/product/updateProduct.js | 167 ++++-------------- .../core/product/utils/isProductPreview.js | 11 ++ .../core/product/utils/isQuickViewOpen.js | 7 + _dev/js/theme/frontAPI/apiAction.js | 13 -- .../frontAPI/product/updateProductAction.js | 37 ---- _dev/js/theme/index.js | 7 +- 21 files changed, 404 insertions(+), 205 deletions(-) create mode 100644 _dev/js/theme/core/product/handler/product/productPopStateHandler.js create mode 100644 _dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js rename _dev/js/theme/core/product/handler/{ => product}/updateProductHandler.js (100%) create mode 100644 _dev/js/theme/core/product/handler/product/updatedProductHandler.js rename _dev/js/theme/core/product/handler/{ => quickView}/quickViewClickHandler.js (100%) rename _dev/js/theme/core/product/handler/{ => quickView}/quickViewHandler.js (90%) delete mode 100644 _dev/js/theme/core/product/handler/updatedProductHandler.js create mode 100644 _dev/js/theme/core/product/persister/productFormDataPersister.js create mode 100644 _dev/js/theme/core/product/request/product/updateProductRequest.js rename _dev/js/theme/core/product/request/{ => quickView}/quickViewRequest.js (87%) delete mode 100644 _dev/js/theme/core/product/request/updateProductRequest.js create mode 100644 _dev/js/theme/core/product/store/productStateStore.js create mode 100644 _dev/js/theme/core/product/utils/isProductPreview.js create mode 100644 _dev/js/theme/core/product/utils/isQuickViewOpen.js delete mode 100644 _dev/js/theme/frontAPI/apiAction.js delete mode 100644 _dev/js/theme/frontAPI/product/updateProductAction.js diff --git a/_dev/js/theme/components/http/useDefaultHttpRequest.js b/_dev/js/theme/components/http/useDefaultHttpRequest.js index 3bf3f859..5f5c6184 100644 --- a/_dev/js/theme/components/http/useDefaultHttpRequest.js +++ b/_dev/js/theme/components/http/useDefaultHttpRequest.js @@ -5,10 +5,11 @@ import useHttpRequest from './useHttpRequest'; * Default http request accepting payload as object and returning promise with response * @param {string} url - url to send request * @param {object} payload - payload to send + * @param {object} options - request options for example different headers * @returns {Promise} */ -const useDefaultHttpRequest = (url, payload) => { - const { request } = useHttpRequest(url); +const useDefaultHttpRequest = (url, payload, options = {}) => { + const { request } = useHttpRequest(url, options); return new Promise((resolve, reject) => { request diff --git a/_dev/js/theme/components/http/useHttpPayloadDefinition.js b/_dev/js/theme/components/http/useHttpPayloadDefinition.js index e2508ba2..fbecbae4 100644 --- a/_dev/js/theme/components/http/useHttpPayloadDefinition.js +++ b/_dev/js/theme/components/http/useHttpPayloadDefinition.js @@ -83,7 +83,7 @@ const useHttpPayloadDefinition = (payload, definition) => { regex, } = fieldDefinition; - if (required && !value) { + if (required && typeof value === 'undefined') { validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.REQUIRED}`); } diff --git a/_dev/js/theme/core/product/handler/product/productPopStateHandler.js b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js new file mode 100644 index 00000000..21b615e5 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js @@ -0,0 +1,36 @@ +import prestashop from 'prestashop'; +import productFormDataPersister from '../../persister/productFormDataPersister'; +import productStateStore from '../../store/productStateStore'; + +const { setOnPopState, isFormChanged } = productStateStore(); + +const { get } = productFormDataPersister(); + +const productPopStateHandler = (event) => { + setOnPopState(true); + + const formData = event?.state?.form || get(); + + if ((!formData || formData?.length === 0) && !isFormChanged()) { + return; + } + + const form = document.querySelector(`${prestashop.selectors.product.actions} form`); + + const handleFormElementState = (data) => { + const element = form.querySelector(`[name="${data.name}"]`); + + if (element) { + element.value = data.value; + } + }; + + formData.forEach(handleFormElementState); + + prestashop.emit('updateProduct', { + eventType: 'updatedProductCombination', + event, + }); +}; + +export default productPopStateHandler; diff --git a/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js b/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js new file mode 100644 index 00000000..b452c4f9 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js @@ -0,0 +1,11 @@ +import useAlertToast from '../../../../components/useAlertToast'; + +const { danger } = useAlertToast(); + +const productUpdateErrorHandler = (event) => { + if (event?.errorMessage) { + danger(event.errorMessage); + } +}; + +export default productUpdateErrorHandler; diff --git a/_dev/js/theme/core/product/handler/updateProductHandler.js b/_dev/js/theme/core/product/handler/product/updateProductHandler.js similarity index 100% rename from _dev/js/theme/core/product/handler/updateProductHandler.js rename to _dev/js/theme/core/product/handler/product/updateProductHandler.js diff --git a/_dev/js/theme/core/product/handler/product/updatedProductHandler.js b/_dev/js/theme/core/product/handler/product/updatedProductHandler.js new file mode 100644 index 00000000..0ba9bef7 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/updatedProductHandler.js @@ -0,0 +1,43 @@ +import isQuickViewOpen from '../../utils/isQuickViewOpen'; +import productStateStore from '../../store/productStateStore'; + +const { isOnPopState, setOnPopState } = productStateStore(); + +const updatedProductHandler = ({ + product_url: responseProductUrl = null, + id_product_attribute: responseIdProductAttribute = null, + product_title: responseProductTitle = '', +}, formData) => { + if (!responseProductUrl || !responseIdProductAttribute) { + return; + } + + /* + * If quickview modal is present we are not on product page, so + * we don't change the url nor title + */ + if (isQuickViewOpen()) { + return; + } + + const pageTitle = document.title; + + if (responseProductTitle) { + document.title = responseProductTitle; + } + + if (!isOnPopState()) { + window.history.pushState( + { + id_product_attribute: responseIdProductAttribute, + form: formData, + }, + pageTitle, + responseProductUrl, + ); + } + + setOnPopState(false); +}; + +export default updatedProductHandler; diff --git a/_dev/js/theme/core/product/handler/quickViewClickHandler.js b/_dev/js/theme/core/product/handler/quickView/quickViewClickHandler.js similarity index 100% rename from _dev/js/theme/core/product/handler/quickViewClickHandler.js rename to _dev/js/theme/core/product/handler/quickView/quickViewClickHandler.js diff --git a/_dev/js/theme/core/product/handler/quickViewHandler.js b/_dev/js/theme/core/product/handler/quickView/quickViewHandler.js similarity index 90% rename from _dev/js/theme/core/product/handler/quickViewHandler.js rename to _dev/js/theme/core/product/handler/quickView/quickViewHandler.js index 24eee7c4..64215e97 100644 --- a/_dev/js/theme/core/product/handler/quickViewHandler.js +++ b/_dev/js/theme/core/product/handler/quickView/quickViewHandler.js @@ -1,5 +1,5 @@ import prestashop from 'prestashop'; -import quickViewRequest from '../request/quickViewRequest'; +import quickViewRequest from '../../request/quickView/quickViewRequest'; /** * Quick view handler diff --git a/_dev/js/theme/core/product/handler/updatedProductHandler.js b/_dev/js/theme/core/product/handler/updatedProductHandler.js deleted file mode 100644 index c293cb96..00000000 --- a/_dev/js/theme/core/product/handler/updatedProductHandler.js +++ /dev/null @@ -1,5 +0,0 @@ -const updatedProductHandler = () => { - -}; - -export default updatedProductHandler; diff --git a/_dev/js/theme/core/product/persister/productFormDataPersister.js b/_dev/js/theme/core/product/persister/productFormDataPersister.js new file mode 100644 index 00000000..ad5b2c50 --- /dev/null +++ b/_dev/js/theme/core/product/persister/productFormDataPersister.js @@ -0,0 +1,38 @@ +import { formSerializeArray } from '../../../utils/formSerialize'; + +let formData = []; + +/** + * Persists product form data + * @module + */ +const productFormDataPersister = () => { + /** + * Persists form data from the form element + * @method + * @param {HTMLFormElement} formElement - form element to persist + * @throws {Error} - if formElement is not a form element + * @return {void} + */ + const persist = (formElement) => { + if (formElement?.tagName !== 'FORM') { + throw new Error('formElement is not a form element'); + } + + formData = formSerializeArray(formElement); + }; + + /** + * Returns persisted data + * @method + * @return {*[]} + */ + const get = () => formData; + + return { + persist, + get, + }; +}; + +export default productFormDataPersister; diff --git a/_dev/js/theme/core/product/productController.js b/_dev/js/theme/core/product/productController.js index f7161839..d5ba0547 100644 --- a/_dev/js/theme/core/product/productController.js +++ b/_dev/js/theme/core/product/productController.js @@ -1,14 +1,31 @@ import prestashop from 'prestashop'; import useEvent from '../../components/event/useEvent'; -import quickViewClickHandler from './handler/quickViewClickHandler'; -import quickViewHandler from './handler/quickViewHandler'; +import quickViewClickHandler from './handler/quickView/quickViewClickHandler'; +import quickViewHandler from './handler/quickView/quickViewHandler'; +import productUpdateErrorHandler from './handler/product/productUpdateErrorHandler'; +import productFormDataPersister from './persister/productFormDataPersister'; +import productPopStateHandler from './handler/product/productPopStateHandler'; +import updatedProductHandler from './handler/product/updatedProductHandler'; const { on } = useEvent(); +const { persist } = productFormDataPersister(); + +const persistFormDataOnInit = () => { + const form = document.querySelector(`${prestashop.selectors.product.actions} form`); + + if (form) { + persist(form); + } +}; const productController = () => { const init = () => { + persistFormDataOnInit(); on(document, 'click', prestashop.selectors.listing.quickview, quickViewClickHandler); + window.addEventListener('popstate', productPopStateHandler); + prestashop.on('updatedProduct', updatedProductHandler); + prestashop.on('showErrorNextToAddtoCartButton', productUpdateErrorHandler); prestashop.on('clickQuickView', ({ idProduct, idProductAttribute }) => quickViewHandler(idProduct, idProductAttribute)); }; diff --git a/_dev/js/theme/core/product/request/product/updateProductRequest.js b/_dev/js/theme/core/product/request/product/updateProductRequest.js new file mode 100644 index 00000000..c8c4dc9b --- /dev/null +++ b/_dev/js/theme/core/product/request/product/updateProductRequest.js @@ -0,0 +1,121 @@ +import prestashop from 'prestashop'; +import useHttpRequest from '../../../../components/http/useHttpRequest'; +import useHttpController from '../../../../components/http/useHttpController'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +const { dispatch, abortAll } = useHttpController(); + +/** + * @typedef ServerResponse + * @type {object} + * @property {string} address_form - new address form html content + */ + +/** + * Update listing facets request + * @param payload {object} - payload for request + * @param payload.preview {number} - is preview 1 or 0 + * @param payload.quickview {number} - is quick view 1 or 0 + * @param payload.quantity_wanted {number} - quantity wanted + * @param payload.id_product {number} - product id + * @param payload.id_product_attribute {number} - product attribute id + * @param payload.id_customization {number} - customization id - optional, default 0 + * @param payload.ajax {number} - optional, default 1 + * @param payload.action {string} - optional, default refresh + * @param payload.group[] {array} - array of attributes groups - optional + * @example + * const url = 'address-form.com/url'; // url to update address form + * const payload = { + * id_product: 1, + * id_product_attribute: 1, + * quantity_wanted: 1, + * preview: 0, + * quickview: 0, + * } + * const { getRequest } = updateProductRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const updateProductRequest = (payload) => { + const { request, controller } = useHttpRequest(prestashop.urls.pages.product); + const payloadToSend = { + ajax: 1, + action: 'refresh', + ...payload, + }; + + const payloadDefinition = { + action: { + type: 'string', + required: true, + }, + ajax: { + type: 'int', + required: true, + }, + preview: { + type: 'int', + required: true, + }, + quickview: { + type: 'int', + required: true, + }, + quantity_wanted: { + type: 'int', + required: true, + }, + id_product: { + type: 'int', + required: true, + }, + id_product_attribute: { + type: 'int', + required: true, + }, + id_customization: { + type: 'int', + required: false, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => { + abortAll(); + + return new Promise((resolve, reject) => { + dispatch(request, controller)(() => request + .query(payloadToSend) + .post() + .json((resp) => { + resolve(resp); + }) + .catch((e) => { + // IF ABORTED + if (e instanceof DOMException) { + return; + } + + reject(); + })); + }); + }; + + return { + getRequest, + }; +}; + +export default updateProductRequest; diff --git a/_dev/js/theme/core/product/request/quickViewRequest.js b/_dev/js/theme/core/product/request/quickView/quickViewRequest.js similarity index 87% rename from _dev/js/theme/core/product/request/quickViewRequest.js rename to _dev/js/theme/core/product/request/quickView/quickViewRequest.js index b048e99e..119d7d85 100644 --- a/_dev/js/theme/core/product/request/quickViewRequest.js +++ b/_dev/js/theme/core/product/request/quickView/quickViewRequest.js @@ -1,6 +1,6 @@ import prestashop from 'prestashop'; -import useDefaultHttpRequest from '../../../components/http/useDefaultHttpRequest'; -import useHttpPayloadDefinition from '../../../components/http/useHttpPayloadDefinition'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; /** * @typedef ServerResponse @@ -44,6 +44,10 @@ const quickViewRequest = (payload) => { type: 'string', required: true, }, + ajax: { + type: 'int', + required: true, + }, id_product: { type: 'int', required: true, diff --git a/_dev/js/theme/core/product/request/updateProductRequest.js b/_dev/js/theme/core/product/request/updateProductRequest.js deleted file mode 100644 index 1d6a021a..00000000 --- a/_dev/js/theme/core/product/request/updateProductRequest.js +++ /dev/null @@ -1,5 +0,0 @@ -const updateProductRequest = () => { - -}; - -export default updateProductRequest; diff --git a/_dev/js/theme/core/product/store/productStateStore.js b/_dev/js/theme/core/product/store/productStateStore.js new file mode 100644 index 00000000..a6bb554c --- /dev/null +++ b/_dev/js/theme/core/product/store/productStateStore.js @@ -0,0 +1,70 @@ +const state = { + isOnPopStateEvent: false, + formChanged: false, + currentRequestDelayedId: null, +}; + +/** + * Store for product state + * @module + */ +const productStateStore = () => { + /** + * Returns true if the current event is a popstate event + * @method + * @return {boolean} + */ + const isOnPopState = () => state.isOnPopStateEvent; + + /** + * Sets the current event as a popstate event + * @method + * @param value {boolean} + */ + const setOnPopState = (value) => { + state.isOnPopStateEvent = value; + }; + + /** + * Sets the form changed state + * @method + * @param value {boolean} + */ + const setFormChanged = (value) => { + state.formChanged = value; + }; + + /** + * Returns true if the form has changed + * @method + * @return {boolean} + */ + const isFormChanged = () => state.formChanged; + + /** + * Returns the current request delayed id + * @method + * @return {null|number} + */ + const getCurrentRequestDelayedId = () => state.currentRequestDelayedId; + + /** + * Sets the current request delayed id + * @method + * @param value {null|number} + */ + const setCurrentRequestDelayedId = (value) => { + state.currentRequestDelayedId = value; + }; + + return { + isOnPopState, + setOnPopState, + setFormChanged, + isFormChanged, + getCurrentRequestDelayedId, + setCurrentRequestDelayedId, + }; +}; + +export default productStateStore; diff --git a/_dev/js/theme/core/product/updateProduct.js b/_dev/js/theme/core/product/updateProduct.js index 08aef27b..24f94a71 100644 --- a/_dev/js/theme/core/product/updateProduct.js +++ b/_dev/js/theme/core/product/updateProduct.js @@ -3,25 +3,18 @@ import { fromSerializeObject } from '@js/theme/utils/formSerialize'; import parseToHtml from '@js/theme/utils/parseToHtml'; import useAlertToast from '@js/theme/components/useAlertToast'; import useEvent from '@js/theme/components/event/useEvent'; +import isQuickViewOpen from './utils/isQuickViewOpen'; +import isProductPreview from './utils/isProductPreview'; +import updateProductRequest from './request/product/updateProductRequest'; +import productFormDataPersister from './persister/productFormDataPersister'; +import productStateStore from './store/productStateStore'; + +const { setFormChanged, getCurrentRequestDelayedId, setCurrentRequestDelayedId } = productStateStore(); const { on } = useEvent(); const { danger } = useAlertToast(); -// Used to clearTimeout if user flood the product quantity input -let currentRequestDelayedId = null; - -// Check for popState event -let isOnPopStateEvent = false; - -// Register form of first update -const firstFormData = []; - -// Detect if the form has changed one time -let formChanged = false; - -const isQuickViewOpen = () => !!document.querySelector('.modal.quickview.in'); - const replaceDOMElements = ({ /* eslint-disable */ product_cover_thumbnails, @@ -59,12 +52,6 @@ const replaceDOMElements = ({ ?.replaceWith(parseToHtml(product_flags)); }; -const isPreview = () => { - const urlParams = new URLSearchParams(window.location.search); - - return urlParams.has('preview'); -}; - /** * Find DOM elements and replace their content * @@ -155,14 +142,7 @@ const updateProductData = async (event, eventType) => { const form = productActions.querySelector('form'); const formSerialized = fromSerializeObject(form); - let updateRatingEvent; - - if (typeof Event === 'function') { - updateRatingEvent = new Event('updateRating'); - } else { - updateRatingEvent = document.createEvent('Event'); - updateRatingEvent.initEvent('updateRating', true, true); - } + const updateRatingEvent = new Event('updateRating'); // New request only if new value if ( @@ -175,8 +155,8 @@ const updateProductData = async (event, eventType) => { quantityWantedInput.dataset.oldValue = quantityWantedInput.value ? quantityWantedInput.value : 1; - if (currentRequestDelayedId) { - clearTimeout(currentRequestDelayedId); + if (getCurrentRequestDelayedId()) { + clearTimeout(getCurrentRequestDelayedId()); } // Most update need to occur (almost) instantly, but in some cases (like keyboard actions) @@ -187,9 +167,23 @@ const updateProductData = async (event, eventType) => { updateDelay = 750; } - currentRequestDelayedId = setTimeout(async () => { + const timeoutId = setTimeout(async () => { + const idProductAttribute = formSerialized?.id_product_attribute || 0; + const idCustomization = formSerialized?.id_customization || 0; + + const payload = { + quantity_wanted: Number.parseInt(quantityWantedInput.value, 10), + preview: isProductPreview() ? 1 : 0, + quickview: isQuickViewOpen() ? 1 : 0, + ...formSerialized, + id_product: Number.parseInt(formSerialized.id_product, 10), + id_product_attribute: Number.parseInt(idProductAttribute, 10), + id_customization: Number.parseInt(idCustomization, 10), + }; + const { getRequest } = updateProductRequest(payload); + try { - const data = await prestashop.frontAPI.updateProduct(formSerialized, quantityWantedInput.value, isQuickViewOpen(), isPreview()); + const data = await getRequest(); // Used to avoid image blinking if same image = epileptic friendly @@ -225,17 +219,22 @@ const updateProductData = async (event, eventType) => { quantityWantedInput.value = minimalProductQuantity; } - prestashop.emit('updatedProduct', data, formSerialized); + const { persist, get: getPersistedData } = productFormDataPersister(); + persist(form); + + prestashop.emit('updatedProduct', data, getPersistedData()); } catch (e) { danger(prestashop.t.alert.genericHttpError); } - currentRequestDelayedId = null; + setCurrentRequestDelayedId(null); }, updateDelay); + + setCurrentRequestDelayedId(timeoutId); }; const handleProductFormChange = (event) => { - formChanged = true; + setFormChanged(true); prestashop.emit('updateProduct', { eventType: 'updatedProductCombination', @@ -261,118 +260,20 @@ const handleUpdateCart = (event) => { } }; -const handleError = (event) => { - if (event?.errorMessage) { - danger(event.errorMessage); - } -}; - const handleUpdateProduct = ({ event, eventType }) => { updateProductData(event, eventType); }; -const handleUpdatedProduct = (args, formData) => { - if (!args.product_url || !args.id_product_attribute) { - return; - } - - /* - * If quickview modal is present we are not on product page, so - * we don't change the url nor title - */ - if (isQuickViewOpen()) { - return; - } - - let pageTitle = document.title; - - if (args.product_title) { - pageTitle = args.product_title; - $(document).attr('title', pageTitle); - } - - if (!isOnPopStateEvent) { - window.history.pushState( - { - id_product_attribute: args.id_product_attribute, - form: formData, - }, - pageTitle, - args.product_url, - ); - } - - isOnPopStateEvent = false; -}; - -const handlePopState = (event) => { - isOnPopStateEvent = true; - - if ( - (!event.state - || (event.state && event.state.form && event.state.form.length === 0)) - && !formChanged - ) { - return; - } - - const form = document.querySelector(`${prestashop.selectors.product.actions} form`); - const handleState = (data) => { - const element = form.querySelector(`[name="${data.name}"]`); - - if (element) { - element.value = data.value; - } - }; - - if (event.state && event.state.form) { - event.state.form.forEach(handleState); - } else { - firstFormData.forEach(handleState); - } - - prestashop.emit('updateProduct', { - eventType: 'updatedProductCombination', - event, - // Following variables are not used anymore, but kept for backward compatibility - resp: {}, - reason: { - productUrl: prestashop.urls.pages.product || '', - }, - }); -}; - const attachEventListeners = () => { on(document, 'change', `${prestashop.selectors.product.variants} *[name]`, handleProductFormChange); prestashop.on('updateCart', handleUpdateCart); - prestashop.on('showErrorNextToAddtoCartButton', handleError); // Refresh all the product content prestashop.on('updateProduct', handleUpdateProduct); - prestashop.on('updatedProduct', handleUpdatedProduct); - - window.addEventListener('popstate', handlePopState); -}; - -const initProductPage = () => { - const productActions = document.querySelector(prestashop.selectors.product.actions); - const formElement = productActions?.querySelector('form'); - const formSerialized = formElement ? fromSerializeObject(formElement) : null; - - if (!formSerialized) { - return; - } - - for (const prop in formSerialized) { - if (Object.hasOwn(formSerialized, prop)) { - firstFormData.push({ name: prop, value: formSerialized[prop] }); - } - } }; const updateProduct = () => { attachEventListeners(); - initProductPage(); }; export default updateProduct; diff --git a/_dev/js/theme/core/product/utils/isProductPreview.js b/_dev/js/theme/core/product/utils/isProductPreview.js new file mode 100644 index 00000000..8c431915 --- /dev/null +++ b/_dev/js/theme/core/product/utils/isProductPreview.js @@ -0,0 +1,11 @@ +/** + * Check if the product is in preview mode + * @returns {boolean} + */ +const isProductPreview = () => { + const urlParams = new URLSearchParams(window.location.search); + + return urlParams.has('preview'); +}; + +export default isProductPreview; diff --git a/_dev/js/theme/core/product/utils/isQuickViewOpen.js b/_dev/js/theme/core/product/utils/isQuickViewOpen.js new file mode 100644 index 00000000..5645a18f --- /dev/null +++ b/_dev/js/theme/core/product/utils/isQuickViewOpen.js @@ -0,0 +1,7 @@ +/** + * Check if quick view is open + * @returns {boolean} + */ +const isQuickViewOpen = () => !!document.querySelector('.modal.quickview.in'); + +export default isQuickViewOpen; diff --git a/_dev/js/theme/frontAPI/apiAction.js b/_dev/js/theme/frontAPI/apiAction.js deleted file mode 100644 index 21f10987..00000000 --- a/_dev/js/theme/frontAPI/apiAction.js +++ /dev/null @@ -1,13 +0,0 @@ -import updateProductAction from '@js/theme/frontAPI/product/updateProductAction'; - -prestashop.frontAPI = {}; - -prestashop.addAction = (actionName, actionFunction) => { - if (typeof prestashop.frontAPI[actionName] !== 'undefined') { - throw new Error(`Action ${actionName} already exists`); - } - - prestashop.frontAPI[actionName] = actionFunction; -}; - -prestashop.addAction('updateProduct', updateProductAction); diff --git a/_dev/js/theme/frontAPI/product/updateProductAction.js b/_dev/js/theme/frontAPI/product/updateProductAction.js deleted file mode 100644 index 8250fd19..00000000 --- a/_dev/js/theme/frontAPI/product/updateProductAction.js +++ /dev/null @@ -1,37 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; -import prestashop from 'prestashop'; -import useHttpController from '@js/theme/components/http/useHttpController'; - -const { dispatch, abortAll } = useHttpController(); - -const updateProductAction = (productFormData, quantityWanted, quickview = false, isPreview = false) => new Promise((resolve, reject) => { - abortAll(); - - const { request, controller } = useHttpRequest(prestashop.urls.pages.product); - - const payload = { - ajax: 1, - action: 'refresh', - quantity_wanted: quantityWanted, - preview: isPreview ? 1 : 0, - quickview: quickview ? 1 : 0, - ...productFormData, - }; - - dispatch(request, controller)(() => request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch((e) => { - // IF ABORTED - if (e instanceof DOMException) { - return; - } - - reject(); - })); -}); - -export default updateProductAction; diff --git a/_dev/js/theme/index.js b/_dev/js/theme/index.js index 7879d9c2..252d8cc9 100644 --- a/_dev/js/theme/index.js +++ b/_dev/js/theme/index.js @@ -1,9 +1,7 @@ import $ from 'jquery'; import EventEmitter from 'events'; -/* eslint-enable */ -import '@js/theme/frontAPI/apiAction'; import '@js/theme/core'; import '@js/theme/vendors/bootstrap/bootstrap-imports'; @@ -17,14 +15,15 @@ import '@js/theme/components/customer'; import '@js/theme/components/quickview'; import '@js/theme/components/product'; import '@js/theme/components/cart/block-cart'; +/* eslint-enable */ + +import prestashop from 'prestashop'; /* eslint-disable */ // "inherit" EventEmitter for (const i in EventEmitter.prototype) { prestashop[i] = EventEmitter.prototype[i]; } - -import prestashop from 'prestashop'; import usePasswordPolicy from '@js/theme/components/password/usePasswordPolicy'; import Form from '@js/theme/components/form'; import TopMenu from '@js/theme/components/TopMenu'; From c55cdfc3feaed06553020dc46da6edc96ade7b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 01:34:49 +0200 Subject: [PATCH 4/9] product update event trigger --- .../product/productFormChangeHandler.js | 15 +++++++++++++ .../theme/core/product/productController.js | 2 ++ _dev/js/theme/core/product/updateProduct.js | 21 +------------------ _dev/js/theme/core/selectors.js | 4 ++-- 4 files changed, 20 insertions(+), 22 deletions(-) create mode 100644 _dev/js/theme/core/product/handler/product/productFormChangeHandler.js diff --git a/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js b/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js new file mode 100644 index 00000000..aa61577c --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js @@ -0,0 +1,15 @@ +import prestashop from 'prestashop'; +import productStateStore from '../../store/productStateStore'; + +const { setFormChanged } = productStateStore(); + +const productFormChangeHandler = (event) => { + setFormChanged(true); + + prestashop.emit('updateProduct', { + eventType: 'updatedProductCombination', + event, + }); +}; + +export default productFormChangeHandler; diff --git a/_dev/js/theme/core/product/productController.js b/_dev/js/theme/core/product/productController.js index d5ba0547..40eed993 100644 --- a/_dev/js/theme/core/product/productController.js +++ b/_dev/js/theme/core/product/productController.js @@ -6,6 +6,7 @@ import productUpdateErrorHandler from './handler/product/productUpdateErrorHandl import productFormDataPersister from './persister/productFormDataPersister'; import productPopStateHandler from './handler/product/productPopStateHandler'; import updatedProductHandler from './handler/product/updatedProductHandler'; +import productFormChangeHandler from './handler/product/productFormChangeHandler'; const { on } = useEvent(); const { persist } = productFormDataPersister(); @@ -22,6 +23,7 @@ const productController = () => { const init = () => { persistFormDataOnInit(); on(document, 'click', prestashop.selectors.listing.quickview, quickViewClickHandler); + on(document, 'change', `${prestashop.selectors.product.variants} *[name]`, productFormChangeHandler); window.addEventListener('popstate', productPopStateHandler); prestashop.on('updatedProduct', updatedProductHandler); diff --git a/_dev/js/theme/core/product/updateProduct.js b/_dev/js/theme/core/product/updateProduct.js index 24f94a71..fff3c766 100644 --- a/_dev/js/theme/core/product/updateProduct.js +++ b/_dev/js/theme/core/product/updateProduct.js @@ -2,16 +2,13 @@ import prestashop from 'prestashop'; import { fromSerializeObject } from '@js/theme/utils/formSerialize'; import parseToHtml from '@js/theme/utils/parseToHtml'; import useAlertToast from '@js/theme/components/useAlertToast'; -import useEvent from '@js/theme/components/event/useEvent'; import isQuickViewOpen from './utils/isQuickViewOpen'; import isProductPreview from './utils/isProductPreview'; import updateProductRequest from './request/product/updateProductRequest'; import productFormDataPersister from './persister/productFormDataPersister'; import productStateStore from './store/productStateStore'; -const { setFormChanged, getCurrentRequestDelayedId, setCurrentRequestDelayedId } = productStateStore(); - -const { on } = useEvent(); +const { getCurrentRequestDelayedId, setCurrentRequestDelayedId } = productStateStore(); const { danger } = useAlertToast(); @@ -233,20 +230,6 @@ const updateProductData = async (event, eventType) => { setCurrentRequestDelayedId(timeoutId); }; -const handleProductFormChange = (event) => { - setFormChanged(true); - - prestashop.emit('updateProduct', { - eventType: 'updatedProductCombination', - event, - // Following variables are not used anymore, but kept for backward compatibility - resp: {}, - reason: { - productUrl: prestashop.urls.pages.product || '', - }, - }); -}; - const handleUpdateCart = (event) => { if (!event || !event.reason || event.reason.linkAction !== 'add-to-cart') { return; @@ -265,8 +248,6 @@ const handleUpdateProduct = ({ event, eventType }) => { }; const attachEventListeners = () => { - on(document, 'change', `${prestashop.selectors.product.variants} *[name]`, handleProductFormChange); - prestashop.on('updateCart', handleUpdateCart); // Refresh all the product content prestashop.on('updateProduct', handleUpdateProduct); diff --git a/_dev/js/theme/core/selectors.js b/_dev/js/theme/core/selectors.js index fdea55cd..548ef21f 100644 --- a/_dev/js/theme/core/selectors.js +++ b/_dev/js/theme/core/selectors.js @@ -14,8 +14,8 @@ prestashop.selectors = { customizationModal: '.js-customization-modal', imageContainer: '.js-quickview js-images-container, .page-product:not(.js-quickview-open) .js-product-container .images-container', container: '.js-product-container', - availability: '#product-availability, .js-product-availability', - actions: '.product-actions, .js-product-actions', + availability: '.js-product-availability', + actions: '.js-product-actions', variants: '.js-product-variants', refresh: '.js-product-refresh', miniature: '.js-product-miniature', From 7591d7dd22a6ec55b0ab15b46a97026239e55191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 01:40:41 +0200 Subject: [PATCH 5/9] jsdocs for product handlers --- .../product/handler/product/productFormChangeHandler.js | 5 +++++ .../core/product/handler/product/productPopStateHandler.js | 5 +++++ .../product/handler/product/productUpdateErrorHandler.js | 4 ++++ .../core/product/handler/product/updatedProductHandler.js | 6 ++++++ _dev/js/theme/core/product/productController.js | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js b/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js index aa61577c..d68041fe 100644 --- a/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js +++ b/_dev/js/theme/core/product/handler/product/productFormChangeHandler.js @@ -3,6 +3,11 @@ import productStateStore from '../../store/productStateStore'; const { setFormChanged } = productStateStore(); +/** + * Sets the form changed state + * Side effect: emits 'updateProduct' event + * @param event {Event} + */ const productFormChangeHandler = (event) => { setFormChanged(true); diff --git a/_dev/js/theme/core/product/handler/product/productPopStateHandler.js b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js index 21b615e5..b40bb602 100644 --- a/_dev/js/theme/core/product/handler/product/productPopStateHandler.js +++ b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js @@ -6,6 +6,11 @@ const { setOnPopState, isFormChanged } = productStateStore(); const { get } = productFormDataPersister(); +/** + * Handle popstate event for product page + * Side effect: emits 'updateProduct' event, sets popState in productStateStore + * @param {Event} event + */ const productPopStateHandler = (event) => { setOnPopState(true); diff --git a/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js b/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js index b452c4f9..e09b1b46 100644 --- a/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js +++ b/_dev/js/theme/core/product/handler/product/productUpdateErrorHandler.js @@ -2,6 +2,10 @@ import useAlertToast from '../../../../components/useAlertToast'; const { danger } = useAlertToast(); +/** + * Handle product update error + * @param event + */ const productUpdateErrorHandler = (event) => { if (event?.errorMessage) { danger(event.errorMessage); diff --git a/_dev/js/theme/core/product/handler/product/updatedProductHandler.js b/_dev/js/theme/core/product/handler/product/updatedProductHandler.js index 0ba9bef7..335709c3 100644 --- a/_dev/js/theme/core/product/handler/product/updatedProductHandler.js +++ b/_dev/js/theme/core/product/handler/product/updatedProductHandler.js @@ -3,6 +3,12 @@ import productStateStore from '../../store/productStateStore'; const { isOnPopState, setOnPopState } = productStateStore(); +/** + * Handle updated product on 'updatedProduct' event + * Side effect: changes the url and title, sets popState in productStateStore + * @param event - event object with response data + * @param formData - form data + */ const updatedProductHandler = ({ product_url: responseProductUrl = null, id_product_attribute: responseIdProductAttribute = null, diff --git a/_dev/js/theme/core/product/productController.js b/_dev/js/theme/core/product/productController.js index 40eed993..d614984c 100644 --- a/_dev/js/theme/core/product/productController.js +++ b/_dev/js/theme/core/product/productController.js @@ -11,6 +11,11 @@ import productFormChangeHandler from './handler/product/productFormChangeHandler const { on } = useEvent(); const { persist } = productFormDataPersister(); +/** + * Persists form data on init + * Side effect: set formData in productFormDataPersister + * @return {void} + */ const persistFormDataOnInit = () => { const form = document.querySelector(`${prestashop.selectors.product.actions} form`); From 3a72a2e12b0b82cdc97874b7d535834e1a6b977c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 02:40:55 +0200 Subject: [PATCH 6/9] product page update event --- _dev/js/theme/components/product.js | 53 ++-- .../components/useCustomQuantityInput.js | 40 ++- .../handler/product/productPopStateHandler.js | 2 +- .../updateProductCustomizationHandler.js | 19 ++ .../updateProductDOMElementsHandler.js | 35 +++ .../handler/product/updateProductHandler.js | 71 +++++- .../product/updateQuantityInputHandler.js | 15 ++ .../theme/core/product/productController.js | 4 +- _dev/js/theme/core/product/updateProduct.js | 235 ------------------ .../catalog/_partials/product-add-to-cart.tpl | 50 ++-- templates/catalog/product.tpl | 2 +- 11 files changed, 233 insertions(+), 293 deletions(-) create mode 100644 _dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js create mode 100644 _dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js create mode 100644 _dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js diff --git a/_dev/js/theme/components/product.js b/_dev/js/theme/components/product.js index 05875a4b..a611c987 100644 --- a/_dev/js/theme/components/product.js +++ b/_dev/js/theme/components/product.js @@ -1,5 +1,7 @@ import $ from 'jquery'; import prestashop from 'prestashop'; +import useCustomQuantityInput from './useCustomQuantityInput'; +import { each } from '../utils/DOMHelpers'; $(() => { const createInputFile = () => { @@ -13,35 +15,24 @@ $(() => { }); }; - const createProductSpin = () => { - const $quantityInput = $('#quantity_wanted'); + const initProductQuantityInput = () => { + const initQtySpinner = (spinner) => { + const { init } = useCustomQuantityInput(spinner, { + onQuantityChange: (event) => { + prestashop.emit('updateProduct', { + eventType: 'updatedProductQuantity', + event, + }); + }, + }); - $quantityInput.TouchSpin({ - verticalupclass: 'material-icons touchspin-up', - verticaldownclass: 'material-icons touchspin-down', - buttondown_class: 'btn btn-touchspin js-touchspin', - buttonup_class: 'btn btn-touchspin js-touchspin', - min: parseInt($quantityInput.attr('min'), 10), - max: 1000000, - }); + init(); + }; - $quantityInput.on('focusout', () => { - if ($quantityInput.val() === '' || $quantityInput.val() < $quantityInput.attr('min')) { - $quantityInput.val($quantityInput.attr('min')); - $quantityInput.trigger('change'); - } - }); - - $('body').on('change keyup', '#quantity_wanted', (event) => { - $(event.currentTarget).trigger('touchspin.stopspin'); - prestashop.emit('updateProduct', { - eventType: 'updatedProductQuantity', - event, - }); - }); + each('.js-product-qty-spinner', initQtySpinner); }; - createProductSpin(); + initProductQuantityInput(); createInputFile(); let updateEvenType = false; @@ -66,17 +57,6 @@ $(() => { prestashop.on('updatedProduct', (event) => { createInputFile(); - if (event && event.product_minimal_quantity) { - const minimalProductQuantity = parseInt(event.product_minimal_quantity, 10); - const quantityInputSelector = '#quantity_wanted'; - const quantityInput = $(quantityInputSelector); - - // @see http://www.virtuosoft.eu/code/bootstrap-touchspin/ about Bootstrap TouchSpin - quantityInput.trigger('touchspin.updatesettings', { - min: minimalProductQuantity, - }); - } - if (updateEvenType === 'updatedProductCombination') { $('.js-product-images').replaceWith(event.product_cover_thumbnails); $('.js-product-images-modal').replaceWith(event.product_images_modal); @@ -86,5 +66,6 @@ $(() => { updateEvenType = false; prestashop.pageLazyLoad.update(); + initProductQuantityInput(); }); }); diff --git a/_dev/js/theme/components/useCustomQuantityInput.js b/_dev/js/theme/components/useCustomQuantityInput.js index 5fb49144..38355a85 100644 --- a/_dev/js/theme/components/useCustomQuantityInput.js +++ b/_dev/js/theme/components/useCustomQuantityInput.js @@ -112,6 +112,40 @@ const useCustomQuantityInput = (spinnerElement, { maxQty = input.getAttribute('max') ? parseInt(input.getAttribute('max'), 10) : defaultMaxQty; }; + /** + * Update minimum quantity + * @method updateMinQty + * @public + * @param newMinQty + * @returns {void} + */ + const updateMinQty = (newMinQty) => { + const qtyValue = Number.parseInt(newMinQty, 10); + + if (Number.isNaN(qtyValue)) { + throw new Error('Invalid minimum quantity'); + } + + minQty = qtyValue; + }; + + /** + * Update maximum quantity + * @method updateMaxQty + * @public + * @param newMaxQty + * @returns {void} + */ + const updateMaxQty = (newMaxQty) => { + const qtyValue = Number.parseInt(newMaxQty, 10); + + if (Number.isNaN(qtyValue)) { + throw new Error('Invalid maximum quantity'); + } + + maxQty = qtyValue; + }; + /** * Should dispatch change event * @method shouldDispatchChange @@ -299,7 +333,7 @@ const useCustomQuantityInput = (spinnerElement, { /** * Destroy spinner instance and detach events * @method destroy - * @static + * @public * @returns {void} */ const destroy = () => { @@ -310,7 +344,7 @@ const useCustomQuantityInput = (spinnerElement, { /** * Initialize spinner instance and attach events * @method init - * @static + * @public * @returns {void} */ const init = () => { @@ -324,6 +358,8 @@ const useCustomQuantityInput = (spinnerElement, { init, destroy, getDOMElements, + updateMinQty, + updateMaxQty, }; }; diff --git a/_dev/js/theme/core/product/handler/product/productPopStateHandler.js b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js index b40bb602..12c2d69c 100644 --- a/_dev/js/theme/core/product/handler/product/productPopStateHandler.js +++ b/_dev/js/theme/core/product/handler/product/productPopStateHandler.js @@ -20,7 +20,7 @@ const productPopStateHandler = (event) => { return; } - const form = document.querySelector(`${prestashop.selectors.product.actions} form`); + const form = document.querySelector(`${prestashop.selectors.product.actions} .js-product-form`); const handleFormElementState = (data) => { const element = form.querySelector(`[name="${data.name}"]`); diff --git a/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js b/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js new file mode 100644 index 00000000..f88dccc0 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js @@ -0,0 +1,19 @@ +import prestashop from 'prestashop'; + +const updateProductCustomizationHandler = (eventType, { id_customization: idCustomization }) => { + const customizationIdInput = document.querySelector(prestashop.selectors.cart.productCustomizationId); + + // refill customizationId input value when updating quantity or combination + if ( + (eventType === 'updatedProductQuantity' || eventType === 'updatedProductCombination') + && idCustomization + ) { + if (customizationIdInput) { + customizationIdInput.value = idCustomization; + } + } else if (customizationIdInput) { + customizationIdInput.value = 0; + } +}; + +export default updateProductCustomizationHandler; diff --git a/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js b/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js new file mode 100644 index 00000000..fa115135 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js @@ -0,0 +1,35 @@ +import prestashop from 'prestashop'; +import parseToHtml from '../../../../utils/parseToHtml'; +import { each } from '../../../../utils/DOMHelpers'; + +const replaceElement = (element, htmlString) => { + const newElement = parseToHtml(htmlString); + + element.replaceWith(newElement); +}; + +const updateProductDOMElementsHandler = ({ + /* eslint-disable */ + product_cover_thumbnails, + product_prices, + product_customization, + product_variants, + product_discounts, + product_additional_info, + product_details, + product_flags, + product_add_to_cart, + /* eslint-enable */ +}) => { + each(prestashop.selectors.product.imageContainer, (el) => replaceElement(el, product_cover_thumbnails)); + each(prestashop.selectors.product.prices, (el) => replaceElement(el, product_prices)); + each(prestashop.selectors.product.customization, (el) => replaceElement(el, product_customization)); + each(prestashop.selectors.product.variantsUpdate, (el) => replaceElement(el, product_variants)); + each(prestashop.selectors.product.discounts, (el) => replaceElement(el, product_discounts)); + each(prestashop.selectors.product.additionalInfos, (el) => replaceElement(el, product_additional_info)); + each(prestashop.selectors.product.details, (el) => replaceElement(el, product_details)); + each(prestashop.selectors.product.flags, (el) => replaceElement(el, product_flags)); + each(prestashop.selectors.product.addToCart, (el) => replaceElement(el, product_add_to_cart)); +}; + +export default updateProductDOMElementsHandler; diff --git a/_dev/js/theme/core/product/handler/product/updateProductHandler.js b/_dev/js/theme/core/product/handler/product/updateProductHandler.js index 7f72809c..231f44c0 100644 --- a/_dev/js/theme/core/product/handler/product/updateProductHandler.js +++ b/_dev/js/theme/core/product/handler/product/updateProductHandler.js @@ -1,5 +1,74 @@ -const updateProductHandler = () => { +import prestashop from 'prestashop'; +import isQuickViewOpen from '../../utils/isQuickViewOpen'; +import isProductPreview from '../../utils/isProductPreview'; +import updateProductRequest from '../../request/product/updateProductRequest'; +import productFormDataPersister from '../../persister/productFormDataPersister'; +import productStateStore from '../../store/productStateStore'; +import updateQuantityInputHandler from './updateQuantityInputHandler'; +import updateProductCustomizationHandler from './updateProductCustomizationHandler'; +import updateProductDOMElementsHandler from './updateProductDOMElementsHandler'; +import { fromSerializeObject } from '../../../../utils/formSerialize'; +import useAlertToast from '../../../../components/useAlertToast'; +const { danger } = useAlertToast(); +const { getCurrentRequestDelayedId, setCurrentRequestDelayedId } = productStateStore(); + +/** + * Handle 'updateProduct' event + * @param params - event object + * @param params.eventType {string} - event type + * @return {Promise} + */ +const updateProductHandler = async ({ eventType }) => { + const productActions = document.querySelector(prestashop.selectors.product.actions); + const quantityWantedInput = productActions.querySelector(prestashop.selectors.quantityWanted); + + const form = productActions.querySelector('.js-product-form'); + const formSerialized = fromSerializeObject(form); + const updateRatingEvent = new Event('updateRating'); + + if (getCurrentRequestDelayedId()) { + clearTimeout(getCurrentRequestDelayedId()); + } + + const updateDelay = 50; + + const timeoutId = setTimeout(async () => { + const idProductAttribute = formSerialized?.id_product_attribute || 0; + const idCustomization = formSerialized?.id_customization || 0; + + const payload = { + quantity_wanted: Number.parseInt(quantityWantedInput.value, 10), + preview: isProductPreview() ? 1 : 0, + quickview: isQuickViewOpen() ? 1 : 0, + ...formSerialized, + id_product: Number.parseInt(formSerialized.id_product, 10), + id_product_attribute: Number.parseInt(idProductAttribute, 10), + id_customization: Number.parseInt(idCustomization, 10), + }; + const { getRequest } = updateProductRequest(payload); + + try { + const data = await getRequest(); + + updateProductDOMElementsHandler(data); + updateProductCustomizationHandler(eventType, data); + updateQuantityInputHandler(eventType, data); + + document.dispatchEvent(updateRatingEvent); + + const { persist, get: getPersistedData } = productFormDataPersister(); + persist(form); + + prestashop.emit('updatedProduct', data, getPersistedData()); + } catch (e) { + danger(prestashop.t.alert.genericHttpError); + } + + setCurrentRequestDelayedId(null); + }, updateDelay); + + setCurrentRequestDelayedId(timeoutId); }; export default updateProductHandler; diff --git a/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js b/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js new file mode 100644 index 00000000..d0fffc53 --- /dev/null +++ b/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js @@ -0,0 +1,15 @@ +import prestashop from 'prestashop'; + +const updateQuantityInputHandler = (eventType, { product_minimal_quantity: productMinimalQuantity }) => { + const minimalProductQuantity = parseInt( + productMinimalQuantity, + 10, + ); + + if (!Number.isNaN(minimalProductQuantity) && eventType !== 'updatedProductQuantity') { + const newQtyInput = document.querySelector(`${prestashop.selectors.product.actions} ${prestashop.selectors.quantityWanted}`); + newQtyInput.value = minimalProductQuantity; + } +}; + +export default updateQuantityInputHandler; diff --git a/_dev/js/theme/core/product/productController.js b/_dev/js/theme/core/product/productController.js index d614984c..4ea81aed 100644 --- a/_dev/js/theme/core/product/productController.js +++ b/_dev/js/theme/core/product/productController.js @@ -6,6 +6,7 @@ import productUpdateErrorHandler from './handler/product/productUpdateErrorHandl import productFormDataPersister from './persister/productFormDataPersister'; import productPopStateHandler from './handler/product/productPopStateHandler'; import updatedProductHandler from './handler/product/updatedProductHandler'; +import updateProductHandler from './handler/product/updateProductHandler'; import productFormChangeHandler from './handler/product/productFormChangeHandler'; const { on } = useEvent(); @@ -17,7 +18,7 @@ const { persist } = productFormDataPersister(); * @return {void} */ const persistFormDataOnInit = () => { - const form = document.querySelector(`${prestashop.selectors.product.actions} form`); + const form = document.querySelector(`${prestashop.selectors.product.actions} .js-product-form`); if (form) { persist(form); @@ -31,6 +32,7 @@ const productController = () => { on(document, 'change', `${prestashop.selectors.product.variants} *[name]`, productFormChangeHandler); window.addEventListener('popstate', productPopStateHandler); + prestashop.on('updateProduct', updateProductHandler); prestashop.on('updatedProduct', updatedProductHandler); prestashop.on('showErrorNextToAddtoCartButton', productUpdateErrorHandler); prestashop.on('clickQuickView', ({ idProduct, idProductAttribute }) => quickViewHandler(idProduct, idProductAttribute)); diff --git a/_dev/js/theme/core/product/updateProduct.js b/_dev/js/theme/core/product/updateProduct.js index fff3c766..f1963f5f 100644 --- a/_dev/js/theme/core/product/updateProduct.js +++ b/_dev/js/theme/core/product/updateProduct.js @@ -1,234 +1,4 @@ import prestashop from 'prestashop'; -import { fromSerializeObject } from '@js/theme/utils/formSerialize'; -import parseToHtml from '@js/theme/utils/parseToHtml'; -import useAlertToast from '@js/theme/components/useAlertToast'; -import isQuickViewOpen from './utils/isQuickViewOpen'; -import isProductPreview from './utils/isProductPreview'; -import updateProductRequest from './request/product/updateProductRequest'; -import productFormDataPersister from './persister/productFormDataPersister'; -import productStateStore from './store/productStateStore'; - -const { getCurrentRequestDelayedId, setCurrentRequestDelayedId } = productStateStore(); - -const { danger } = useAlertToast(); - -const replaceDOMElements = ({ - /* eslint-disable */ - product_cover_thumbnails, - product_prices, - product_customization, - product_variants, - product_discounts, - product_additional_info, - product_details, - product_flags, - /* eslint-enable */ -}) => { - document.querySelector(prestashop.selectors.product.imageContainer) - ?.replaceWith(parseToHtml(product_cover_thumbnails)); - - document.querySelector(prestashop.selectors.product.prices) - ?.replaceWith(parseToHtml(product_prices)); - - document.querySelector(prestashop.selectors.product.customization) - ?.replaceWith(parseToHtml(product_customization)); - - document.querySelector(prestashop.selectors.product.variantsUpdate) - ?.replaceWith(parseToHtml(product_variants)); - - document.querySelector(prestashop.selectors.product.discounts) - ?.replaceWith(parseToHtml(product_discounts)); - - document.querySelector(prestashop.selectors.product.additionalInfos) - ?.replaceWith(parseToHtml(product_additional_info)); - - document.querySelector(prestashop.selectors.product.details) - ?.replaceWith(parseToHtml(product_details)); - - document.querySelector(prestashop.selectors.product.flags) - ?.replaceWith(parseToHtml(product_flags)); -}; - -/** - * Find DOM elements and replace their content - * - * @param {object} replacement Data to be replaced on the current page - */ -const replaceAddToCartSection = ({ addToCartSnippet, targetParent, targetSelector }) => { - const destinationObject = targetParent.querySelector(targetSelector); - - if (destinationObject === null) { - return; - } - - const replace = addToCartSnippet.querySelector(targetSelector); - - if (replace) { - destinationObject.replaceWith(parseToHtml(replace.outerHTML)); - } else { - destinationObject.textContent = ''; - } -}; - -/** - * Replace all "add to cart" sections but the quantity input - * in order to keep quantity field intact i.e. - * - * @param {object} data of updated product and cat - */ -const replaceAddToCartSections = ({ - // eslint-disable-next-line camelcase - product_add_to_cart, -}) => { - let productAddToCart = null; - const addToCartHtml = parseToHtml(product_add_to_cart); - const addToCartElements = document.querySelectorAll('.js-product-add-to-cart'); - - for (let i = 0; i < addToCartElements.length; i += 1) { - if (addToCartElements[i].classList.contains('product-add-to-cart')) { - productAddToCart = addToCartElements[i]; - } - } - - if (productAddToCart === null) { - danger(prestashop.t.alert.genericHttpError); - - return; - } - - let currentAddToCartBlockSelector = prestashop.selectors.product.addToCart; - - if (isQuickViewOpen()) { - currentAddToCartBlockSelector = `.js-quickview ${currentAddToCartBlockSelector}`; - } - - const currentAddToCartBlock = document.querySelector(currentAddToCartBlockSelector); - const productAvailabilitySelector = '.js-add-to-cart-btn-wrapper'; - const productAvailabilityMessageSelector = '.js-product-availability'; - const productMinimalQuantitySelector = '.js-product-minimal-quantity'; - - replaceAddToCartSection({ - addToCartSnippet: addToCartHtml, - targetParent: currentAddToCartBlock, - targetSelector: productAvailabilitySelector, - }); - - replaceAddToCartSection({ - addToCartSnippet: addToCartHtml, - targetParent: currentAddToCartBlock, - targetSelector: productAvailabilityMessageSelector, - }); - - replaceAddToCartSection({ - addToCartSnippet: addToCartHtml, - targetParent: currentAddToCartBlock, - targetSelector: productMinimalQuantitySelector, - }); -}; - -/** - * Update the product html - * - * @param {string} event - * @param {string} eventType - * @param {string} updateUrl - */ -const updateProductData = async (event, eventType) => { - const productActions = document.querySelector(prestashop.selectors.product.actions); - const quantityWantedInput = productActions.querySelector(prestashop.selectors.quantityWanted); - - const form = productActions.querySelector('form'); - const formSerialized = fromSerializeObject(form); - const updateRatingEvent = new Event('updateRating'); - - // New request only if new value - if ( - event - && event.type === 'keyup' - && quantityWantedInput?.value === quantityWantedInput?.dataset.oldValue - ) { - return; - } - - quantityWantedInput.dataset.oldValue = quantityWantedInput.value ? quantityWantedInput.value : 1; - - if (getCurrentRequestDelayedId()) { - clearTimeout(getCurrentRequestDelayedId()); - } - - // Most update need to occur (almost) instantly, but in some cases (like keyboard actions) - // we need to delay the update a bit more - let updateDelay = 30; - - if (eventType === 'updatedProductQuantity') { - updateDelay = 750; - } - - const timeoutId = setTimeout(async () => { - const idProductAttribute = formSerialized?.id_product_attribute || 0; - const idCustomization = formSerialized?.id_customization || 0; - - const payload = { - quantity_wanted: Number.parseInt(quantityWantedInput.value, 10), - preview: isProductPreview() ? 1 : 0, - quickview: isQuickViewOpen() ? 1 : 0, - ...formSerialized, - id_product: Number.parseInt(formSerialized.id_product, 10), - id_product_attribute: Number.parseInt(idProductAttribute, 10), - id_customization: Number.parseInt(idCustomization, 10), - }; - const { getRequest } = updateProductRequest(payload); - - try { - const data = await getRequest(); - - // Used to avoid image blinking if same image = epileptic friendly - - replaceDOMElements(data); - - const customizationIdInput = document.querySelector(prestashop.selectors.cart.productCustomizationId); - // refill customizationId input value when updating quantity or combination - if ( - (eventType === 'updatedProductQuantity' || eventType === 'updatedProductCombination') - && data.id_customization - ) { - if (customizationIdInput) { - customizationIdInput.value = data.id_customization; - } - } else if (customizationIdInput) { - customizationIdInput.value = 0; - } - - replaceAddToCartSections(data); - const minimalProductQuantity = parseInt( - data.product_minimal_quantity, - 10, - ); - - document.dispatchEvent(updateRatingEvent); - - // Prevent quantity input from blinking with classic theme. - if ( - !Number.isNaN(minimalProductQuantity) - && eventType !== 'updatedProductQuantity' - ) { - quantityWantedInput.setAttribute('min', minimalProductQuantity); - quantityWantedInput.value = minimalProductQuantity; - } - - const { persist, get: getPersistedData } = productFormDataPersister(); - persist(form); - - prestashop.emit('updatedProduct', data, getPersistedData()); - } catch (e) { - danger(prestashop.t.alert.genericHttpError); - } - - setCurrentRequestDelayedId(null); - }, updateDelay); - - setCurrentRequestDelayedId(timeoutId); -}; const handleUpdateCart = (event) => { if (!event || !event.reason || event.reason.linkAction !== 'add-to-cart') { @@ -243,14 +13,9 @@ const handleUpdateCart = (event) => { } }; -const handleUpdateProduct = ({ event, eventType }) => { - updateProductData(event, eventType); -}; - const attachEventListeners = () => { prestashop.on('updateCart', handleUpdateCart); // Refresh all the product content - prestashop.on('updateProduct', handleUpdateProduct); }; const updateProduct = () => { diff --git a/templates/catalog/_partials/product-add-to-cart.tpl b/templates/catalog/_partials/product-add-to-cart.tpl index 2bd6c6ac..acadc8b8 100644 --- a/templates/catalog/_partials/product-add-to-cart.tpl +++ b/templates/catalog/_partials/product-add-to-cart.tpl @@ -28,24 +28,42 @@ {block name='product_quantity'}
- +
+ +
+ +
+ +
+ +
-
-
{/if} From 095112bc5d0c4f23c3a71e419949c79ffa1f14bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 02:48:11 +0200 Subject: [PATCH 8/9] Add to cart fix --- .../cart/handler/cart/addToCartHandler.js | 4 +-- _dev/js/theme/core/product/index.js | 2 -- _dev/js/theme/core/product/updateProduct.js | 25 ------------------- 3 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 _dev/js/theme/core/product/updateProduct.js diff --git a/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js b/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js index 76e5c98a..1407294f 100644 --- a/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js +++ b/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js @@ -47,8 +47,8 @@ const addToCartHandler = async (event) => { const payload = { id_product: idProduct, qty, - id_product_attribute: idProductAttribute, - id_customization: idCustomization, + id_product_attribute: Number.parseInt(idProductAttribute, 10), + id_customization: Number.parseInt(idCustomization, 10), }; const { getRequest } = addToCartRequest(payload); diff --git a/_dev/js/theme/core/product/index.js b/_dev/js/theme/core/product/index.js index d03ff335..1a8dfa23 100644 --- a/_dev/js/theme/core/product/index.js +++ b/_dev/js/theme/core/product/index.js @@ -1,10 +1,8 @@ -import updateProduct from '@js/theme/core/product/updateProduct'; import productController from './productController'; import DOMReady from '../../utils/DOMReady'; const { init } = productController(); DOMReady(() => { - updateProduct(); init(); }); diff --git a/_dev/js/theme/core/product/updateProduct.js b/_dev/js/theme/core/product/updateProduct.js deleted file mode 100644 index f1963f5f..00000000 --- a/_dev/js/theme/core/product/updateProduct.js +++ /dev/null @@ -1,25 +0,0 @@ -import prestashop from 'prestashop'; - -const handleUpdateCart = (event) => { - if (!event || !event.reason || event.reason.linkAction !== 'add-to-cart') { - return; - } - - const quantityInput = document.querySelector('#quantity_wanted'); - // Force value to 1, it will automatically trigger updateProduct and reset the appropriate min value if needed - - if (quantityInput) { - quantityInput.value = 1; - } -}; - -const attachEventListeners = () => { - prestashop.on('updateCart', handleUpdateCart); - // Refresh all the product content -}; - -const updateProduct = () => { - attachEventListeners(); -}; - -export default updateProduct; From e11f6bc4a75767178c9a6fc5bf14a654a2a14ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Mon, 2 Oct 2023 02:58:41 +0200 Subject: [PATCH 9/9] jsdocs fixes --- .../updateProductCustomizationHandler.js | 8 ++++++++ .../updateProductDOMElementsHandler.js | 20 +++++++++++++++++++ .../product/updateQuantityInputHandler.js | 6 ++++++ .../request/product/updateProductRequest.js | 19 ++++++++++++++++-- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js b/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js index f88dccc0..48d3d692 100644 --- a/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js +++ b/_dev/js/theme/core/product/handler/product/updateProductCustomizationHandler.js @@ -1,5 +1,13 @@ import prestashop from 'prestashop'; +/** + * Update product customization input value + * Side effect: update product customization input value + * @param eventType {string} - event type + * @param eventData {object} - event data + * @param eventData.id_customization {number} - customization id + * @return {void} + */ const updateProductCustomizationHandler = (eventType, { id_customization: idCustomization }) => { const customizationIdInput = document.querySelector(prestashop.selectors.cart.productCustomizationId); diff --git a/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js b/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js index fa115135..160468a9 100644 --- a/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js +++ b/_dev/js/theme/core/product/handler/product/updateProductDOMElementsHandler.js @@ -2,12 +2,32 @@ import prestashop from 'prestashop'; import parseToHtml from '../../../../utils/parseToHtml'; import { each } from '../../../../utils/DOMHelpers'; +/** + * Replace element with new html string + * @param element {HTMLElement} - element to replace + * @param htmlString {string} - html string to replace with + */ const replaceElement = (element, htmlString) => { const newElement = parseToHtml(htmlString); element.replaceWith(newElement); }; +/** + * Update DOM elements of the product page + * Side effect: update DOM elements of the product page + * @param eventData {object} - event data + * @param eventData.product_cover_thumbnails {string} - product cover thumbnails html string + * @param eventData.product_prices {string} - product prices html string + * @param eventData.product_customization {string} - product customization html string + * @param eventData.product_variants {string} - product variants html string + * @param eventData.product_discounts {string} - product discounts html string + * @param eventData.product_additional_info {string} - product additional info html string + * @param eventData.product_details {string} - product details html string + * @param eventData.product_flags {string} - product flags html string + * @param eventData.product_add_to_cart {string} - product add to cart html string + * @return {void} + */ const updateProductDOMElementsHandler = ({ /* eslint-disable */ product_cover_thumbnails, diff --git a/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js b/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js index d0fffc53..9339378d 100644 --- a/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js +++ b/_dev/js/theme/core/product/handler/product/updateQuantityInputHandler.js @@ -1,5 +1,11 @@ import prestashop from 'prestashop'; +/** + * Update quantity input value + * @param eventType {string} - event type + * @param event {object} - event data + * @param event.product_minimal_quantity {number} - product minimal quantity + */ const updateQuantityInputHandler = (eventType, { product_minimal_quantity: productMinimalQuantity }) => { const minimalProductQuantity = parseInt( productMinimalQuantity, diff --git a/_dev/js/theme/core/product/request/product/updateProductRequest.js b/_dev/js/theme/core/product/request/product/updateProductRequest.js index c8c4dc9b..62da2d93 100644 --- a/_dev/js/theme/core/product/request/product/updateProductRequest.js +++ b/_dev/js/theme/core/product/request/product/updateProductRequest.js @@ -8,7 +8,23 @@ const { dispatch, abortAll } = useHttpController(); /** * @typedef ServerResponse * @type {object} - * @property {string} address_form - new address form html content + * @property {number} id_customization - saved customization id + * @property {number} id_product_attribute - saved product attribute id + * @property {number} product_minimal_quantity - product minimal quantity + * @property {boolean} is_quick_view - is quick view + * @property {boolean} product_has_combinations - product has combinations + * @property {string} product_url - product url address + * @property {string} product_title - product meta title + * @property {string} product_add_to_cart - product add to cart html content + * @property {string} product_additional_info - product additional info html content + * @property {string} product_cover_thumbnails - product cover thumbnails html content + * @property {string} product_customization - product customization html content + * @property {string} product_details - product details html content + * @property {string} product_discounts - product discounts html content + * @property {string} product_flags - product flags html content + * @property {string} product_prices - product prices html content + * @property {string} product_images_modal - product images modal html content + * @property {string} product_variants - product variants html content */ /** @@ -24,7 +40,6 @@ const { dispatch, abortAll } = useHttpController(); * @param payload.action {string} - optional, default refresh * @param payload.group[] {array} - array of attributes groups - optional * @example - * const url = 'address-form.com/url'; // url to update address form * const payload = { * id_product: 1, * id_product_attribute: 1,