diff --git a/_dev/.eslintrc.js b/_dev/.eslintrc.js index 849ed84c..021e7f71 100644 --- a/_dev/.eslintrc.js +++ b/_dev/.eslintrc.js @@ -15,7 +15,10 @@ module.exports = { }, extends: ['airbnb-base'], rules: { - 'max-len': ['error', {code: 140}], + 'max-len': ['error', { + code: 140, + ignoreComments: true, + }], 'no-underscore-dangle': 'off', 'no-restricted-syntax': 'off', 'no-param-reassign': 'off' diff --git a/_dev/js/theme/components/display/useToggleDisplay.js b/_dev/js/theme/components/display/useToggleDisplay.js index d7e128c9..be754ad9 100644 --- a/_dev/js/theme/components/display/useToggleDisplay.js +++ b/_dev/js/theme/components/display/useToggleDisplay.js @@ -3,7 +3,7 @@ const useToggleDisplay = () => { /** * Show element - * @param element Element to show + * @param element {HTMLElement} Element to show * @returns {void} */ const show = (element) => { @@ -13,7 +13,7 @@ const useToggleDisplay = () => { /** * Hide element - * @param element Element to hide + * @param element {HTMLElement} Element to hide * @returns {void} */ const hide = (element) => { @@ -23,8 +23,8 @@ const useToggleDisplay = () => { /** * Toggle element - * @param element Element to toggle - * @param display Display or hide + * @param element {HTMLElement} Element to toggle + * @param display {boolean} Display or hide * @returns {void} */ const toggle = (element, display) => { diff --git a/_dev/js/theme/components/http/useDefaultHttpRequest.js b/_dev/js/theme/components/http/useDefaultHttpRequest.js new file mode 100644 index 00000000..3bf3f859 --- /dev/null +++ b/_dev/js/theme/components/http/useDefaultHttpRequest.js @@ -0,0 +1,32 @@ +import prestashop from 'prestashop'; +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 + * @returns {Promise} + */ +const useDefaultHttpRequest = (url, payload) => { + const { request } = useHttpRequest(url); + + return new Promise((resolve, reject) => { + request + .query(payload) + .post() + .json((resp) => { + if (resp.errors) { + const errors = Array.isArray(resp.errors) ? resp.errors : [resp.errors]; + + reject(Error(errors.join('\n'))); + } else { + resolve(resp); + } + }) + .catch(() => { + reject(Error(prestashop.t.alert.genericHttpError)); + }); + }); +}; + +export default useDefaultHttpRequest; diff --git a/_dev/js/theme/components/http/useHttpController.js b/_dev/js/theme/components/http/useHttpController.js index e57cf2a8..d93d89d8 100644 --- a/_dev/js/theme/components/http/useHttpController.js +++ b/_dev/js/theme/components/http/useHttpController.js @@ -1,8 +1,19 @@ -import getUniqueId from '@js/theme/utils/getUniqueId'; +import getUniqueId from '../../utils/getUniqueId'; +/** + * @module useHttpController + * This function is used to control HTTP requests. + */ const useHttpController = () => { let requestsStack = {}; + /** + * @method + * Adds request to request stack + * @param {number} id - unique id of request + * @param {Promise} request - request promise + * @param {AbortController} controller - AbortController object + */ const addRequestToRequestStack = (id, request, controller) => { const newRequestStack = { ...requestsStack }; newRequestStack[id] = { request, controller }; @@ -10,12 +21,24 @@ const useHttpController = () => { requestsStack = newRequestStack; }; + /** + * @method + * Removes request from request stack + * @param {number} id - unique id of request + */ const removeRequestFromRequestStack = (id) => { const { [id]: erasedId, ...newRequestStack } = requestsStack; requestsStack = newRequestStack; }; + /** + * @method + * Dispatches request and adds it to request stack + * @param {object} request - request object + * @param {AbortController} controller - AbortController object + * @returns {(function(*): Promise)|*} + */ const dispatch = (request, controller) => { const id = getUniqueId(); addRequestToRequestStack(id, request, controller); @@ -26,6 +49,11 @@ const useHttpController = () => { }; }; + /** + * @method + * Aborts all requests in request stack + * @returns {void} + */ const abortAll = () => { for (const id in requestsStack) { if (Object.hasOwn(requestsStack, id)) { diff --git a/_dev/js/theme/components/http/useHttpPayloadDefinition.js b/_dev/js/theme/components/http/useHttpPayloadDefinition.js new file mode 100644 index 00000000..e2508ba2 --- /dev/null +++ b/_dev/js/theme/components/http/useHttpPayloadDefinition.js @@ -0,0 +1,218 @@ +/** + * useHttpPayloadDefinition - validate payload against definition + * @module useHttpPayloadDefinition + * @param {object} payload - payload to validate + * @param {object} definition - definition to validate payload against + */ +const useHttpPayloadDefinition = (payload, definition) => { + const ERROR_MESSAGES = { + REQUIRED: 'field is required', + TYPE: 'field must be of type', + MIN_LENGTH: 'field must be at least', + MAX_LENGTH: 'field must be at most', + MIN_VALUE: 'field must be at least', + MAX_VALUE: 'field must be at most', + REGEX: 'field must match the following regex', + }; + + const defaultDefinitionForField = { + required: false, + minLength: null, + maxLength: null, + minValue: null, + maxValue: null, + regex: null, + }; + + const requiredDefinitionFields = [ + 'type', + ]; + + if (!definition) { + throw new Error('Payload definition is required'); + } + + if (!payload) { + throw new Error('Payload is required'); + } + + /** + * @method + * Sets default definition for field + * @param customDefinition - custom definition for field + * @returns {object} - definition for field + */ + const setDefaultDefinitionForField = (customDefinition) => ({ + ...defaultDefinitionForField, + ...customDefinition, + }); + + const payloadDefinition = {}; + + Object.keys(definition).forEach((fieldName) => { + const definitionForField = definition[fieldName]; + + payloadDefinition[fieldName] = setDefaultDefinitionForField(definitionForField); + }); + + /** + * @method + * Gets value type + * @param value + * @returns {"undefined"|"object"|"boolean"|"number"|"string"|"function"|"symbol"|"bigint"} + */ + const getValueType = (value) => typeof value; + + /** + * @method + * Validates field value against field definition and returns errors + * @param {string} fieldName - name of field + * @param {any} value - value of field + * @param {object} fieldDefinition - definition for field + * @returns {string[]} + */ + const validate = (fieldName, value, fieldDefinition) => { + const validateErrors = []; + const { + type, + required, + minLength, + maxLength, + minValue, + maxValue, + regex, + } = fieldDefinition; + + if (required && !value) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.REQUIRED}`); + } + + switch (type) { + case 'string': + if (typeof value !== 'string') { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} string, ${getValueType(value)} received`); + } + break; + case 'float': + if (typeof value !== 'number') { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} float, ${getValueType(value)} received`); + } + break; + case 'int': + if (typeof value !== 'number') { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} int, ${getValueType(value)} received`); + } + break; + case 'boolean': + if (typeof value !== 'boolean') { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} boolean, ${getValueType(value)} received`); + } + break; + case 'object': + if (typeof value !== 'object') { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} object, ${getValueType(value)} received`); + } + break; + case 'array': + if (!Array.isArray(value)) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} array, ${getValueType(value)} received`); + } + break; + default: + break; + } + + if (minLength && value.length < minLength) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MIN_LENGTH} ${minLength}`); + } + + if (maxLength && value.length > maxLength) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MAX_LENGTH} ${maxLength}`); + } + + if (minValue && value < minValue) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MIN_VALUE} ${minValue}`); + } + + if (maxValue && value > maxValue) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MAX_VALUE} ${maxValue}`); + } + + if (regex && !regex.test(value)) { + validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.REGEX} ${regex}`); + } + + return validateErrors; + }; + + /** + * @method + * Validates definition for field and returns errors + * @param fieldName + * @param fieldsDefinition + * @returns {string[]} + */ + const validateDefinitionForField = (fieldName, fieldsDefinition) => { + const definitionErrors = []; + const definitionKeys = Object.keys(fieldsDefinition); + + requiredDefinitionFields.forEach((requiredDefinitionField) => { + if (!definitionKeys.includes(requiredDefinitionField)) { + definitionErrors.push(`'${fieldName}' definition is missing ${requiredDefinitionField} field`); + } + }); + + return definitionErrors; + }; + + /** + * @method + * Validates definition and returns errors + * @param fieldsDefinition + * @returns {string[]} + */ + const validateDefinition = (fieldsDefinition) => { + const definitionErrors = []; + const definitionKeys = Object.keys(fieldsDefinition); + + definitionKeys.forEach((defName) => { + const fieldName = defName; + const fieldDef = fieldsDefinition[defName]; + const definitionForFieldErrors = validateDefinitionForField(fieldName, fieldDef); + + definitionErrors.push(...definitionForFieldErrors); + }); + + return definitionErrors; + }; + + /** + * @method + * Validates payload against definition and returns errors + * @returns {string[]} + */ + const validatePayload = () => { + const payloadErrors = []; + const definitionErrors = validateDefinition(payloadDefinition); + + if (definitionErrors.length) { + payloadErrors.push(...definitionErrors); + } + + Object.keys(payloadDefinition).forEach((fieldName) => { + const definitionForField = payloadDefinition[fieldName]; + + const definitionForFieldErrors = validate(fieldName, payload[fieldName], definitionForField); + + payloadErrors.push(...definitionForFieldErrors); + }); + + return payloadErrors; + }; + + return { + validatePayload, + }; +}; + +export default useHttpPayloadDefinition; diff --git a/_dev/js/theme/components/http/useHttpRequest.js b/_dev/js/theme/components/http/useHttpRequest.js index a2e76287..02159a5d 100644 --- a/_dev/js/theme/components/http/useHttpRequest.js +++ b/_dev/js/theme/components/http/useHttpRequest.js @@ -2,6 +2,13 @@ import wretch from 'wretch'; import QueryStringAddon from 'wretch/addons/queryString'; import AbortAddon from 'wretch/addons/abort'; +/** + * useHttpRequest + * @param url {string} - request url + * @param options {object} - request options + * @param addons {array} - request addons, wretch/addons, default used: [AbortAddon, QueryStringAddon] + * @returns {{request: wretch, controller: AbortController}} + */ const useHttpRequest = (url, options = {}, addons = []) => { if (!options?.headers) { options.headers = {}; diff --git a/_dev/js/theme/components/password/usePasswordPolicy.js b/_dev/js/theme/components/password/usePasswordPolicy.js index 3cb186dd..97e9cbbd 100644 --- a/_dev/js/theme/components/password/usePasswordPolicy.js +++ b/_dev/js/theme/components/password/usePasswordPolicy.js @@ -4,8 +4,8 @@ import prestashop from 'prestashop'; /** * Verify password score. * Estimate guesses needed to crack the password. - * @param {String} password - * @returns {Promise} + * @param {string} password + * @returns {promise} */ window.prestashop.checkPasswordScore = async (password) => { const zxcvbn = (await import('zxcvbn')).default; diff --git a/_dev/js/theme/components/useCustomQuantityInput.js b/_dev/js/theme/components/useCustomQuantityInput.js index 56ce971b..5fb49144 100644 --- a/_dev/js/theme/components/useCustomQuantityInput.js +++ b/_dev/js/theme/components/useCustomQuantityInput.js @@ -1,3 +1,17 @@ +/** + * Custom quantity input + * @module useCustomQuantityInput + * @param {HTMLElement} spinnerElement - spinner element to initialize (required) + * @param {object} configuration - configuration object (optional) + * @param {string} configuration.spinnerInitializedClass - class to add when spinner is initialized (default: js-custom-qty-spinner-initialized) + * @param {string} configuration.spinnerInputClass - class of the input element (default: js-custom-qty-spinner-input) + * @param {string} configuration.spinnerBtnClassUp - class of the up button (default: js-custom-qty-btn-up) + * @param {string} configuration.spinnerBtnClassDown - class of the down button (default: js-custom-qty-btn-down) + * @param {number} configuration.defaultMinQty - default minimum quantity (default: 1) + * @param {number} configuration.defaultMaxQty - default maximum quantity (default: 1000000) + * @param {number} configuration.timeout - timeout in ms to wait before dispatching change event (default: 500) + * @param {function} configuration.onQuantityChange - callback function to call when quantity changes + */ const useCustomQuantityInput = (spinnerElement, { spinnerInitializedClass = 'js-custom-qty-spinner-initialized', spinnerInputClass = 'js-custom-qty-spinner-input', @@ -8,13 +22,52 @@ const useCustomQuantityInput = (spinnerElement, { timeout = 500, onQuantityChange = () => {}, }) => { + /** + * Timeout id + * @private + * @type {int|null} + */ let timeoutId = null; + /** + * Start value + * @private + * @type {int|null} + */ let startValue = null; + /** + * Minimum quantity + * @private + * @type {int|null} + */ let minQty = null; + + /** + * Maximum quantity + * @private + * @type {int|null} + */ let maxQty = null; + + /** + * Current quantity + * @private + * @type {int|null} + */ let currentQty = null; + + /** + * DOM elements + * @private + * @type {Object|null} + */ let DOMElements = null; + /** + * Set DOM elements + * @method setDOMElements + * @private + * @returns {{input: HTMLElement | undefined, btnUp: HTMLElement | undefined, btnDown: HTMLElement | undefined}} + */ const setDOMElements = () => { const elements = { input: spinnerElement?.querySelector(`.${spinnerInputClass}`), @@ -27,12 +80,30 @@ const useCustomQuantityInput = (spinnerElement, { return elements; }; + /** + * Get DOM elements + * @method getDOMElements + * @public + * @returns {{input: (HTMLElement|undefined), btnUp: (HTMLElement|undefined), btnDown: (HTMLElement|undefined)}} + */ const getDOMElements = () => (DOMElements || setDOMElements()); + /** + * Reset DOM elements + * @method resetDomElements + * @private + * @returns {void} + */ const resetDomElements = () => { DOMElements = null; }; + /** + * Set initial value + * @method setInitialValue + * @private + * @returns {void} + */ const setInitialValue = () => { const { input } = getDOMElements(); startValue = input.value ? parseInt(input.value, 10) : defaultMinQty; @@ -41,8 +112,20 @@ const useCustomQuantityInput = (spinnerElement, { maxQty = input.getAttribute('max') ? parseInt(input.getAttribute('max'), 10) : defaultMaxQty; }; + /** + * Should dispatch change event + * @method shouldDispatchChange + * @private + * @returns {boolean} + */ const shouldDispatchChange = () => currentQty !== startValue; + /** + * Get operation type + * @method getOperationType + * @private + * @returns {string} increase|decrease + */ const getOperationType = () => { if (currentQty > startValue) { return 'increase'; @@ -51,8 +134,20 @@ const useCustomQuantityInput = (spinnerElement, { return 'decrease'; }; + /** + * Get quantity difference + * @method getQtyDifference + * @private + * @returns {number} + */ const getQtyDifference = () => Math.abs(currentQty - startValue); + /** + * Dispatch change event + * @method dispatchChange + * @private + * @returns {void} + */ const dispatchChange = () => { clearTimeout(timeoutId); @@ -72,6 +167,13 @@ const useCustomQuantityInput = (spinnerElement, { }, timeout); }; + /** + * Set quantity + * @param {number} qty - quantity to set + * @method setQty + * @private + * @returns {void} + */ const setQty = (qty) => { const { input } = getDOMElements(); @@ -81,8 +183,15 @@ const useCustomQuantityInput = (spinnerElement, { dispatchChange(); }; - const handleClickUp = (e) => { - e.preventDefault(); + /** + * Handle click up + * @param {Event} event - event object + * @method handleClickUp + * @private + * @returns {void} + */ + const handleClickUp = (event) => { + event.preventDefault(); let newQty = parseInt(currentQty, 10) + 1; @@ -93,8 +202,15 @@ const useCustomQuantityInput = (spinnerElement, { setQty(newQty); }; - const handleClickDown = (e) => { - e.preventDefault(); + /** + * Handle click down + * @param {Event} event - event object + * @method handleClickDown + * @private + * @returns {void} + */ + const handleClickDown = (event) => { + event.preventDefault(); let newQty = parseInt(currentQty, 10) - 1; @@ -105,6 +221,12 @@ const useCustomQuantityInput = (spinnerElement, { setQty(newQty); }; + /** + * Handle input change + * @method handleInputChange + * @private + * @returns {void} + */ const handleInputChange = () => { const { input } = getDOMElements(); let newQty = parseInt(input.value, 10); @@ -120,14 +242,32 @@ const useCustomQuantityInput = (spinnerElement, { setQty(newQty); }; + /** + * Add initialized class + * @method setInitializedClass + * @private + * @returns {void} + */ const setInitializedClass = () => { spinnerElement?.classList.add(spinnerInitializedClass); }; + /** + * Remove initialized class + * @method removeInitializedClass + * @private + * @returns {void} + */ const removeInitializedClass = () => { spinnerElement?.classList.remove(spinnerInitializedClass); }; + /** + * Detach events from DOM elements + * @method detachEvents + * @private + * @returns {void} + */ const detachEvents = () => { const { input, btnUp, btnDown } = getDOMElements(); @@ -139,6 +279,12 @@ const useCustomQuantityInput = (spinnerElement, { input?.removeEventListener('blur', handleInputChange); }; + /** + * Attach events to DOM elements + * @method attachEvents + * @private + * @returns {void} + */ const attachEvents = () => { const { input, btnUp, btnDown } = getDOMElements(); @@ -150,11 +296,23 @@ const useCustomQuantityInput = (spinnerElement, { input?.addEventListener('blur', handleInputChange); }; + /** + * Destroy spinner instance and detach events + * @method destroy + * @static + * @returns {void} + */ const destroy = () => { detachEvents(); resetDomElements(); }; + /** + * Initialize spinner instance and attach events + * @method init + * @static + * @returns {void} + */ const init = () => { destroy(); setInitialValue(); diff --git a/_dev/js/theme/core/address/addressController.js b/_dev/js/theme/core/address/addressController.js new file mode 100644 index 00000000..6940e528 --- /dev/null +++ b/_dev/js/theme/core/address/addressController.js @@ -0,0 +1,16 @@ +import useEvent from '../../components/event/useEvent'; +import changeAddressCountryHandler from './handler/changeAddressCountryHandler'; + +const { on } = useEvent(); + +const addressController = () => { + const init = () => { + on(document, 'change', '.js-country', changeAddressCountryHandler); + }; + + return { + init, + }; +}; + +export default addressController; diff --git a/_dev/js/theme/core/address/countryAddressChange.js b/_dev/js/theme/core/address/handler/changeAddressCountryHandler.js similarity index 70% rename from _dev/js/theme/core/address/countryAddressChange.js rename to _dev/js/theme/core/address/handler/changeAddressCountryHandler.js index 040468d3..aab53bda 100644 --- a/_dev/js/theme/core/address/countryAddressChange.js +++ b/_dev/js/theme/core/address/handler/changeAddressCountryHandler.js @@ -1,12 +1,15 @@ import prestashop from 'prestashop'; -import parseToHtml from '@js/theme/utils/parseToHtml'; -import useAlertToast from '@js/theme/components/useAlertToast'; -import useEvent from '@js/theme/components/event/useEvent'; +import useAlertToast from '../../../components/useAlertToast'; +import parseToHtml from '../../../utils/parseToHtml'; +import updateAddressRequest from '../request/updateAddressRequest'; const { danger } = useAlertToast(); -const { on } = useEvent(); -const handleAddressChange = async () => { +/** + * Change address country handler + * @returns {Promise} + */ +const changeAddressCountryHandler = async () => { const DOMSelectors = { addressFormWrapperSelector: '.js-address-form', countrySelectSelector: '.js-country', @@ -29,11 +32,18 @@ const handleAddressChange = async () => { formInputs, } = getDOMAddressElements(); const url = addressForm.dataset?.refreshUrl; - const idCountry = countrySelect.value; - const idAddress = addressForm.dataset?.idAddress; + const idCountry = Number.parseInt(countrySelect.value, 10); + const idAddress = Number.parseInt(addressForm.dataset?.idAddress, 10); + + const payload = { + id_address: idAddress, + id_country: idCountry, + }; + + const { getRequest } = updateAddressRequest(url, payload); try { - const data = await prestashop.frontAPI.updateAddress(url, idAddress, idCountry); + const data = await getRequest(); const inputsValue = []; @@ -70,8 +80,4 @@ const handleAddressChange = async () => { } }; -const countryAddressChange = () => { - on(document, 'change', '.js-country', handleAddressChange); -}; - -export default countryAddressChange; +export default changeAddressCountryHandler; diff --git a/_dev/js/theme/core/address/index.js b/_dev/js/theme/core/address/index.js index 78057205..0f8aaa4d 100644 --- a/_dev/js/theme/core/address/index.js +++ b/_dev/js/theme/core/address/index.js @@ -1,6 +1,8 @@ -import $ from 'jquery'; -import countryAddressChange from '@js/theme/core/address/countryAddressChange'; +import addressController from './addressController'; +import DOMReady from '../../utils/DOMReady'; -$(() => { - countryAddressChange(); +const { init } = addressController(); + +DOMReady(() => { + init(); }); diff --git a/_dev/js/theme/core/address/request/updateAddressRequest.js b/_dev/js/theme/core/address/request/updateAddressRequest.js new file mode 100644 index 00000000..4d1feb43 --- /dev/null +++ b/_dev/js/theme/core/address/request/updateAddressRequest.js @@ -0,0 +1,80 @@ +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 url {string} - new url with from-xhr param + * @param payload {object} - payload for request + * @param payload.id_country {number} - country id + * @param payload.id_address {number} - address id + * @example + * const url = 'address-form.com/url'; // url to update address form + * const payload = { + * id_address: 1, + * id_country: 1, + * } + * const { getRequest } = updateAddressRequest(url, payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const updateAddressRequest = (url, payload) => { + const { request, controller } = useHttpRequest(url); + + const payloadDefinition = { + id_country: { + type: 'int', + required: true, + }, + id_address: { + type: 'int', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payload, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => new Promise((resolve, reject) => { + abortAll(); + + dispatch(request, controller)(() => request + .query(payload) + .post() + .json((resp) => { + resolve(resp); + }) + .catch((e) => { + // IF ABORTED + if (e instanceof DOMException) { + return; + } + + reject(e); + })); + }); + + return { + getRequest, + }; +}; + +export default updateAddressRequest; diff --git a/_dev/js/theme/core/cart/cartController.js b/_dev/js/theme/core/cart/cartController.js new file mode 100644 index 00000000..5f59782e --- /dev/null +++ b/_dev/js/theme/core/cart/cartController.js @@ -0,0 +1,65 @@ +import prestashop from 'prestashop'; +import useEvent from '../../components/event/useEvent'; +import submitVoucherHandler from './handler/voucher/submitVoucherHandler'; +import codeLinkSubmitHandler from './handler/voucher/codeLinkSubmitHandler'; +import deleteVoucherHandler from './handler/voucher/deleteVoucherHandler'; +import addToCartHandler from './handler/cart/addToCartHandler'; +import deleteFromCartHandler from './handler/cart/deleteFromCartHandler'; +import quantityChangeHandler from './handler/cart/quantityChangeHandler'; +import updateCartHandler from './handler/cart/updateCartHandler'; +import updatedCartHandler from './handler/cart/updatedCartHandler'; +import useCustomQuantityInput from '../../components/useCustomQuantityInput'; + +const { on } = useEvent(); + +/** + * Cart controller + * @returns {object} return + * @returns {function} return.init initialize cart controller + */ +const cartController = () => { + const attachSpinnerEvents = () => { + const spinners = document.querySelectorAll('.js-custom-cart-qty-spinner'); + + spinners.forEach((spinner) => { + const { init, getDOMElements } = useCustomQuantityInput(spinner, { + onQuantityChange: ({ operation, qtyDifference }) => { + const { input } = getDOMElements(); + + quantityChangeHandler(operation, qtyDifference, input); + }, + }); + + init(); + }); + }; + + const init = () => { + const { + discountCode, + } = prestashop.selectors.cart; + + on(document, 'submit', '.js-voucher-form', submitVoucherHandler); + on(document, 'click', discountCode, codeLinkSubmitHandler); + on(document, 'click', '.js-voucher-delete', deleteVoucherHandler); + on(document, 'click', '[data-button-action="add-to-cart"]', addToCartHandler); + on(document, 'click', '.js-remove-from-cart', deleteFromCartHandler); + + attachSpinnerEvents(); + + prestashop.on('updatedCart', (event) => { + attachSpinnerEvents(); + updatedCartHandler(event); + }); + + prestashop.on('updateCart', (event) => { + updateCartHandler(event); + }); + }; + + return { + init, + }; +}; + +export default cartController; diff --git a/_dev/js/theme/core/cart/cartDelete.js b/_dev/js/theme/core/cart/cartDelete.js deleted file mode 100644 index ba57a6bb..00000000 --- a/_dev/js/theme/core/cart/cartDelete.js +++ /dev/null @@ -1,37 +0,0 @@ -import useAlertToast from '@js/theme/components/useAlertToast'; -import useEvent from '@js/theme/components/event/useEvent'; - -const { on } = useEvent(); -const { danger } = useAlertToast(); - -const handleCartDelete = async (event) => { - event.preventDefault(); - - const button = event.currentTarget; - const { dataset } = button; - const { idProduct, idProductAttribute, idCustomization = 0 } = dataset; - - try { - const resp = await prestashop.frontAPI.deleteFromCart(idProduct, idProductAttribute, idCustomization); - - if (!resp.hasError) { - prestashop.emit('updateCart', { - reason: dataset || resp, - resp, - }); - } else { - prestashop.emit('handleError', { - eventType: 'deleteProductFromCart', - resp, - }); - } - } catch (error) { - danger(error.message); - } -}; - -const cartDelete = () => { - on(document, 'click', '.js-remove-from-cart', handleCartDelete); -}; - -export default cartDelete; diff --git a/_dev/js/theme/core/cart/cartQuantity.js b/_dev/js/theme/core/cart/cartQuantity.js deleted file mode 100644 index d42a64cb..00000000 --- a/_dev/js/theme/core/cart/cartQuantity.js +++ /dev/null @@ -1,115 +0,0 @@ -import useCustomQuantityInput from '@js/theme/components/useCustomQuantityInput'; -import useAlertToast from '@js/theme/components/useAlertToast'; -import prestashop from 'prestashop'; - -prestashop.cart = prestashop.cart || {}; - -let errorMsg = ''; -let isUpdateOperation = false; -let hasError = false; - -const { danger } = useAlertToast(); - -const checkUpdateOperation = (resp) => { - const { hasError: hasErrorOccurred, errors: errorData } = resp; - hasError = hasErrorOccurred ?? false; - errorMsg = errorData ?? ''; - - isUpdateOperation = true; -}; - -const handleQuantityChange = async ({ operation, qtyDifference, input }) => { - const { dataset } = input; - const { productAttributeId, productId, customizationId } = dataset; - - const simpleOperation = operation === 'decrease' ? 'down' : 'up'; - - document.querySelector('body').classList.add('cart-loading'); - - try { - const resp = await prestashop.frontAPI.updateCartQuantity( - simpleOperation, - productId, - productAttributeId, - qtyDifference, - customizationId, - ); - - checkUpdateOperation(resp); - - if (!resp.hasError) { - prestashop.emit('updateCart', { - reason: dataset || resp, - resp, - }); - } else { - prestashop.emit('handleError', { - eventType: 'updateProductQuantityInCart', - resp, - }); - } - } catch (error) { - danger(error.message); - } - - document.querySelector('body').classList.remove('cart-loading'); -}; - -const switchErrorStat = () => { - const checkoutButtons = document.querySelectorAll(prestashop.selectors.checkout.btn); - - const toggleDisabledState = (disabled) => { - checkoutButtons.forEach((btn) => { - btn.disabled = disabled; - }); - }; - - if (document.querySelector(prestashop.selectors.notifications.dangerAlert) || (errorMsg !== '' && !hasError)) { - toggleDisabledState(true); - } - - if (errorMsg !== '') { - danger(errorMsg); - errorMsg = ''; - isUpdateOperation = false; - - if (hasError) { - // if hasError is true, quantity was not updated : allow checkout - toggleDisabledState(false); - } - } else if (!hasError && isUpdateOperation) { - hasError = false; - isUpdateOperation = false; - toggleDisabledState(false); - } -}; - -const createSpinner = () => { - const spinners = document.querySelectorAll('.js-custom-cart-qty-spinner'); - - spinners.forEach((spinner) => { - const { init, getDOMElements } = useCustomQuantityInput(spinner, { - onQuantityChange: ({ - operation, qtyDifference, - }) => { - const { input } = getDOMElements(); - - handleQuantityChange({ operation, qtyDifference, input }); - }, - }); - - init(); - }); - - switchErrorStat(); -}; - -const cartQuantity = () => { - prestashop.on('updatedCart', () => { - createSpinner(); - }); - - createSpinner(); -}; - -export default cartQuantity; diff --git a/_dev/js/theme/core/cart/cartVouchers.js b/_dev/js/theme/core/cart/cartVouchers.js deleted file mode 100644 index 91e244eb..00000000 --- a/_dev/js/theme/core/cart/cartVouchers.js +++ /dev/null @@ -1,97 +0,0 @@ -import useAlertToast from '@js/theme/components/useAlertToast'; -import parseToHtml from '@js/theme/utils/parseToHtml'; -import prestashop from 'prestashop'; -import useEvent from '@js/theme/components/event/useEvent'; - -const { on } = useEvent(); - -const { danger } = useAlertToast(); - -const formEventHandler = async (event) => { - event.preventDefault(); - - const addVoucherForm = event.currentTarget; - const btn = addVoucherForm.querySelector('[type="submit"]'); - const input = addVoucherForm.querySelector('[name="discount_name"]'); - const voucherName = input?.value || ''; - - btn.disabled = true; - - try { - const resp = await prestashop.frontAPI.addVoucherToCart(voucherName); - - if (!resp.hasError) { - prestashop.emit('updateCart', { - reason: event.target.dataset, - resp, - }); - } else { - const alert = document.querySelector('.js-voucher-error'); - const alertText = alert.querySelector('.js-voucher-error-text'); - - if (alert && alertText && resp.errors?.length) { - const errors = resp.errors.map((error) => `
${error}
`); - - alert.style.display = 'block'; - alertText.textContent = ''; - alertText.append(parseToHtml(errors.join(''))); - } - } - } catch (error) { - danger(error.message); - } - - btn.disabled = false; -}; - -const deleteHandler = async (event) => { - event.preventDefault(); - - const btn = event.currentTarget; - const { dataset } = btn; - const { idDiscount } = dataset; - - try { - const resp = await prestashop.frontAPI.deleteVoucherFromCart(idDiscount); - - if (!resp.hasError) { - prestashop.emit('updateCart', { - reason: dataset || resp, - resp, - }); - } else { - prestashop.emit('handleError', { - eventType: 'deleteVoucherFromCart', - resp, - }); - } - } catch (error) { - danger(error.message); - } -}; - -const linkEventHandler = (event) => { - event.preventDefault(); - - const link = event.currentTarget; - const input = document.querySelector('[name="discount_name"]'); - const form = input?.closest('.js-voucher-form'); - - if (input && form) { - const formEvent = new Event('submit', { - bubbles: true, - cancelable: true, - }); - - input.value = link.textContent; - form.dispatchEvent(formEvent); - } -}; - -const cartVouchers = () => { - on(document, 'submit', '.js-voucher-form', formEventHandler); - on(document, 'click', prestashop.selectors.cart.discountCode, linkEventHandler); - on(document, 'click', '.js-voucher-delete', deleteHandler); -}; - -export default cartVouchers; diff --git a/_dev/js/theme/core/cart/cartAddProduct.js b/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js similarity index 67% rename from _dev/js/theme/core/cart/cartAddProduct.js rename to _dev/js/theme/core/cart/handler/cart/addToCartHandler.js index 7bbca68e..76e5c98a 100644 --- a/_dev/js/theme/core/cart/cartAddProduct.js +++ b/_dev/js/theme/core/cart/handler/cart/addToCartHandler.js @@ -1,18 +1,20 @@ -import useAlertToast from '@js/theme/components/useAlertToast'; -import sprintf from '@js/theme/utils/sprintf'; -import useEvent from '@js/theme/components/event/useEvent'; +import useAlertToast from '../../../../components/useAlertToast'; +import addToCartRequest from '../../request/cart/addToCartRequest'; +import sprintf from '../../../../utils/sprintf'; -const { on } = useEvent(); const { danger } = useAlertToast(); -const eventHandler = async (event) => { +/** + * Handle add to cart event on form submit + * @param event {Event} - submit event + * @returns {Promise} + */ +const addToCartHandler = async (event) => { event.preventDefault(); const form = event.currentTarget?.form; const addToCartButton = event.currentTarget; - addToCartButton.setAttribute('disabled', true); - const isQuantityInputValid = (input) => { let validInput = true; @@ -25,9 +27,9 @@ const eventHandler = async (event) => { return validInput; }; - const idProduct = form.querySelector('[name=id_product]').value; + const idProduct = Number.parseInt(form.querySelector('[name=id_product]').value, 10); const quantityInput = form.querySelector('[name=qty]'); - const quantity = quantityInput?.value || 0; + const qty = Number.parseInt(quantityInput?.value, 10) || 0; const idProductAttribute = form.querySelector('[name=id_product_attribute]')?.value || 0; const idCustomization = form.querySelector('[name=id_customization]')?.value || 0; @@ -42,8 +44,19 @@ const eventHandler = async (event) => { return; } + const payload = { + id_product: idProduct, + qty, + id_product_attribute: idProductAttribute, + id_customization: idCustomization, + }; + + const { getRequest } = addToCartRequest(payload); + + addToCartButton.setAttribute('disabled', true); + try { - const resp = await prestashop.frontAPI.addToCart(idProduct, quantity, idProductAttribute, idCustomization); + const resp = await getRequest(); if (!resp.hasError) { prestashop.emit('updateCart', { @@ -71,8 +84,4 @@ const eventHandler = async (event) => { }, 1000); }; -const cartAddProduct = () => { - on(document, 'click', '[data-button-action="add-to-cart"]', eventHandler); -}; - -export default cartAddProduct; +export default addToCartHandler; diff --git a/_dev/js/theme/core/cart/handler/cart/deleteFromCartHandler.js b/_dev/js/theme/core/cart/handler/cart/deleteFromCartHandler.js new file mode 100644 index 00000000..9e1f5825 --- /dev/null +++ b/_dev/js/theme/core/cart/handler/cart/deleteFromCartHandler.js @@ -0,0 +1,45 @@ +import useAlertToast from '../../../../components/useAlertToast'; +import deleteFromCartRequest from '../../request/cart/deleteFromCartRequest'; + +const { danger } = useAlertToast(); + +/** + * Delete product from cart handler + * @param event {Event} - event object + * @returns {Promise} + */ +const deleteFromCartHandler = async (event) => { + event.preventDefault(); + + const button = event.currentTarget; + const { dataset } = button; + const { idProduct, idProductAttribute, idCustomization = 0 } = dataset; + + const payload = { + id_product: Number.parseInt(idProduct, 10), + id_product_attribute: Number.parseInt(idProductAttribute, 10), + id_customization: Number.parseInt(idCustomization, 10), + }; + + const { getRequest } = deleteFromCartRequest(payload); + + try { + const resp = await getRequest(); + + if (!resp.hasError) { + prestashop.emit('updateCart', { + reason: dataset || resp, + resp, + }); + } else { + prestashop.emit('handleError', { + eventType: 'deleteProductFromCart', + resp, + }); + } + } catch (error) { + danger(error.message); + } +}; + +export default deleteFromCartHandler; diff --git a/_dev/js/theme/core/cart/handler/cart/quantityChangeHandler.js b/_dev/js/theme/core/cart/handler/cart/quantityChangeHandler.js new file mode 100644 index 00000000..f03ccd90 --- /dev/null +++ b/_dev/js/theme/core/cart/handler/cart/quantityChangeHandler.js @@ -0,0 +1,52 @@ +import prestashop from 'prestashop'; +import quantityChangeRequest from '../../request/cart/quantityChangeRequest'; +import useAlertToast from '../../../../components/useAlertToast'; + +const { danger } = useAlertToast(); + +/** + * @param {string} operation - increase|decrease + * @param {number} qtyDifference - quantity difference + * @param {HTMLElement} input - input element + * @returns {Promise} + */ +const quantityChangeHandler = async (operation, qtyDifference, input) => { + const { dataset } = input; + const { productAttributeId, productId, customizationId } = dataset; + + const simpleOperation = operation === 'decrease' ? 'down' : 'up'; + + document.querySelector('body').classList.add('cart-loading'); + + const payload = { + qty: qtyDifference, + id_product: Number.parseInt(productId, 10), + id_product_attribute: Number.parseInt(productAttributeId, 10), + id_customization: Number.parseInt(customizationId, 10), + op: simpleOperation, + }; + + const { getRequest } = quantityChangeRequest(payload); + + try { + const resp = await getRequest(); + + if (!resp.hasError) { + prestashop.emit('updateCart', { + reason: dataset || resp, + resp, + }); + } else { + prestashop.emit('handleError', { + eventType: 'updateProductQuantityInCart', + resp, + }); + } + } catch (error) { + danger(error.message); + } + + document.querySelector('body').classList.remove('cart-loading'); +}; + +export default quantityChangeHandler; diff --git a/_dev/js/theme/core/cart/updateCart.js b/_dev/js/theme/core/cart/handler/cart/updateCartHandler.js similarity index 67% rename from _dev/js/theme/core/cart/updateCart.js rename to _dev/js/theme/core/cart/handler/cart/updateCartHandler.js index c41fc362..2468e1b2 100644 --- a/_dev/js/theme/core/cart/updateCart.js +++ b/_dev/js/theme/core/cart/handler/cart/updateCartHandler.js @@ -1,28 +1,22 @@ -import useAlertToast from '@js/theme/components/useAlertToast'; -import parseToHtml from '@js/theme/utils/parseToHtml'; import prestashop from 'prestashop'; +import parseToHtml from '../../../../utils/parseToHtml'; +import useAlertToast from '../../../../components/useAlertToast'; +import updateCartContentRequest from '../../request/cart/updateCartContentRequest'; const { danger } = useAlertToast(); -const handleUpdatedEvent = () => { - document.querySelector('body').classList.remove('cart-loading'); -}; - -const handleUpdateEvent = async (event) => { +/** + * Update cart handler - update cart content and emit updatedCart event + * @param {object} event - update cart event object + * @returns {Promise} + */ +const updateCartHandler = async (event) => { prestashop.cart = event.resp.cart; - - const quickViewModal = document.querySelector(prestashop.selectors.cart.quickview); - - // TO DO REMOVE JQUERY - $(quickViewModal).modal('hide'); document.querySelector('body').classList.add('cart-loading'); - - if (prestashop.page.page_name !== 'cart') { - return; - } + const { getRequest } = updateCartContentRequest(); try { - const resp = await prestashop.frontAPI.refreshCartPage(); + const resp = await getRequest(); document.querySelector(prestashop.selectors.cart.detailedTotals) ?.replaceWith(parseToHtml(resp.cart_detailed_totals)); @@ -49,9 +43,4 @@ const handleUpdateEvent = async (event) => { } }; -const updateCart = () => { - prestashop.on('updateCart', handleUpdateEvent); - prestashop.on('updatedCart', handleUpdatedEvent); -}; - -export default updateCart; +export default updateCartHandler; diff --git a/_dev/js/theme/core/cart/handler/cart/updatedCartHandler.js b/_dev/js/theme/core/cart/handler/cart/updatedCartHandler.js new file mode 100644 index 00000000..4f143b59 --- /dev/null +++ b/_dev/js/theme/core/cart/handler/cart/updatedCartHandler.js @@ -0,0 +1,8 @@ +/** + * Updated cart handler + */ +const updatedCartHandler = () => { + document.querySelector('body').classList.remove('cart-loading'); +}; + +export default updatedCartHandler; diff --git a/_dev/js/theme/core/cart/handler/voucher/codeLinkSubmitHandler.js b/_dev/js/theme/core/cart/handler/voucher/codeLinkSubmitHandler.js new file mode 100644 index 00000000..89d15bf0 --- /dev/null +++ b/_dev/js/theme/core/cart/handler/voucher/codeLinkSubmitHandler.js @@ -0,0 +1,24 @@ +/** + * Submit voucher code from link + * @param event {object} - click event + */ +const codeLinkSubmitHandler = (event) => { + event.preventDefault(); + + const link = event.currentTarget; + const input = document.querySelector('[name="discount_name"]'); + const form = document.querySelector('.js-voucher-form'); + const code = link.dataset?.code; + + if (input && form && code) { + const formEvent = new Event('submit', { + bubbles: true, + cancelable: true, + }); + + input.value = code; + form.dispatchEvent(formEvent); + } +}; + +export default codeLinkSubmitHandler; diff --git a/_dev/js/theme/core/cart/handler/voucher/deleteVoucherHandler.js b/_dev/js/theme/core/cart/handler/voucher/deleteVoucherHandler.js new file mode 100644 index 00000000..d245b31c --- /dev/null +++ b/_dev/js/theme/core/cart/handler/voucher/deleteVoucherHandler.js @@ -0,0 +1,42 @@ +import useAlertToast from '../../../../components/useAlertToast'; +import deleteVoucherFromCartRequest from '../../request/voucher/deleteVoucherFromCartRequest'; + +const { danger } = useAlertToast(); + +/** + * Delete voucher handler + * @param event {object} - click event + * @returns {Promise} + */ +const deleteVoucherHandler = async (event) => { + event.preventDefault(); + + const btn = event.currentTarget; + const { dataset } = btn; + const { idDiscount } = dataset; + const payload = { + deleteDiscount: Number.parseInt(idDiscount, 10), + }; + + const { getRequest } = deleteVoucherFromCartRequest(payload); + + try { + const resp = await getRequest(); + + if (!resp.hasError) { + prestashop.emit('updateCart', { + reason: dataset || resp, + resp, + }); + } else { + prestashop.emit('handleError', { + eventType: 'deleteVoucherFromCart', + resp, + }); + } + } catch (error) { + danger(error.message); + } +}; + +export default deleteVoucherHandler; diff --git a/_dev/js/theme/core/cart/handler/voucher/submitVoucherHandler.js b/_dev/js/theme/core/cart/handler/voucher/submitVoucherHandler.js new file mode 100644 index 00000000..72e8bbfa --- /dev/null +++ b/_dev/js/theme/core/cart/handler/voucher/submitVoucherHandler.js @@ -0,0 +1,55 @@ +import addVoucherToCartRequest from '../../request/voucher/addVoucherToCartRequest'; +import parseToHtml from '../../../../utils/parseToHtml'; +import useAlertToast from '../../../../components/useAlertToast'; + +const { danger } = useAlertToast(); + +/** + * Submit voucher handler + * @param event {object} - submit event + * @returns {Promise} + */ +const submitVoucherHandler = async (event) => { + event.preventDefault(); + + const addVoucherForm = event.currentTarget; + const btn = addVoucherForm.querySelector('[type="submit"]'); + const input = addVoucherForm.querySelector('[name="discount_name"]'); + const voucherName = input?.value || ''; + + const payload = { + discount_name: voucherName, + }; + + const { getRequest } = addVoucherToCartRequest(payload); + + btn.disabled = true; + + try { + const resp = await getRequest(); + + if (!resp.hasError) { + prestashop.emit('updateCart', { + reason: event.target, + resp, + }); + } else { + const alert = document.querySelector('.js-voucher-error'); + const alertText = alert.querySelector('.js-voucher-error-text'); + + if (alert && alertText && resp.errors?.length) { + const errors = resp.errors.map((error) => `
${error}
`); + + alert.style.display = 'block'; + alertText.textContent = ''; + alertText.append(parseToHtml(errors.join(''))); + } + } + } catch (error) { + danger(error.message); + } + + btn.disabled = false; +}; + +export default submitVoucherHandler; diff --git a/_dev/js/theme/core/cart/index.js b/_dev/js/theme/core/cart/index.js index b640e9a2..53486760 100644 --- a/_dev/js/theme/core/cart/index.js +++ b/_dev/js/theme/core/cart/index.js @@ -1,17 +1,11 @@ -import updateCart from '@js/theme/core/cart/updateCart'; -import cartAddProduct from '@js/theme/core/cart/cartAddProduct'; -import cartVouchers from '@js/theme/core/cart/cartVouchers'; -import cartQuantity from '@js/theme/core/cart/cartQuantity'; -import cartDelete from '@js/theme/core/cart/cartDelete'; -import DOMReady from '@js/theme/utils/DOMReady'; +import prestashop from 'prestashop'; +import DOMReady from '../../utils/DOMReady'; +import cartController from './cartController'; -DOMReady(() => { - updateCart(); - cartQuantity(); - cartDelete(); -}); +prestashop.cart = prestashop.cart || {}; -$(() => { - cartAddProduct(); - cartVouchers(); +const { init } = cartController(); + +DOMReady(() => { + init(); }); diff --git a/_dev/js/theme/core/cart/request/cart/addToCartRequest.js b/_dev/js/theme/core/cart/request/cart/addToCartRequest.js new file mode 100644 index 00000000..0f10b4b7 --- /dev/null +++ b/_dev/js/theme/core/cart/request/cart/addToCartRequest.js @@ -0,0 +1,104 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string|string[]} errors - the errors returned by the server + * @property {number} id_product - product id + * @property {number} id_product_attribute - product attribute id + * @property {number} id_customization - product customization id + * @property {number} quantity - product quantity + * @property {boolean} success - success flag + * @property {object} cart - cart front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.id_product {number} - product id - Required + * @param payload.qty {number} - product quantity - Required + * @param payload.id_product_attribute {number} - product id attribute - optional pass 0 if not set + * @param payload.id_customization {number} - customization id - optional pass 0 if not set + * @param payload.add {number} - optional + * @param payload.action {string} - optional + * @param payload.token {string} - optional + * @param payload.ajax {number} - optional + * @example + * const payload = { + * id_product: 1, // Required + * qty: 1, // Required + * id_product_attribute: 2, // optional + * id_customization: 3, // optional + * }; + * + * const { getRequest } = addToCartRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const addToCartRequest = (payload) => { + const payloadToSend = { + add: 1, + action: 'update', + ajax: 1, + token: prestashop.static_token, + ...payload, + }; + + const payloadDefinition = { + id_product: { + type: 'int', + required: true, + }, + qty: { + type: 'int', + required: true, + }, + id_product_attribute: { + type: 'int', + required: false, + }, + id_customization: { + type: 'int', + required: false, + }, + add: { + type: 'int', + required: true, + }, + action: { + type: 'string', + required: true, + }, + ajax: { + type: 'int', + required: true, + }, + token: { + type: 'string', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default addToCartRequest; diff --git a/_dev/js/theme/core/cart/request/cart/deleteFromCartRequest.js b/_dev/js/theme/core/cart/request/cart/deleteFromCartRequest.js new file mode 100644 index 00000000..66661b5d --- /dev/null +++ b/_dev/js/theme/core/cart/request/cart/deleteFromCartRequest.js @@ -0,0 +1,98 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string|string[]} errors - the errors returned by the server + * @property {number} id_product - product id + * @property {number} id_product_attribute - product attribute id + * @property {number} id_customization - product customization id + * @property {number} quantity - product quantity + * @property {boolean} success - success flag + * @property {object} cart - cart front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.id_product {number} - product id - Required + * @param payload.id_product_attribute {number} - product id attribute - optional pass 0 if not set + * @param payload.id_customization {number} - customization id - optional pass 0 if not set + * @param payload.delete {number} - optional + * @param payload.action {string} - optional + * @param payload.token {string} - optional + * @param payload.ajax {number} - optional + * @example + * const payload = { + * id_product: 1, // Required + * id_product_attribute: 2, // optional + * id_customization: 3, // optional + * }; + * + * const { getRequest } = removeFromCartRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const deleteFromCartRequest = (payload) => { + const payloadToSend = { + delete: 1, + action: 'update', + ajax: 1, + token: prestashop.static_token, + ...payload, + }; + + const payloadDefinition = { + id_product: { + type: 'int', + required: true, + }, + id_product_attribute: { + type: 'int', + required: false, + }, + id_customization: { + type: 'int', + required: false, + }, + delete: { + type: 'int', + required: true, + }, + action: { + type: 'string', + required: true, + }, + ajax: { + type: 'int', + required: true, + }, + token: { + type: 'string', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default deleteFromCartRequest; diff --git a/_dev/js/theme/core/cart/request/cart/quantityChangeRequest.js b/_dev/js/theme/core/cart/request/cart/quantityChangeRequest.js new file mode 100644 index 00000000..21168042 --- /dev/null +++ b/_dev/js/theme/core/cart/request/cart/quantityChangeRequest.js @@ -0,0 +1,109 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string|string[]} errors - the errors returned by the server + * @property {number} id_product - product id + * @property {number} id_product_attribute - product attribute id + * @property {number} id_customization - product customization id + * @property {number} quantity - product quantity + * @property {boolean} success - success flag + * @property {object} cart - cart front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.id_product {number} - product id - Required + * @param payload.qty {number} - product quantity - Required + * @param payload.id_product_attribute {number} - product id attribute - optional pass 0 if not set + * @param payload.id_customization {number} - customization id - optional pass 0 if not set + * @param payload.add {number} - optional + * @param payload.action {string} - optional + * @param payload.token {string} - optional + * @param payload.ajax {number} - optional + * @example + * const payload = { + * id_product: 1, // Required + * qty: 1, // Required + * op: 'up', // up|down Required + * id_product_attribute: 2, // optional + * id_customization: 3, // optional + * }; + * + * const { getRequest } = quantityChangeRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const quantityChangeRequest = (payload) => { + const payloadToSend = { + update: 1, + action: 'update', + ajax: 1, + token: prestashop.static_token, + ...payload, + }; + + const payloadDefinition = { + id_product: { + type: 'int', + required: true, + }, + qty: { + type: 'int', + required: true, + }, + id_product_attribute: { + type: 'int', + required: false, + }, + id_customization: { + type: 'int', + required: false, + }, + update: { + type: 'int', + required: true, + }, + action: { + type: 'string', + required: true, + }, + ajax: { + type: 'int', + required: true, + }, + op: { + type: 'string', + required: true, + }, + token: { + type: 'string', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default quantityChangeRequest; diff --git a/_dev/js/theme/core/cart/request/cart/updateCartContentRequest.js b/_dev/js/theme/core/cart/request/cart/updateCartContentRequest.js new file mode 100644 index 00000000..a2ad938f --- /dev/null +++ b/_dev/js/theme/core/cart/request/cart/updateCartContentRequest.js @@ -0,0 +1,42 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +/** + * @typedef ServerResponse + * @type {object} + * @property {string} cart_detailed - cart detailed html used in cart page + * @property {string} cart_detailed_actions - cart actions html used in cart page + * @property {string} cart_detailed_totals - cart totals html used in cart page + * @property {string} cart_summary_items_subtotal - cart summary items subtotal html used in checkout page + * @property {string} cart_summary_products - cart summary products html used in checkout page + * @property {string} cart_summary_subtotals_container - cart summary products html used in checkout page + * @property {string} cart_summary_top - cart summary top html used in checkout page + * @property {string} cart_summary_totals - cart summary totals html used in checkout page + * @property {string} cart_voucher - cart voucher html used in checkout page + */ + +/** + * Update cart content request + * @example + * const { getRequest } = updateCartContentRequest(); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const updateCartContentRequest = () => { + const payloadToSend = { + action: 'refresh', + ajax: 1, + }; + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default updateCartContentRequest; diff --git a/_dev/js/theme/core/cart/request/voucher/addVoucherToCartRequest.js b/_dev/js/theme/core/cart/request/voucher/addVoucherToCartRequest.js new file mode 100644 index 00000000..a51ea30f --- /dev/null +++ b/_dev/js/theme/core/cart/request/voucher/addVoucherToCartRequest.js @@ -0,0 +1,86 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string|string[]} errors - the errors returned by the server + * @property {number} id_customization - always 0 + * @property {number} id_product - always 0 + * @property {number} id_product_attribute - always 0 + * @property {number} quantity - always 0 + * @property {boolean} success - success flag + * @property {object} cart - cart front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.discount_name {string} - discount code - Required + * @param payload.addDiscount {number} - optional + * @param payload.action {string} - optional + * @param payload.token {string} - optional + * @param payload.ajax {number} - optional + * @example + * const payload = { + * discount_name: 'voucherName', // Required + * }; + * + * const { getRequest } = addVoucherToCartRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const addVoucherToCartRequest = (payload) => { + const payloadToSend = { + addDiscount: 1, + action: 'update', + token: prestashop.static_token, + ajax: 1, + ...payload, + }; + + const payloadDefinition = { + addDiscount: { + type: 'int', + required: true, + }, + action: { + type: 'string', + required: true, + }, + token: { + type: 'string', + required: true, + }, + ajax: { + type: 'int', + required: true, + }, + discount_name: { + type: 'string', + required: true, + }, + }; + + const { validatePayload } = useHttpPayloadDefinition(payloadToSend, payloadDefinition); + + const validationErrors = validatePayload(); + + if (validationErrors.length) { + throw Error(validationErrors.join(',\n')); + } + + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default addVoucherToCartRequest; diff --git a/_dev/js/theme/core/cart/request/voucher/deleteVoucherFromCartRequest.js b/_dev/js/theme/core/cart/request/voucher/deleteVoucherFromCartRequest.js new file mode 100644 index 00000000..85d57443 --- /dev/null +++ b/_dev/js/theme/core/cart/request/voucher/deleteVoucherFromCartRequest.js @@ -0,0 +1,80 @@ +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../../components/http/useDefaultHttpRequest'; +import useHttpPayloadDefinition from '../../../../components/http/useHttpPayloadDefinition'; + +/** + * @typedef ServerResponse + * @type {object} + * @property {string|string[]} errors - the errors returned by the server + * @property {number} id_customization - always 0 + * @property {number} id_product - always 0 + * @property {number} id_product_attribute - always 0 + * @property {number} quantity - always 0 + * @property {boolean} success - success flag + * @property {object} cart - cart front object + */ + +/** + * Add voucher to cart request + * @param payload {Object} - payload object to send + * @param payload.deleteDiscount {number} - discount code id - Required + * @param payload.action {string} - optional + * @param payload.token {string} - optional + * @param payload.ajax {number} - optional + * @example + * const payload = { + * deleteDiscount: 2, // required + * }; + * + * const { getRequest } = deleteVoucherFromCartRequest(payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const deleteVoucherFromCartRequest = (payload) => { + const payloadToSend = { + action: 'update', + token: prestashop.static_token, + ajax: 1, + ...payload, + }; + + const payloadDefinition = { + deleteDiscount: { + type: 'int', + required: true, + }, + action: { + type: 'string', + required: true, + }, + token: { + type: 'string', + required: true, + }, + ajax: { + 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.cart, payloadToSend); + + return { + getRequest, + }; +}; + +export default deleteVoucherFromCartRequest; diff --git a/_dev/js/theme/core/checkout/checkoutController.js b/_dev/js/theme/core/checkout/checkoutController.js index d8465cef..f1f44012 100644 --- a/_dev/js/theme/core/checkout/checkoutController.js +++ b/_dev/js/theme/core/checkout/checkoutController.js @@ -1,19 +1,49 @@ import prestashop from 'prestashop'; import useEvent from '../../components/event/useEvent'; -import editAddressHandler from './handler/editAddressHandler'; -import changeAddressHandler from './handler/changeAddressHandler'; +import editAddressHandler from './handler/address/editAddressHandler'; +import changeAddressHandler from './handler/address/changeAddressHandler'; +import changeDeliveryMethodHandler from './handler/delivery/changeDeliveryMethodHandler'; +import editDeliveryStepHandler from './handler/delivery/editDeliveryStepHandler'; +import showAddressErrorMessageHandler from './handler/address/showAddressErrorMessageHandler'; +import orderConfirmationErrorsHandler from './handler/payment/orderConfirmationErrorsHandler'; +import toggleOrderButtonStateHandler from './handler/payment/toggleOrderButtonStateHandler'; +import togglePaymentOptionsHandler from './handler/payment/togglePaymentOptionsHandler'; +import confirmOrderHandler from './handler/payment/confirmOrderHandler'; +import checkoutFormSubmitHandler from './handler/misc/checkoutFormSubmitHandler'; +import checkoutStepChangeHandler from './handler/misc/checkoutStepChangeHandler'; const { on } = useEvent(); +/** + * Checkout controller + * @returns {object} return + * @returns {function} return.init initialize checkout controller + */ const checkoutController = () => { const { editAddresses, deliveryAddressRadios, + deliveryFormSelector, + editDeliveryButtonSelector, + conditionsSelector, + confirmationSelector, } = prestashop.selectors.checkout; const init = () => { on(document, 'click', editAddresses, editAddressHandler); on(document, 'click', deliveryAddressRadios, changeAddressHandler); + on(document, 'change', `${deliveryFormSelector} input[type="radio"]`, changeDeliveryMethodHandler); + on(document, 'click', editDeliveryButtonSelector, editDeliveryStepHandler); + on(document, 'change', `${conditionsSelector} input[type="checkbox"]`, toggleOrderButtonStateHandler); + on(document, 'change', 'input[name="payment-option"]', togglePaymentOptionsHandler); + on(document, 'click', `${confirmationSelector} button`, confirmOrderHandler); + on(document, 'submit', prestashop.selectors.checkout.form, checkoutFormSubmitHandler); + on(document, 'click', prestashop.selectors.checkout.stepEdit, checkoutStepChangeHandler); + + prestashop.on('orderConfirmationErrors', orderConfirmationErrorsHandler); + + showAddressErrorMessageHandler(); + toggleOrderButtonStateHandler(); }; return { diff --git a/_dev/js/theme/core/checkout/checkoutDelivery.js b/_dev/js/theme/core/checkout/checkoutDelivery.js deleted file mode 100644 index 1f77fb2c..00000000 --- a/_dev/js/theme/core/checkout/checkoutDelivery.js +++ /dev/null @@ -1,85 +0,0 @@ -import prestashop from 'prestashop'; -import selectDeliveryMethodRequest from '@js/theme/core/checkout/request/selectDeliveryMethodRequest'; -import useAlertToast from '@js/theme/components/useAlertToast'; -import parseToHtml from '@js/theme/utils/parseToHtml'; -import useEvent from '@js/theme/components/event/useEvent'; -import { fromSerializeObject, formSerializeArray } from '../../utils/formSerialize'; - -const { on } = useEvent(); -const { danger } = useAlertToast(); - -const refreshCheckoutPage = () => { - const urlParams = new URLSearchParams(window.location.search); - - if (urlParams.has('updatedTransaction')) { - window.location.reload(); - - return; - } - - urlParams.append('updatedTransaction', 1); - window.location.href = `${window.location.pathname}?${urlParams.toString()}`; -}; - -const updateDeliveryForm = async (event) => { - const { - deliveryFormSelector, summarySelector, deliveryOption, cartPaymentStepRefresh, - } = prestashop.selectors.checkout; - - const deliveryMethodForm = document.querySelector(deliveryFormSelector); - - if (deliveryMethodForm === null) { - return; - } - - const requestData = fromSerializeObject(deliveryMethodForm); - const checkedInput = event.target; - const newDeliveryOption = checkedInput.closest(deliveryOption); - const url = newDeliveryOption.dataset.urlUpdate; - const { getRequest } = selectDeliveryMethodRequest(url, requestData); - - try { - const resp = await getRequest(); - - document.querySelectorAll(summarySelector).forEach((element) => { - element.replaceWith(parseToHtml(resp.preview)); - }); - - if (document.querySelector(cartPaymentStepRefresh) !== null) { - // we get the refresh flag : on payment step we need to refresh page to be sure - // amount is correctly updated on payment modules - refreshCheckoutPage(); - } - - prestashop.emit('updatedDeliveryForm', { - dataForm: formSerializeArray(deliveryMethodForm), - deliveryOption: newDeliveryOption, - resp, - }); - } catch (error) { - danger(error.message); - prestashop.emit('handleError', { - eventType: 'updateDeliveryOptions', - resp: {}, - }); - } -}; - -const addEvents = () => { - const { - deliveryFormSelector, deliveryStepSelector, editDeliveryButtonSelector, stepEdit, - } = prestashop.selectors.checkout; - - on(document, 'change', `${deliveryFormSelector} input`, updateDeliveryForm); - on(document, 'click', editDeliveryButtonSelector, (event) => { - event.stopPropagation(); - document.querySelector(`${deliveryStepSelector} ${stepEdit}`)?.click(); - prestashop.emit('editDelivery'); - }); -}; - -const checkoutDelivery = () => { - addEvents(); -}; - -export default checkoutDelivery; diff --git a/_dev/js/theme/core/checkout/checkoutPayment.js b/_dev/js/theme/core/checkout/checkoutPayment.js deleted file mode 100644 index 56a5b73d..00000000 --- a/_dev/js/theme/core/checkout/checkoutPayment.js +++ /dev/null @@ -1,189 +0,0 @@ -import checkCartStillOrderableRequest from '@js/theme/core/checkout/request/checkCartStillOrderableRequest'; -import useEvent from '@js/theme/components/event/useEvent'; -import prestashop from 'prestashop'; - -const { on } = useEvent(); - -const collapsePaymentOptions = () => { - const { additionalInformatonSelector, optionsForm } = prestashop.selectors.checkout; - const paymentRelatedBlocks = document.querySelectorAll(`${additionalInformatonSelector}, ${optionsForm}`); - - paymentRelatedBlocks.forEach((paymentRelatedBlock) => { - paymentRelatedBlock.classList.add('d-none'); - }); -}; - -const getSelectedOptionId = () => { - const selectedOption = document.querySelector('input[name="payment-option"]:checked'); - - return selectedOption?.id ? selectedOption.id : null; -}; - -const getPaymentOptionSelector = (option) => { - const paymentOption = document.querySelector(`#${option}`); - const { moduleName } = paymentOption.dataset; - - return `.js-payment-${moduleName}`; -}; - -const toggleConfirmation = (show = true) => { - const { confirmationSelector } = prestashop.selectors.checkout; - - const confirmation = document.querySelector(confirmationSelector); - - if (show) { - confirmation?.classList.remove('d-none'); - } else { - confirmation?.classList.add('d-none'); - } -}; - -const haveTermsBeenAccepted = () => { - const termsCheckbox = document.querySelectorAll(prestashop.selectors.checkout.termsCheckboxSelector); - let checked = true; - - termsCheckbox.forEach((checkbox) => { - if (!checkbox.checked) { - checked = false; - } - }); - - return checked; -}; - -const showNativeFormErrors = () => { - const { termsCheckboxSelector } = prestashop.selectors.checkout; - const formElements = document.querySelectorAll(`input[name=payment-option], ${termsCheckboxSelector}`); - - formElements.forEach((formElement) => { - formElement.reportValidity(); - }); -}; - -const handleOrderConfirmationErrors = ({ resp }) => { - if (resp.cartUrl !== '') { - window.location.href = resp.cartUrl; - } -}; - -const areConditionsAccepted = () => { - let accepted = true; - const { conditionsSelector } = prestashop.selectors.checkout; - const conditions = document.querySelectorAll(`${conditionsSelector} input[type="checkbox"]`); - - conditions.forEach((condition) => { - if (!condition.checked) { - accepted = false; - } - }); - - return accepted; -}; - -const toggleOrderButton = () => { - const { confirmationSelector, paymentBinary, conditionAlertSelector } = prestashop.selectors.checkout; - - let paymentBtnEnabled = areConditionsAccepted(); - - prestashop.emit('termsUpdated', { - isChecked: paymentBtnEnabled, - }); - - collapsePaymentOptions(); - - const selectedOptionID = getSelectedOptionId(); - - if (!selectedOptionID) { - paymentBtnEnabled = false; - } - - document.querySelectorAll(`#${selectedOptionID}-additional-information, #pay-with-${selectedOptionID}-form`) - .forEach((element) => { - element.classList.remove('d-none'); - }); - - document.querySelectorAll(paymentBinary) - .forEach((element) => { - element.classList.add('d-none'); - }); - - if (document.querySelector(`#${selectedOptionID}`)?.classList.contains('binary')) { - const paymentOptionSelector = getPaymentOptionSelector(selectedOptionID); - toggleConfirmation(false); - - document.querySelectorAll(paymentOptionSelector) - .forEach((element) => { - element.classList.remove('d-none'); - }); - - document.querySelectorAll(`${paymentOptionSelector} button, ${paymentOptionSelector} input`).forEach((element) => { - if (paymentBtnEnabled) { - element.removeAttribute('disabled'); - } else { - element.setAttribute('disabled', 'disabled'); - } - }); - } else { - toggleConfirmation(true); - - document.querySelectorAll(`${confirmationSelector} button`).forEach((element) => { - element.classList.toggle('disabled', !paymentBtnEnabled); - }); - - document.querySelectorAll(conditionAlertSelector).forEach((element) => { - element.classList.toggle('d-none', paymentBtnEnabled); - }); - } -}; - -const confirm = async () => { - const { confirmationSelector } = prestashop.selectors.checkout; - const option = getSelectedOptionId(); - const termsAccepted = haveTermsBeenAccepted(); - - if (option === undefined || termsAccepted === false) { - showNativeFormErrors(); - return; - } - - // We ask cart controller, if everything in the cart is still orderable - const { getRequest } = checkCartStillOrderableRequest(window.prestashop.urls.pages.order); - - const resp = await getRequest(); - - // We process the information and allow other modules to intercept this - const isRedirected = prestashop.checkout.onCheckOrderableCartResponse(resp); - - // If there is a redirect, we deny the form submit below, to allow the redirect to complete - if (isRedirected) return; - - document.querySelectorAll(`${confirmationSelector} button`).forEach((element) => { - element.classList.add('disabled'); - }); - - document.querySelector(`#pay-with-${option}-form form`)?.submit(); -}; - -const init = () => { - prestashop.on('orderConfirmationErrors', handleOrderConfirmationErrors); - - const { conditionsSelector, confirmationSelector } = prestashop.selectors.checkout; - - on(document, 'change', `${conditionsSelector} input[type="checkbox"]`, toggleOrderButton); - on(document, 'change', 'input[name="payment-option"]', toggleOrderButton); - on(document, 'click', `${confirmationSelector} button`, confirm); - - // call toggle once on init to handle situation where everything - // is already ok (like 0 price order, payment already preselected and so on) - toggleOrderButton(); - - if (!getSelectedOptionId()) { - collapsePaymentOptions(); - } -}; - -const checkoutPayment = () => { - init(); -}; - -export default checkoutPayment; diff --git a/_dev/js/theme/core/checkout/useCheckoutStepsController.js b/_dev/js/theme/core/checkout/components/useCheckoutStepsController.js similarity index 78% rename from _dev/js/theme/core/checkout/useCheckoutStepsController.js rename to _dev/js/theme/core/checkout/components/useCheckoutStepsController.js index 2fbb816a..5d5271ad 100644 --- a/_dev/js/theme/core/checkout/useCheckoutStepsController.js +++ b/_dev/js/theme/core/checkout/components/useCheckoutStepsController.js @@ -1,6 +1,6 @@ import prestashop from 'prestashop'; -import { getAllSiblingsBeforeElement, getAllSiblingsAfterElement } from '@js/theme/utils/DOMSelectorsHelper'; +import { getAllSiblingsBeforeElement, getAllSiblingsAfterElement } from '../../../utils/DOMSelectorsHelper'; const useCheckoutStepsController = (stepsSelector = prestashop.selectors.checkout.step) => { const DOMClasses = { @@ -29,7 +29,7 @@ const useCheckoutStepsController = (stepsSelector = prestashop.selectors.checkou for (const nextStep of nextSteps) { nextStep.classList.add(DOMClasses.STEP_UNREACHABLE); - nextStep.classList.remove(DOMClasses.STEP_COMPLETE); + nextStep.classList.remove(DOMClasses.STEP_COMPLETE, DOMClasses.STEP_REACHABLE); nextStep.querySelector(prestashop.selectors.checkout.stepTitle).classList.add('not-allowed'); } }; @@ -44,26 +44,25 @@ const useCheckoutStepsController = (stepsSelector = prestashop.selectors.checkou } }; - const handleStepClick = (event) => { - const clickedStep = event.target.closest(stepsSelector); - - if (!clickedStep) { + const changeStep = (step) => { + if (!step) { return; } - if (!isStepUnreachable(clickedStep) && !isStepCurrent(clickedStep)) { - setCurrentStep(clickedStep); + if (!isStepUnreachable(step) && !isStepCurrent(step)) { + setCurrentStep(step); - if (hasStepContinueButton(clickedStep)) { - disableAllAfter(clickedStep); + if (hasStepContinueButton(step)) { + disableAllAfter(step); } else { - enableAllBefore(clickedStep); + enableAllBefore(step); } } }; return { - handleStepClick, + changeStep, + stepsSelector, }; }; diff --git a/_dev/js/theme/core/checkout/handler/changeAddressHandler.js b/_dev/js/theme/core/checkout/handler/address/changeAddressHandler.js similarity index 65% rename from _dev/js/theme/core/checkout/handler/changeAddressHandler.js rename to _dev/js/theme/core/checkout/handler/address/changeAddressHandler.js index dc9dff6e..c61260f1 100644 --- a/_dev/js/theme/core/checkout/handler/changeAddressHandler.js +++ b/_dev/js/theme/core/checkout/handler/address/changeAddressHandler.js @@ -1,11 +1,15 @@ import prestashop from 'prestashop'; -import { isElementVisible } from '@js/theme/utils/DOMHelpers'; -import useToggleDisplay from '@js/theme/components/display/useToggleDisplay'; -import switchEditAddressButtonColor from '@js/theme/core/checkout/utils/switchEditAddressButtonColor'; -import switchConfirmAddressesButtonState from '@js/theme/core/checkout/utils/switchConfirmAddressesButtonState'; -import getEditAddress from '@js/theme/core/checkout/utils/getEditAddress'; - -const changeAddressHandler = (e) => { +import useToggleDisplay from '../../../../components/display/useToggleDisplay'; +import switchEditAddressButtonColor from '../../utils/switchEditAddressButtonColor'; +import getEditAddress from '../../utils/getEditAddress'; +import switchConfirmAddressesButtonState from '../../utils/switchConfirmAddressesButtonState'; +import { isElementVisible, each } from '../../../../utils/DOMHelpers'; + +/** + * Change address handler + * @param event {object} - change event + */ +const changeAddressHandler = (event) => { const { addressItem, addressItemChecked, @@ -13,14 +17,14 @@ const changeAddressHandler = (e) => { notValidAddresses, } = prestashop.selectors.checkout; - document.querySelectorAll(addressItem).forEach((element) => { + each(addressItem, (element) => { element.classList.remove('selected'); }); - document.querySelectorAll(addressItemChecked).forEach((element) => { + each(addressItemChecked, (element) => { element.classList.add('selected'); }); - const eventTarget = e.currentTarget; + const eventTarget = event.currentTarget; const addressErrorElement = document.querySelector(addressError); const idFailureAddress = addressErrorElement ? addressErrorElement?.id.split('-').pop() : null; const notValidAddressesVal = document.querySelector(notValidAddresses)?.value; @@ -31,14 +35,14 @@ const changeAddressHandler = (e) => { switchEditAddressButtonColor(false, idFailureAddress, addressType); if (notValidAddressesVal !== '' && getEditAddress() === null && notValidAddressesVal.split(',').indexOf(eventTarget.value) >= 0) { - addressErrorElements.forEach(show); + each(addressErrorElements, show); switchEditAddressButtonColor(true, eventTarget.value, addressType); if (addressErrorElement) { addressErrorElement.id = `id-failure-address-${eventTarget.value}`; } } else { - addressErrorElements.forEach(hide); + each(addressErrorElements, hide); } const allAddressErrors = document.querySelectorAll(addressError); diff --git a/_dev/js/theme/core/checkout/handler/editAddressHandler.js b/_dev/js/theme/core/checkout/handler/address/editAddressHandler.js similarity index 82% rename from _dev/js/theme/core/checkout/handler/editAddressHandler.js rename to _dev/js/theme/core/checkout/handler/address/editAddressHandler.js index 05f326d9..8e219f5c 100644 --- a/_dev/js/theme/core/checkout/handler/editAddressHandler.js +++ b/_dev/js/theme/core/checkout/handler/address/editAddressHandler.js @@ -1,5 +1,9 @@ import prestashop from 'prestashop'; +/** + * Edit address handler + * @param event {object} - click event + */ const editAddressHandler = (event) => { const { addressesStep, diff --git a/_dev/js/theme/core/checkout/checkoutAddress.js b/_dev/js/theme/core/checkout/handler/address/showAddressErrorMessageHandler.js similarity index 64% rename from _dev/js/theme/core/checkout/checkoutAddress.js rename to _dev/js/theme/core/checkout/handler/address/showAddressErrorMessageHandler.js index d029781b..ab34a2ac 100644 --- a/_dev/js/theme/core/checkout/checkoutAddress.js +++ b/_dev/js/theme/core/checkout/handler/address/showAddressErrorMessageHandler.js @@ -1,11 +1,15 @@ import prestashop from 'prestashop'; -import useToggleDisplay from '@js/theme/components/display/useToggleDisplay'; -import switchEditAddressButtonColor from '@js/theme/core/checkout/utils/switchEditAddressButtonColor'; -import switchConfirmAddressesButtonState from '@js/theme/core/checkout/utils/switchConfirmAddressesButtonState'; -import getEditAddress from '@js/theme/core/checkout/utils/getEditAddress'; -import { isElementVisible } from '@js/theme/utils/DOMHelpers'; +import useToggleDisplay from '../../../../components/display/useToggleDisplay'; +import switchConfirmAddressesButtonState from '../../utils/switchConfirmAddressesButtonState'; +import switchEditAddressButtonColor from '../../utils/switchEditAddressButtonColor'; +import { isElementVisible } from '../../../../utils/DOMHelpers'; +import getEditAddress from '../../utils/getEditAddress'; -const handleOnLoad = () => { +/** + * Show address error message handler + * @returns {void} + */ +const showAddressErrorMessageHandler = () => { const { addressForm, addressError } = prestashop.selectors.checkout; const getAllAddressErrors = () => document.querySelectorAll(addressError); const getVisibleAddressErrors = () => Array.from(getAllAddressErrors()).filter(isElementVisible); @@ -36,8 +40,4 @@ const handleOnLoad = () => { switchConfirmAddressesButtonState(getVisibleAddressErrors().length <= 0); }; -const checkoutAddress = () => { - handleOnLoad(); -}; - -export default checkoutAddress; +export default showAddressErrorMessageHandler; diff --git a/_dev/js/theme/core/checkout/handler/delivery/changeDeliveryMethodHandler.js b/_dev/js/theme/core/checkout/handler/delivery/changeDeliveryMethodHandler.js new file mode 100644 index 00000000..8913b917 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/delivery/changeDeliveryMethodHandler.js @@ -0,0 +1,60 @@ +import { formSerializeArray, fromSerializeObject } from '../../../../utils/formSerialize'; +import selectDeliveryMethodRequest from '../../request/selectDeliveryMethodRequest'; +import parseToHtml from '../../../../utils/parseToHtml'; +import useAlertToast from '../../../../components/useAlertToast'; +import refreshCheckoutPage from '../../utils/refreshCheckoutPage'; +import { each } from '../../../../utils/DOMHelpers'; + +const { danger } = useAlertToast(); + +/** + * Change delivery method handler + * @param event {object} - change event + * @returns {Promise} + */ +const changeDeliveryMethodHandler = async (event) => { + const { + deliveryFormSelector, summarySelector, deliveryOption, cartPaymentStepRefresh, + } = prestashop.selectors.checkout; + + const deliveryMethodForm = document.querySelector(deliveryFormSelector); + + if (deliveryMethodForm === null) { + return; + } + + const payload = fromSerializeObject(deliveryMethodForm); + const checkedInput = event.target; + const newDeliveryOption = checkedInput.closest(deliveryOption); + const url = newDeliveryOption.dataset.urlUpdate; + + const { getRequest } = selectDeliveryMethodRequest(url, payload); + + try { + const resp = await getRequest(); + + each(summarySelector, (element) => { + element.replaceWith(parseToHtml(resp.preview)); + }); + + if (document.querySelector(cartPaymentStepRefresh) !== null) { + // we get the refresh flag : on payment step we need to refresh page to be sure + // amount is correctly updated on payment modules + refreshCheckoutPage(); + } + + prestashop.emit('updatedDeliveryForm', { + dataForm: formSerializeArray(deliveryMethodForm), + deliveryOption: newDeliveryOption, + resp, + }); + } catch (error) { + danger(error.message); + prestashop.emit('handleError', { + eventType: 'updateDeliveryOptions', + resp: {}, + }); + } +}; + +export default changeDeliveryMethodHandler; diff --git a/_dev/js/theme/core/checkout/handler/delivery/editDeliveryStepHandler.js b/_dev/js/theme/core/checkout/handler/delivery/editDeliveryStepHandler.js new file mode 100644 index 00000000..f1ffe1f3 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/delivery/editDeliveryStepHandler.js @@ -0,0 +1,18 @@ +import prestashop from 'prestashop'; +import useCheckoutStepsController from '../../components/useCheckoutStepsController'; + +/** + * Edit delivery step handler + * @param event {object} - click event + */ +const editDeliveryStepHandler = (event) => { + event.preventDefault(); + event.stopPropagation(); + const { changeStep } = useCheckoutStepsController(); + const deliveryStep = document.querySelector('#checkout-delivery-step'); + + changeStep(deliveryStep); + prestashop.emit('editDelivery'); +}; + +export default editDeliveryStepHandler; diff --git a/_dev/js/theme/core/checkout/handler/misc/checkoutFormSubmitHandler.js b/_dev/js/theme/core/checkout/handler/misc/checkoutFormSubmitHandler.js new file mode 100644 index 00000000..a6db247b --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/misc/checkoutFormSubmitHandler.js @@ -0,0 +1,20 @@ +import prestashop from 'prestashop'; +import { each } from '../../../../utils/DOMHelpers'; + +/** + * Checkout form submit handler + * @param event {object} - submit event + */ +const checkoutFormSubmitHandler = (event) => { + const submitButtons = event.target.querySelectorAll('button[type="submit"]'); + + event.target.dataset.disabled = true; + + each(submitButtons, (submitButton) => { + submitButton.classList.add('disabled'); + }); + + prestashop.emit('submitCheckoutForm', { event }); +}; + +export default checkoutFormSubmitHandler; diff --git a/_dev/js/theme/core/checkout/handler/misc/checkoutStepChangeHandler.js b/_dev/js/theme/core/checkout/handler/misc/checkoutStepChangeHandler.js new file mode 100644 index 00000000..df3430c7 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/misc/checkoutStepChangeHandler.js @@ -0,0 +1,18 @@ +import prestashop from 'prestashop'; +import useCheckoutStepsController from '../../components/useCheckoutStepsController'; + +/** + * Checkout step change handler + * @param event {object} - click event + */ +const checkoutStepChangeHandler = (event) => { + event.preventDefault(); + const { changeStep, stepsSelector } = useCheckoutStepsController(); + + const clickedStep = event.target.closest(stepsSelector); + + changeStep(clickedStep); + prestashop.emit('changedCheckoutStep', { event }); +}; + +export default checkoutStepChangeHandler; diff --git a/_dev/js/theme/core/checkout/handler/payment/confirmOrderHandler.js b/_dev/js/theme/core/checkout/handler/payment/confirmOrderHandler.js new file mode 100644 index 00000000..7b133b40 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/payment/confirmOrderHandler.js @@ -0,0 +1,44 @@ +import prestashop from 'prestashop'; +import getSelectedPaymentOption from '../../utils/getSelectedPaymentOption'; +import canProceedOrder from '../../utils/canProceedOrder'; +import checkCartStillOrderableRequest from '../../request/checkCartStillOrderableRequest'; +import toggleOrderConfirmationButtonState from '../../utils/toggleOrderConfirmationButtonState'; +import { each } from '../../../../utils/DOMHelpers'; + +const showNativeFormErrors = () => { + const { termsCheckboxSelector } = prestashop.selectors.checkout; + + each(`input[name=payment-option], ${termsCheckboxSelector}`, (formElement) => { + formElement.reportValidity(); + }); +}; + +const confirmOrderHandler = async (e) => { + e.preventDefault(); + + const selectedPaymentOption = getSelectedPaymentOption(); + const selectedPaymentOptionId = selectedPaymentOption?.id; + + if (!canProceedOrder()) { + showNativeFormErrors(); + return; + } + + // We ask cart controller, if everything in the cart is still orderable + const { getRequest } = checkCartStillOrderableRequest(); + + const resp = await getRequest(); + + // We process the information and allow other modules to intercept this + const isRedirected = prestashop.checkout.onCheckOrderableCartResponse(resp); + + // If there is a redirect, we deny the form submit below, to allow the redirect to complete + if (isRedirected) { + return; + } + + toggleOrderConfirmationButtonState(false); + document.querySelector(`#pay-with-${selectedPaymentOptionId}-form form`)?.submit(); +}; + +export default confirmOrderHandler; diff --git a/_dev/js/theme/core/checkout/handler/payment/orderConfirmationErrorsHandler.js b/_dev/js/theme/core/checkout/handler/payment/orderConfirmationErrorsHandler.js new file mode 100644 index 00000000..516029f5 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/payment/orderConfirmationErrorsHandler.js @@ -0,0 +1,11 @@ +/** + * Redirect to cart page if there are errors on order confirmation + * @param event {object} - event object + */ +const orderConfirmationErrorsHandler = ({ resp }) => { + if (resp?.cartUrl !== '') { + window.location.href = resp.cartUrl; + } +}; + +export default orderConfirmationErrorsHandler; diff --git a/_dev/js/theme/core/checkout/handler/payment/toggleOrderButtonStateHandler.js b/_dev/js/theme/core/checkout/handler/payment/toggleOrderButtonStateHandler.js new file mode 100644 index 00000000..e015bed1 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/payment/toggleOrderButtonStateHandler.js @@ -0,0 +1,25 @@ +import prestashop from 'prestashop'; +import useToggleDisplay from '../../../../components/display/useToggleDisplay'; +import canProceedOrder from '../../utils/canProceedOrder'; +import toggleOrderConfirmationButtonState from '../../utils/toggleOrderConfirmationButtonState'; +import { each } from '../../../../utils/DOMHelpers'; + +const { toggle } = useToggleDisplay(); + +const toggleOrderButtonStateHandler = () => { + const { conditionAlertSelector } = prestashop.selectors.checkout; + + const paymentBtnEnabled = canProceedOrder(); + + prestashop.emit('termsUpdated', { + isChecked: paymentBtnEnabled, + }); + + toggleOrderConfirmationButtonState(paymentBtnEnabled); + + each(conditionAlertSelector, (element) => { + toggle(element, !paymentBtnEnabled); + }); +}; + +export default toggleOrderButtonStateHandler; diff --git a/_dev/js/theme/core/checkout/handler/payment/togglePaymentOptionsHandler.js b/_dev/js/theme/core/checkout/handler/payment/togglePaymentOptionsHandler.js new file mode 100644 index 00000000..b9592dd3 --- /dev/null +++ b/_dev/js/theme/core/checkout/handler/payment/togglePaymentOptionsHandler.js @@ -0,0 +1,23 @@ +import collapseAllPaymentOptions from '../../utils/collapseAllPaymentOptions'; +import getSelectedPaymentOption from '../../utils/getSelectedPaymentOption'; +import useToggleDisplay from '../../../../components/display/useToggleDisplay'; +import canProceedOrder from '../../utils/canProceedOrder'; +import toggleOrderConfirmationButtonState from '../../utils/toggleOrderConfirmationButtonState'; +import { each } from '../../../../utils/DOMHelpers'; + +const { show } = useToggleDisplay(); + +const togglePaymentOptionsHandler = () => { + const paymentBtnEnabled = canProceedOrder(); + + collapseAllPaymentOptions(); + + const selectedPaymentOption = getSelectedPaymentOption(); + const selectedOptionID = selectedPaymentOption?.id; + + each(`#${selectedOptionID}-additional-information, #pay-with-${selectedOptionID}-form`, show); + + toggleOrderConfirmationButtonState(paymentBtnEnabled); +}; + +export default togglePaymentOptionsHandler; diff --git a/_dev/js/theme/core/checkout/index.js b/_dev/js/theme/core/checkout/index.js index a1090daf..b8342142 100644 --- a/_dev/js/theme/core/checkout/index.js +++ b/_dev/js/theme/core/checkout/index.js @@ -1,77 +1,32 @@ -import DOMReady from '@js/theme/utils/DOMReady'; -import useCheckoutStepsController from '@js/theme/core/checkout/useCheckoutStepsController'; import prestashop from 'prestashop'; -import checkoutPayment from '@js/theme/core/checkout/checkoutPayment'; -import checkoutDelivery from '@js/theme/core/checkout/checkoutDelivery'; -import checkoutAddress from '@js/theme/core/checkout/checkoutAddress'; +import DOMReady from '../../utils/DOMReady'; -import checkoutController from '@js/theme/core/checkout/checkoutController'; +import checkoutController from './checkoutController'; prestashop.checkout = prestashop.checkout || {}; +// GLOBAL prestashop.checkout.onCheckOrderableCartResponse = (resp, paymentObject) => { if (resp.errors === true) { prestashop.emit('orderConfirmationErrors', { resp, paymentObject, }); - return true; - } - return false; -}; - -const { handleStepClick } = useCheckoutStepsController(); -const handleCheckoutStepChange = () => { - const editStepElements = document.querySelectorAll(prestashop.selectors.checkout.stepEdit); - - if (!editStepElements) { - return; + return true; } - editStepElements.forEach((editStepElement) => { - editStepElement.addEventListener('click', (event) => { - event.preventDefault(); - handleStepClick(event); - - prestashop.emit('changedCheckoutStep', { event }); - }); - }); -}; - -const handleSubmitButton = () => { - const checkoutForms = document.querySelectorAll(prestashop.selectors.checkout.form); - - checkoutForms.forEach((checkoutForm) => { - checkoutForm.addEventListener('submit', (event) => { - const submitButtons = event.target.querySelectorAll('button[type="submit"]'); - - event.target.dataset.disabled = true; - - if (submitButtons) { - submitButtons.forEach((submitButton) => { - submitButton.classList.add('disabled'); - }); - } - - prestashop.emit('submitCheckoutForm', { event }); - }); - }); + return false; }; const initCheckout = () => { - if (document.querySelector('body#checkout') === null) { + if (prestashop.page.page_name !== 'checkout') { return; } const { init } = checkoutController(); init(); - handleSubmitButton(); - handleCheckoutStepChange(); - checkoutPayment(); - checkoutDelivery(); - checkoutAddress(); }; DOMReady(() => { diff --git a/_dev/js/theme/core/checkout/request/checkCartStillOrderableRequest.js b/_dev/js/theme/core/checkout/request/checkCartStillOrderableRequest.js index b3f68354..1793f3c2 100644 --- a/_dev/js/theme/core/checkout/request/checkCartStillOrderableRequest.js +++ b/_dev/js/theme/core/checkout/request/checkCartStillOrderableRequest.js @@ -1,23 +1,25 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; +import prestashop from 'prestashop'; +import useDefaultHttpRequest from '../../../components/http/useDefaultHttpRequest'; -const checkCartStillOrderableRequest = (url) => { - const payload = { +/** + * @typedef ServerResponse + * @type {object} + * @property {string} cartUrl - cart page url + * @property {boolean} errors - errors flag (true if errors) + */ + +/** + * Check cart still orderable request + * @returns {{getRequest: (function(): Promise)}} + */ +const checkCartStillOrderableRequest = () => { + // payload not typed because it isn't needed + const payloadToSend = { ajax: 1, action: 'checkCartStillOrderable', }; - const { request } = useHttpRequest(url); - const getRequest = () => new Promise((resolve, reject) => { - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); - }); + const getRequest = () => useDefaultHttpRequest(prestashop.urls.pages.order, payloadToSend); return { getRequest, diff --git a/_dev/js/theme/core/checkout/request/selectDeliveryMethodRequest.js b/_dev/js/theme/core/checkout/request/selectDeliveryMethodRequest.js index 8607c7d2..1927f09e 100644 --- a/_dev/js/theme/core/checkout/request/selectDeliveryMethodRequest.js +++ b/_dev/js/theme/core/checkout/request/selectDeliveryMethodRequest.js @@ -1,22 +1,42 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; +import useDefaultHttpRequest from '../../../components/http/useDefaultHttpRequest'; -const selectDeliveryMethodRequest = (url, payload) => { - const { request } = useHttpRequest(url); +/** + * @typedef ServerResponse + * @type {object} + * @property {string} preview - checkout summary html content + */ - payload.ajax = 1; - payload.action = 'selectDeliveryOption'; +/** + * Select delivery method request + * @param url {string} - checkout url to send request + * @param payload {object} - request payload + * @param payload.delivery_option[id] {string} - delivery option id with id_address_delivery + * @param payload.ajax {number} - optional + * @param payload.action {string} - optional + * @example + * const payload = { + * 'delivery_option[1]': '2,', + * }; + * + * const { getRequest } = selectDeliveryMethodRequest(url, payload); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.log(error); + * } + * + * @returns {{getRequest: (function(): Promise)}} + */ +const selectDeliveryMethodRequest = (url, payload) => { + // payload not typed because delivery option parameter is dynamic + const payloadToSend = { + ajax: 1, + action: 'selectDeliveryOption', + ...payload, + }; - const getRequest = () => new Promise((resolve, reject) => { - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); - }); + const getRequest = () => useDefaultHttpRequest(url, payloadToSend); return { getRequest, diff --git a/_dev/js/theme/core/checkout/utils/areConditionsAccepted.js b/_dev/js/theme/core/checkout/utils/areConditionsAccepted.js new file mode 100644 index 00000000..a50d9cff --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/areConditionsAccepted.js @@ -0,0 +1,22 @@ +import prestashop from 'prestashop'; +import { each } from '../../../utils/DOMHelpers'; + +/** + * Check if all conditions are accepted + * @returns {boolean} true if all conditions are accepted or false otherwise + */ +const areConditionsAccepted = () => { + let accepted = true; + const { conditionsSelector } = prestashop.selectors.checkout; + const conditions = document.querySelectorAll(`${conditionsSelector} input[type="checkbox"]`); + + each(conditions, (condition) => { + if (!condition.checked) { + accepted = false; + } + }); + + return accepted; +}; + +export default areConditionsAccepted; diff --git a/_dev/js/theme/core/checkout/utils/canProceedOrder.js b/_dev/js/theme/core/checkout/utils/canProceedOrder.js new file mode 100644 index 00000000..b213a90d --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/canProceedOrder.js @@ -0,0 +1,18 @@ +import areConditionsAccepted from './areConditionsAccepted'; +import getSelectedPaymentOption from './getSelectedPaymentOption'; + +/** + * Check if order can be proceeded + * @returns {boolean} true if order can be proceeded or false otherwise + */ +const canProceedOrder = () => { + let proceed = areConditionsAccepted(); + + if (!getSelectedPaymentOption()) { + proceed = false; + } + + return proceed; +}; + +export default canProceedOrder; diff --git a/_dev/js/theme/core/checkout/utils/collapseAllPaymentOptions.js b/_dev/js/theme/core/checkout/utils/collapseAllPaymentOptions.js new file mode 100644 index 00000000..b11e71ba --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/collapseAllPaymentOptions.js @@ -0,0 +1,16 @@ +import prestashop from 'prestashop'; +import { each } from '../../../utils/DOMHelpers'; +import useToggleDisplay from '../../../components/display/useToggleDisplay'; + +/** + * Collapse all payment options additional information blocks and options form + */ +const collapseAllPaymentOptions = () => { + const { additionalInformatonSelector, optionsForm } = prestashop.selectors.checkout; + const paymentRelatedBlocks = document.querySelectorAll(`${additionalInformatonSelector}, ${optionsForm}`); + const { hide } = useToggleDisplay(); + + each(paymentRelatedBlocks, hide); +}; + +export default collapseAllPaymentOptions; diff --git a/_dev/js/theme/core/checkout/utils/getEditAddress.js b/_dev/js/theme/core/checkout/utils/getEditAddress.js index 467fe5e4..2fce3e2e 100644 --- a/_dev/js/theme/core/checkout/utils/getEditAddress.js +++ b/_dev/js/theme/core/checkout/utils/getEditAddress.js @@ -1,6 +1,6 @@ /** * Check if browser url contains editAddress parameter - * @returns {string | null} + * @returns {string | null} editAddress parameter value or null if not found */ const getEditAddress = () => { const urlParams = new URLSearchParams(window.location.search); diff --git a/_dev/js/theme/core/checkout/utils/getSelectedPaymentOption.js b/_dev/js/theme/core/checkout/utils/getSelectedPaymentOption.js new file mode 100644 index 00000000..7ab0144f --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/getSelectedPaymentOption.js @@ -0,0 +1,7 @@ +/** + * Get selected payment option + * @returns {HTMLElement|null} Selected payment option HTMLElement or null if not found + */ +const getSelectedPaymentOption = () => document.querySelector('input[name="payment-option"]:checked'); + +export default getSelectedPaymentOption; diff --git a/_dev/js/theme/core/checkout/utils/refreshCheckoutPage.js b/_dev/js/theme/core/checkout/utils/refreshCheckoutPage.js new file mode 100644 index 00000000..94eec1a1 --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/refreshCheckoutPage.js @@ -0,0 +1,17 @@ +/** + * Refresh checkout page with updated transaction parameter + */ +const refreshCheckoutPage = () => { + const urlParams = new URLSearchParams(window.location.search); + + if (urlParams.has('updatedTransaction')) { + window.location.reload(); + + return; + } + + urlParams.append('updatedTransaction', 1); + window.location.href = `${window.location.pathname}?${urlParams.toString()}`; +}; + +export default refreshCheckoutPage; diff --git a/_dev/js/theme/core/checkout/utils/switchConfirmAddressesButtonState.js b/_dev/js/theme/core/checkout/utils/switchConfirmAddressesButtonState.js index 2f5fb0d1..2f115664 100644 --- a/_dev/js/theme/core/checkout/utils/switchConfirmAddressesButtonState.js +++ b/_dev/js/theme/core/checkout/utils/switchConfirmAddressesButtonState.js @@ -1,8 +1,11 @@ +import { each } from '../../../utils/DOMHelpers'; + /** - * Enable/disable the continue address button + * Switch confirm addresses button state + * @param enable {boolean} - false if button should be disabled */ const switchConfirmAddressesButtonState = (enable) => { - document.querySelectorAll('button[name=confirm-addresses]').forEach((button) => { + each('button[name=confirm-addresses]', (button) => { button.disabled = !enable; }); }; diff --git a/_dev/js/theme/core/checkout/utils/switchEditAddressButtonColor.js b/_dev/js/theme/core/checkout/utils/switchEditAddressButtonColor.js index 7a026a16..c41eb6a6 100644 --- a/_dev/js/theme/core/checkout/utils/switchEditAddressButtonColor.js +++ b/_dev/js/theme/core/checkout/utils/switchEditAddressButtonColor.js @@ -1,22 +1,23 @@ +import { each } from '../../../utils/DOMHelpers'; + /** * Change the color of the edit button for the wrong address - * @param {Boolean} enabled - * @param {Number} id - * @param {String} type + * @param {boolean} enabled - true if button should be dangered or false otherwise + * @param {number} id - address id + * @param {string} type - address type (delivery or invoice) */ const switchEditAddressButtonColor = ( enabled, id, type, ) => { - const addressBtns = document.querySelectorAll(`#id_address_${type}-address-${id} .js-edit-address`); const classesToToggle = ['text-danger']; - document.querySelectorAll(`#${type}-addresses .js-edit-address`).forEach((button) => { + each(`#${type}-addresses .js-edit-address`, (button) => { button.classList.remove(...classesToToggle); }); - addressBtns.forEach((addressBtn) => { + each(`#id_address_${type}-address-${id} .js-edit-address`, (addressBtn) => { if (enabled) { addressBtn.classList.add(...classesToToggle); } else { diff --git a/_dev/js/theme/core/checkout/utils/toggleOrderConfirmationButtonState.js b/_dev/js/theme/core/checkout/utils/toggleOrderConfirmationButtonState.js new file mode 100644 index 00000000..8c55914c --- /dev/null +++ b/_dev/js/theme/core/checkout/utils/toggleOrderConfirmationButtonState.js @@ -0,0 +1,15 @@ +import prestashop from 'prestashop'; + +/** + * Toggle order confirmation button state + * @param active {boolean} - false if button should be disabled + */ +const toggleOrderConfirmationButtonState = (active = false) => { + const { confirmationSelector } = prestashop.selectors.checkout; + + document.querySelectorAll(`${confirmationSelector} button`).forEach((element) => { + element.classList.toggle('disabled', !active); + }); +}; + +export default toggleOrderConfirmationButtonState; diff --git a/_dev/js/theme/core/listing/handler/updateFacetsHandler.js b/_dev/js/theme/core/listing/handler/updateFacetsHandler.js new file mode 100644 index 00000000..ebbdd174 --- /dev/null +++ b/_dev/js/theme/core/listing/handler/updateFacetsHandler.js @@ -0,0 +1,34 @@ +import prestashop from 'prestashop'; +import updateListingFacetsRequest from '../request/updateListingFacetsRequest'; +import useAlertToast from '../../../components/useAlertToast'; + +const { danger } = useAlertToast(); + +/** + * Build new facets url - add from-xhr param + * @param {string} url - current url + * @returns {string} - new url with from-xhr param + */ +const buildNewFacetsUrl = (url) => { + const urlObject = new URL(url); + const params = new URLSearchParams(urlObject.search); + params.set('from-xhr', '1'); + + return `${urlObject.origin}${urlObject.pathname}?${params.toString()}`; +}; + +const updateFacetsHandler = async (url) => { + const newUrl = buildNewFacetsUrl(url); + const { getRequest } = updateListingFacetsRequest(newUrl); + + try { + const data = await getRequest(); + + prestashop.emit('updateProductList', data); + window.history.pushState(data, document.title, data.current_url); + } catch (error) { + danger(prestashop.t.alert.genericHttpError); + } +}; + +export default updateFacetsHandler; diff --git a/_dev/js/theme/core/listing/index.js b/_dev/js/theme/core/listing/index.js index 5df1b790..e2d9ce65 100644 --- a/_dev/js/theme/core/listing/index.js +++ b/_dev/js/theme/core/listing/index.js @@ -1,6 +1,8 @@ import DOMReady from '@js/theme/utils/DOMReady'; -import updateFacets from '@js/theme/core/listing/updateFacets'; +import listingController from './listingController'; + +const { init } = listingController(); DOMReady(() => { - updateFacets(); + init(); }); diff --git a/_dev/js/theme/core/listing/listingController.js b/_dev/js/theme/core/listing/listingController.js new file mode 100644 index 00000000..15f2fe53 --- /dev/null +++ b/_dev/js/theme/core/listing/listingController.js @@ -0,0 +1,14 @@ +import prestashop from 'prestashop'; +import updateFacetsHandler from './handler/updateFacetsHandler'; + +const listingController = () => { + const init = () => { + prestashop.on('updateFacets', updateFacetsHandler); + }; + + return { + init, + }; +}; + +export default listingController; diff --git a/_dev/js/theme/core/listing/request/updateListingFacetsRequest.js b/_dev/js/theme/core/listing/request/updateListingFacetsRequest.js new file mode 100644 index 00000000..95a0e94e --- /dev/null +++ b/_dev/js/theme/core/listing/request/updateListingFacetsRequest.js @@ -0,0 +1,72 @@ +import useHttpRequest from '../../../components/http/useHttpRequest'; +import useHttpController from '../../../components/http/useHttpController'; + +const { dispatch, abortAll } = useHttpController(); + +/** + * @typedef ServerResponse + * @type {object} + * @property {string} current_url - new url + * @property {boolean} js_enabled - is js enabled + * @property {string} label - listing label + * @property {object} pagination - pagination object + * @property {number} pagination.current_page - pagination current page + * @property {number} pagination.items_shown_from - pagination items shown from + * @property {number} pagination.items_shown_to - pagination items shown to + * @property {array} pagination.pages - pagination pages array + * @property {object[]} products - array of front representations of products + * @property {string} rendered_active_filters - active filters html content + * @property {string} rendered_facets - facets html content + * @property {string} rendered_products - listing products html content + * @property {string} rendered_products_bottom - listing products bottom html content + * @property {string} rendered_products_header - listing products header html content + * @property {string} rendered_products_top - listing products top html content + * @property {object} result - result empty object + * @property {object[]} sort_orders - available sort orders + * @property {string} sort_selected - selected sort order + */ + +/** + * Update listing facets request + * @param url {string} - new url with from-xhr param + * @example + * const { getRequest } = updateListingFacetsRequest(url); + * + * try { + * const resp = await getRequest(); + * } catch (error) { + * console.error(error); + * } + * @returns {{getRequest: (function(): Promise)}} + */ +const updateListingFacetsRequest = (url) => { + const { request, controller } = useHttpRequest(url, { + headers: { + accept: 'application/json', + }, + }); + + const getRequest = () => new Promise((resolve, reject) => { + abortAll(); + + dispatch(request, controller)(() => request + .get() + .json((resp) => { + resolve(resp); + }) + .catch((e) => { + // IF ABORTED + if (e instanceof DOMException) { + return; + } + + reject(e); + })); + }); + + return { + getRequest, + }; +}; + +export default updateListingFacetsRequest; diff --git a/_dev/js/theme/core/listing/updateFacets.js b/_dev/js/theme/core/listing/updateFacets.js deleted file mode 100644 index df7aa044..00000000 --- a/_dev/js/theme/core/listing/updateFacets.js +++ /dev/null @@ -1,26 +0,0 @@ -import prestashop from 'prestashop'; -import useAlertToast from '@js/theme/components/useAlertToast'; - -const { danger } = useAlertToast(); - -const handleFacetsUpdate = async (url) => { - try { - const separator = url.indexOf('?') >= 0 ? '&' : '?'; - const slightlyDifferentURL = `${url + separator}from-xhr`; - - const data = await prestashop.frontAPI.updateListingFacets(slightlyDifferentURL); - - prestashop.emit('updateProductList', data); - window.history.pushState(data, document.title, data.current_url); - } catch (error) { - danger(prestashop.t.alert.genericHttpError); - } -}; - -const updateFacets = () => { - prestashop.on('updateFacets', (url) => { - handleFacetsUpdate(url); - }); -}; - -export default updateFacets; diff --git a/_dev/js/theme/frontAPI/address/updateAddressAction.js b/_dev/js/theme/frontAPI/address/updateAddressAction.js deleted file mode 100644 index 66d04291..00000000 --- a/_dev/js/theme/frontAPI/address/updateAddressAction.js +++ /dev/null @@ -1,32 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; -import useHttpController from '@js/theme/components/http/useHttpController'; - -const { dispatch, abortAll } = useHttpController(); - -const updateAddressAction = (url, idAddress, idCountry) => new Promise((resolve, reject) => { - abortAll(); - - const { request, controller } = useHttpRequest(url, {}); - - const payload = { - id_address: idAddress, - id_country: idCountry, - }; - - dispatch(request, controller)(() => request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch((e) => { - // IF ABORTED - if (e instanceof DOMException) { - return; - } - - reject(); - })); -}); - -export default updateAddressAction; diff --git a/_dev/js/theme/frontAPI/apiAction.js b/_dev/js/theme/frontAPI/apiAction.js index ed8df220..21f10987 100644 --- a/_dev/js/theme/frontAPI/apiAction.js +++ b/_dev/js/theme/frontAPI/apiAction.js @@ -1,12 +1,4 @@ -import addToCartAction from '@js/theme/frontAPI/cart/addToCartAction'; -import addVoucherToCartAction from '@js/theme/frontAPI/cart/addVoucherToCartAction'; -import refreshCartPageAction from '@js/theme/frontAPI/cart/refreshCartPageAction'; -import updateCartQuantityAction from '@js/theme/frontAPI/cart/updateCartQuantityAction'; -import deleteFromCartAction from '@js/theme/frontAPI/cart/deleteFromCartAction'; -import deleteVoucherFromCartAction from '@js/theme/frontAPI/cart/deleteVoucherFromCartAction'; import updateProductAction from '@js/theme/frontAPI/product/updateProductAction'; -import updateListingFacetsAction from '@js/theme/frontAPI/listing/updateListingFacetsAction'; -import updateAddressAction from '@js/theme/frontAPI/address/updateAddressAction'; prestashop.frontAPI = {}; @@ -18,12 +10,4 @@ prestashop.addAction = (actionName, actionFunction) => { prestashop.frontAPI[actionName] = actionFunction; }; -prestashop.addAction('addToCart', addToCartAction); -prestashop.addAction('addVoucherToCart', addVoucherToCartAction); -prestashop.addAction('deleteVoucherFromCart', deleteVoucherFromCartAction); -prestashop.addAction('refreshCartPage', refreshCartPageAction); -prestashop.addAction('updateCartQuantity', updateCartQuantityAction); -prestashop.addAction('deleteFromCart', deleteFromCartAction); prestashop.addAction('updateProduct', updateProductAction); -prestashop.addAction('updateListingFacets', updateListingFacetsAction); -prestashop.addAction('updateAddress', updateAddressAction); diff --git a/_dev/js/theme/frontAPI/cart/addToCartAction.js b/_dev/js/theme/frontAPI/cart/addToCartAction.js deleted file mode 100644 index a7f30b96..00000000 --- a/_dev/js/theme/frontAPI/cart/addToCartAction.js +++ /dev/null @@ -1,34 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const addToCartAction = async (idProduct, quantity, idProductAttribute = 0, idCustomization = 0) => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const payload = { - add: 1, - id_product: parseInt(idProduct, 10), - action: 'update', - token: prestashop.static_token, - qty: parseInt(quantity, 10), - ajax: 1, - }; - - if (idProductAttribute > 0) { - payload.id_product_attribute = parseInt(idProductAttribute, 10); - } - - if (idCustomization > 0) { - payload.id_customization = parseInt(idCustomization, 10); - } - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default addToCartAction; diff --git a/_dev/js/theme/frontAPI/cart/addVoucherToCartAction.js b/_dev/js/theme/frontAPI/cart/addVoucherToCartAction.js deleted file mode 100644 index 5a96523c..00000000 --- a/_dev/js/theme/frontAPI/cart/addVoucherToCartAction.js +++ /dev/null @@ -1,25 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const addVoucherToCartAction = async (discountName) => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const payload = { - addDiscount: 1, - discount_name: discountName, - action: 'update', - token: prestashop.static_token, - ajax: 1, - }; - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default addVoucherToCartAction; diff --git a/_dev/js/theme/frontAPI/cart/deleteFromCartAction.js b/_dev/js/theme/frontAPI/cart/deleteFromCartAction.js deleted file mode 100644 index 41ce42fe..00000000 --- a/_dev/js/theme/frontAPI/cart/deleteFromCartAction.js +++ /dev/null @@ -1,30 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const deleteFromCartAction = async (idProduct, idProductAttribute, idCustomization = 0) => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const payload = { - delete: 1, - id_product: parseInt(idProduct, 10), - id_product_attribute: parseInt(idProductAttribute, 10), - action: 'update', - token: prestashop.static_token, - ajax: 1, - }; - - if (idCustomization > 0) { - payload.id_customization = parseInt(idCustomization, 10); - } - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default deleteFromCartAction; diff --git a/_dev/js/theme/frontAPI/cart/deleteVoucherFromCartAction.js b/_dev/js/theme/frontAPI/cart/deleteVoucherFromCartAction.js deleted file mode 100644 index 37431946..00000000 --- a/_dev/js/theme/frontAPI/cart/deleteVoucherFromCartAction.js +++ /dev/null @@ -1,24 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const deleteVoucherFromCartAction = async (idDiscount) => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const payload = { - deleteDiscount: idDiscount, - action: 'update', - token: prestashop.static_token, - ajax: 1, - }; - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default deleteVoucherFromCartAction; diff --git a/_dev/js/theme/frontAPI/cart/refreshCartPageAction.js b/_dev/js/theme/frontAPI/cart/refreshCartPageAction.js deleted file mode 100644 index 2491beaf..00000000 --- a/_dev/js/theme/frontAPI/cart/refreshCartPageAction.js +++ /dev/null @@ -1,22 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const refreshCartPageAction = async () => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const payload = { - action: 'refresh', - ajax: 1, - }; - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default refreshCartPageAction; diff --git a/_dev/js/theme/frontAPI/cart/updateCartQuantityAction.js b/_dev/js/theme/frontAPI/cart/updateCartQuantityAction.js deleted file mode 100644 index b194e9fc..00000000 --- a/_dev/js/theme/frontAPI/cart/updateCartQuantityAction.js +++ /dev/null @@ -1,44 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; - -const updateCartQuantityAction = async ( - operation, - idProduct, - idProductAttribute, - quantity, - idCustomization = 0, -) => new Promise((resolve, reject) => { - const { request } = useHttpRequest(prestashop.urls.pages.cart); - - const allowedOperations = ['up', 'down']; - - if (!allowedOperations.includes(operation)) { - reject(Error('Invalid operation')); - } - - const payload = { - update: 1, - id_product: parseInt(idProduct, 10), - id_product_attribute: parseInt(idProductAttribute, 10), - op: operation, - action: 'update', - token: prestashop.static_token, - qty: parseInt(quantity, 10), - ajax: 1, - }; - - if (idCustomization > 0) { - payload.id_customization = parseInt(idCustomization, 10); - } - - request - .query(payload) - .post() - .json((resp) => { - resolve(resp); - }) - .catch(() => { - reject(Error(prestashop.t.alert.genericHttpError)); - }); -}); - -export default updateCartQuantityAction; diff --git a/_dev/js/theme/frontAPI/listing/updateListingFacetsAction.js b/_dev/js/theme/frontAPI/listing/updateListingFacetsAction.js deleted file mode 100644 index e3567060..00000000 --- a/_dev/js/theme/frontAPI/listing/updateListingFacetsAction.js +++ /dev/null @@ -1,30 +0,0 @@ -import useHttpRequest from '@js/theme/components/http/useHttpRequest'; -import useHttpController from '@js/theme/components/http/useHttpController'; - -const { dispatch, abortAll } = useHttpController(); - -const updateListingFacetsAction = (url) => new Promise((resolve, reject) => { - abortAll(); - - const { request, controller } = useHttpRequest(url, { - headers: { - accept: 'application/json, text/javascript, */*', - }, - }); - - dispatch(request, controller)(() => request - .get() - .json((resp) => { - resolve(resp); - }) - .catch((e) => { - // IF ABORTED - if (e instanceof DOMException) { - return; - } - - reject(); - })); -}); - -export default updateListingFacetsAction; diff --git a/_dev/js/theme/utils/DOMHelpers.js b/_dev/js/theme/utils/DOMHelpers.js index 974672a4..aa31e456 100644 --- a/_dev/js/theme/utils/DOMHelpers.js +++ b/_dev/js/theme/utils/DOMHelpers.js @@ -1,7 +1,7 @@ /** * Check if element is visible - * @param el - * @returns {boolean} + * @param el {HTMLElement} - element to check + * @returns {boolean} - true if element is visible and false otherwise */ export const isElementVisible = (el) => !!( el.offsetWidth @@ -11,8 +11,8 @@ export const isElementVisible = (el) => !!( /** * Run a function on each element - * @param elementsOrSelector - * @param fnc + * @param elementsOrSelector {NodeList|HTMLCollection|string} - elements or selector + * @param fnc {function} - function to run on each element * @returns {void} */ export const each = (elementsOrSelector, fnc) => { diff --git a/_dev/js/theme/utils/DOMReady.js b/_dev/js/theme/utils/DOMReady.js index bf7cd2f9..cd463d5a 100644 --- a/_dev/js/theme/utils/DOMReady.js +++ b/_dev/js/theme/utils/DOMReady.js @@ -1,3 +1,7 @@ +/** + * DOMReady function runs a callback function when the DOM is ready (DOMContentLoaded) + * @param callback {function} - callback function + */ const DOMReady = (callback) => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); diff --git a/_dev/js/theme/utils/DOMSelectorsHelper.js b/_dev/js/theme/utils/DOMSelectorsHelper.js index 9099ceaf..f13ab10b 100644 --- a/_dev/js/theme/utils/DOMSelectorsHelper.js +++ b/_dev/js/theme/utils/DOMSelectorsHelper.js @@ -1,3 +1,9 @@ +/** + * Get all siblings before element + * @param elem {HTMLElement} - element to get siblings + * @param includeCurrentElement {boolean} - include current element in siblings list + * @returns {HTMLElement[]} - siblings list + */ export const getAllSiblingsBeforeElement = (elem, includeCurrentElement = false) => { const siblings = []; let sibling = elem.previousElementSibling; @@ -16,6 +22,12 @@ export const getAllSiblingsBeforeElement = (elem, includeCurrentElement = false) return siblings; }; +/** + * Get all siblings after element + * @param elem {HTMLElement} - element to get siblings + * @param includeCurrentElement {boolean} - include current element in siblings list + * @returns {HTMLElement[]} - siblings list + */ export const getAllSiblingsAfterElement = (elem, includeCurrentElement = false) => { const siblings = []; let sibling = elem.nextElementSibling; diff --git a/_dev/js/theme/utils/debounce.js b/_dev/js/theme/utils/debounce.js index 8d3a59e3..1fbabe54 100644 --- a/_dev/js/theme/utils/debounce.js +++ b/_dev/js/theme/utils/debounce.js @@ -1,8 +1,16 @@ -export default function debounce(func, timeout = 300) { +/** + * Debounce function + * @param func {function} - function to debounce + * @param timeout {number} - timeout in ms (default: 300) + * @returns {(function(...[*]): void)|*} + */ +const debounce = (func, timeout = 300) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, timeout); }; -} +}; + +export default debounce; diff --git a/_dev/js/theme/utils/formSerialize.js b/_dev/js/theme/utils/formSerialize.js index 45d39f48..8f89b862 100644 --- a/_dev/js/theme/utils/formSerialize.js +++ b/_dev/js/theme/utils/formSerialize.js @@ -1,7 +1,7 @@ /** * Serializes the form into an object - * @param {HTMLFormElement} form - * @returns {Object} + * @param form {HTMLFormElement} - form element to serialize + * @returns {Object} - serialized form data */ export const fromSerializeObject = (form) => { const data = {}; @@ -16,15 +16,15 @@ export const fromSerializeObject = (form) => { /** * Substitutes the jQuery.serialize() method - * @param {HTMLFormElement} form - * @returns {string} + * @param form {HTMLFormElement} - form element to serialize + * @returns {string} - serialized form data in URLSearchParams format */ export const fromSerialize = (form) => new URLSearchParams(new FormData(form)).toString(); /** * Substitutes the jQuery.serializeArray() method - * @param {HTMLFormElement} form - * @returns {Array} + * @param form {HTMLFormElement} - form element to serialize + * @returns {Array} - serialized form data in array format [{name: 'name', value: 'value'}] */ export const formSerializeArray = (form) => { const data = []; diff --git a/_dev/js/theme/utils/getUniqueId.js b/_dev/js/theme/utils/getUniqueId.js index 8a6c4c91..005988af 100644 --- a/_dev/js/theme/utils/getUniqueId.js +++ b/_dev/js/theme/utils/getUniqueId.js @@ -1,5 +1,10 @@ let index = 0; +/** + * Get unique id + * @param suffix {string} - suffix for unique id + * @returns {string} - unique id with suffix + */ const getUniqueId = (suffix = '') => { index += 1; return index + suffix; diff --git a/_dev/js/theme/utils/parseToHtml.js b/_dev/js/theme/utils/parseToHtml.js index 053ad364..6476931c 100644 --- a/_dev/js/theme/utils/parseToHtml.js +++ b/_dev/js/theme/utils/parseToHtml.js @@ -1,7 +1,7 @@ /** * Convert a template string into HTML DOM nodes - * @param {String} str The template string - * @return {Node} The template HTML + * @param str {string} - string representation of the HTML + * @return {HTMLElement} - HTML element from the string */ const parseToHtml = (str) => { const parser = new DOMParser(); diff --git a/_dev/js/theme/utils/sprintf.js b/_dev/js/theme/utils/sprintf.js index 4219ec2c..3436a362 100644 --- a/_dev/js/theme/utils/sprintf.js +++ b/_dev/js/theme/utils/sprintf.js @@ -1,3 +1,9 @@ +/** + * simple sprintf implementation + * @param str {string} - string to format + * @param args {Array} - arguments to replace + * @returns {string} - formatted string + */ const sprintf = (str, ...args) => { let i = 0; diff --git a/templates/checkout/_partials/cart-voucher.tpl b/templates/checkout/_partials/cart-voucher.tpl index eb6cd060..9deea9f6 100644 --- a/templates/checkout/_partials/cart-voucher.tpl +++ b/templates/checkout/_partials/cart-voucher.tpl @@ -88,7 +88,10 @@ {foreach from=$cart.discounts item=discount}
  • - {$discount.code} - {$discount.name} + + {$discount.code} + + - {$discount.name}
  • {/foreach}