/** * Copyright since 2007 PrestaShop SA and Contributors * PrestaShop is an International Registered Trademark & Property of PrestaShop SA * * NOTICE OF LICENSE * * This source file is subject to the Open Software License (OSL 3.0) * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: * https://opensource.org/licenses/OSL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * * DISCLAIMER * * Do not edit or add to this file if you wish to upgrade PrestaShop to newer * versions in the future. If you wish to customize PrestaShop for your * needs please refer to https://devdocs.prestashop.com/ for more information. * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ import {showGrowl} from '@app/utils/growl'; import ComponentsMap from '@components/components-map'; import ClickEvent = JQuery.ClickEvent; const {$} = window; export type SubmittableInputConfig = { wrapperSelector: string; submitCallback: (input: HTMLInputElement) => any; afterSuccess?: ((input: HTMLInputElement, response: AjaxResponse) => any); afterFailure?: ((input: HTMLInputElement, error: AjaxError) => any); } /** * Activates, deactivates, shows, hides submit button inside an input * (depending if input was changed comparing to initial value) * After button is clicked, component fires the callback function which was provided to constructor. */ export default class SubmittableInput { config: SubmittableInputConfig; inputSelector: string; inputsInContainerSelector: string; buttonSelector: string; loading: boolean; constructor(config: SubmittableInputConfig) { this.config = config; this.inputSelector = ComponentsMap.submittableInput.inputSelector; this.buttonSelector = ComponentsMap.submittableInput.buttonSelector; this.inputsInContainerSelector = `${this.config.wrapperSelector} ${this.inputSelector}`; this.loading = false; this.init(); } public reset(input: HTMLInputElement, value: string|number): void { $(input).val(value); $(input).data('initialValue', value); } private init(): void { $(document).on('focus mouseenter', this.inputsInContainerSelector, (e) => { this.refreshButtonState(e.currentTarget, true); }); $(document).on('input blur mouseleave', this.inputsInContainerSelector, (e) => { this.refreshButtonState(e.currentTarget); }); $(document).on('click', `${this.config.wrapperSelector} ${this.buttonSelector}`, (e: ClickEvent) => { e.stopImmediatePropagation(); this.submitInput(this.findInput(e.currentTarget)); }); $(document).on('keyup', this.inputsInContainerSelector, (e: JQueryEventObject) => { // only on ENTER if (e.keyCode !== 13) { return; } e.preventDefault(); e.stopImmediatePropagation(); this.submitInput(e.target as HTMLInputElement); }); } private submitInput(input: HTMLInputElement): void { if (this.loading || !this.inputValueChanged(input)) { return; } this.toggleLoading(input, true); const button = this.findButton(input); this.config.submitCallback(input) .then((response: AjaxResponse) => { $(input).data('initialValue', input.value); this.toggleButtonVisibility(button, false); this.toggleLoading(input, false); if (response.message) { showGrowl('success', response.message); } if (this.config.afterSuccess) { this.config.afterSuccess(input, response); } }) .catch((error: AjaxError) => { this.toggleError(button, true); this.toggleButtonVisibility(button, false); this.toggleLoading(input, false); if (typeof error.responseJSON.errors === 'undefined') { return; } const messages = error.responseJSON.errors; Object.keys(messages).forEach((key) => { showGrowl('error', messages[key]); }); if (this.config.afterFailure) { this.config.afterFailure(input, error); } }); } private refreshButtonState( input: HTMLElement, visible: boolean | null = null, ): void { const button = this.findButton(input); const valueWasChanged = this.inputValueChanged(input); this.toggleButtonActivity(button, valueWasChanged); if (visible !== null) { this.toggleButtonVisibility(button, visible); } else { this.toggleButtonVisibility(button, valueWasChanged); } } private toggleButtonActivity(button: HTMLElement, active: boolean): void { $(button).toggleClass('active', active); } private toggleButtonVisibility( button: Element, visible: boolean, ): void { const $button = $(button); $button.toggleClass('d-none', !visible); } private toggleLoading(input: HTMLInputElement, loading: boolean): void { this.loading = loading; const button = this.findButton(input); // eslint-disable-next-line no-param-reassign input.disabled = this.loading; button.disabled = this.loading; if (this.loading) { $(button).html(''); } else { $(button).html('check'); } } private toggleError(button: HTMLButtonElement, error: boolean): void { const input = this.findInput(button); $(input).toggleClass('is-invalid', error); } private findButton(input: Element): HTMLButtonElement { return $(input) .closest(this.config.wrapperSelector) .find(this.buttonSelector)[0]; } private findInput(button: HTMLButtonElement): HTMLInputElement { return $(button) .closest(this.config.wrapperSelector) .find(this.inputSelector)[0]; } private inputValueChanged(input: HTMLElement): boolean { const initialValue = $(input).data('initial-value'); let newValue = $(input).val(); if ($(input).hasClass('is-invalid')) { $(input).removeClass('is-invalid'); } if (typeof initialValue === 'number') { newValue = Number(newValue); } return initialValue !== newValue; } }