first commit

This commit is contained in:
2024-11-05 12:22:50 +01:00
commit e5682a3912
19641 changed files with 2948548 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
export default class AutocompleteWithEmail {
constructor(emailInputSelector, map = []) {
this.map = map;
this.$emailInput = $(emailInputSelector);
this.$emailInput.on('change', () => this.change());
}
change() {
$.get({
url: this.$emailInput.data('customer-information-url'),
dataType: 'json',
data: {
email: this.$emailInput.val(),
},
}).then((response) => {
Object.keys(this.map).forEach((key) => {
if (response[key] !== undefined) {
$(this.map[key]).val(response[key]);
}
});
}).catch((response) => {
if (typeof response.responseJSON !== 'undefined') {
window.showErrorMessage(response.responseJSON.message);
}
});
}
}

View File

@@ -0,0 +1,249 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ChangePasswordHandler from '../change-password-handler';
import PasswordValidator from '../password-validator';
const {$} = window;
/**
* Class responsible for actions related to "change password" form type.
* Generates random passwords, validates new password and it's confirmation,
* displays error messages related to validation.
*/
export default class ChangePasswordControl {
constructor(
inputsBlockSelector,
showButtonSelector,
hideButtonSelector,
generatePasswordButtonSelector,
oldPasswordInputSelector,
newPasswordInputSelector,
confirmNewPasswordInputSelector,
generatedPasswordDisplaySelector,
passwordStrengthFeedbackContainerSelector,
) {
// Block that contains password inputs
this.$inputsBlock = $(inputsBlockSelector);
// Button that shows the password inputs block
this.showButtonSelector = showButtonSelector;
// Button that hides the password inputs block
this.hideButtonSelector = hideButtonSelector;
// Button that generates a random password
this.generatePasswordButtonSelector = generatePasswordButtonSelector;
// Input to enter old password
this.oldPasswordInputSelector = oldPasswordInputSelector;
// Input to enter new password
this.newPasswordInputSelector = newPasswordInputSelector;
// Input to confirm the new password
this.confirmNewPasswordInputSelector = confirmNewPasswordInputSelector;
// Input that displays generated random password
this.generatedPasswordDisplaySelector = generatedPasswordDisplaySelector;
// Main input for password generation
this.$newPasswordInputs = this.$inputsBlock
.find(this.newPasswordInputSelector);
// Generated password will be copied to these inputs
this.$copyPasswordInputs = this.$inputsBlock
.find(this.confirmNewPasswordInputSelector)
.add(this.generatedPasswordDisplaySelector);
// All inputs in the change password block, that are submittable with the form.
this.$submittableInputs = this.$inputsBlock
.find(this.oldPasswordInputSelector)
.add(this.newPasswordInputSelector)
.add(this.confirmNewPasswordInputSelector);
this.passwordHandler = new ChangePasswordHandler(
passwordStrengthFeedbackContainerSelector,
);
this.passwordValidator = new PasswordValidator(
this.newPasswordInputSelector,
this.confirmNewPasswordInputSelector,
);
this.hideInputsBlock();
this.initEvents();
return {};
}
/**
* Initialize events.
*
* @private
*/
initEvents() {
// Show the inputs block when show button is clicked
$(document).on('click', this.showButtonSelector, (e) => {
this.hide($(e.currentTarget));
this.showInputsBlock();
});
$(document).on('click', this.hideButtonSelector, () => {
this.hideInputsBlock();
this.show($(this.showButtonSelector));
});
// Watch and display feedback about password's strength
this.passwordHandler.watchPasswordStrength(this.$newPasswordInputs);
$(document).on('click', this.generatePasswordButtonSelector, () => {
// Generate the password into main input.
this.passwordHandler.generatePassword(this.$newPasswordInputs);
// Copy the generated password from main input to additional inputs
this.$copyPasswordInputs.val(this.$newPasswordInputs.val());
this.checkPasswordValidity();
});
// Validate new password and it's confirmation when any of the inputs is changed
$(document).on(
'keyup',
`${this.newPasswordInputSelector},${this.confirmNewPasswordInputSelector}`,
() => {
this.checkPasswordValidity();
},
);
// Prevent submitting the form if new password is not valid
$(document).on('submit', $(this.oldPasswordInputSelector).closest('form'), (event) => {
// If password input is disabled - we don't need to validate it.
if ($(this.oldPasswordInputSelector).is(':disabled')) {
return;
}
if (!this.passwordValidator.isPasswordValid()) {
event.preventDefault();
}
});
}
/**
* Check if password is valid, show error messages if it's not.
*
* @private
*/
checkPasswordValidity() {
const $firstPasswordErrorContainer = $(this.newPasswordInputSelector).parent().find('.form-text');
const $secondPasswordErrorContainer = $(this.confirmNewPasswordInputSelector).parent().find('.form-text');
$firstPasswordErrorContainer
.text(this.getPasswordLengthValidationMessage())
.toggleClass('text-danger', !this.passwordValidator.isPasswordLengthValid());
$secondPasswordErrorContainer
.text(this.getPasswordConfirmationValidationMessage())
.toggleClass('text-danger', !this.passwordValidator.isPasswordMatchingConfirmation());
}
/**
* Get password confirmation validation message.
*
* @returns {String}
*
* @private
*/
getPasswordConfirmationValidationMessage() {
if (!this.passwordValidator.isPasswordMatchingConfirmation()) {
return $(this.confirmNewPasswordInputSelector).data('invalid-password');
}
return '';
}
/**
* Get password length validation message.
*
* @returns {String}
*
* @private
*/
getPasswordLengthValidationMessage() {
if (this.passwordValidator.isPasswordTooShort()) {
return $(this.newPasswordInputSelector).data('password-too-short');
}
if (this.passwordValidator.isPasswordTooLong()) {
return $(this.newPasswordInputSelector).data('password-too-long');
}
return '';
}
/**
* Show the password inputs block.
*
* @private
*/
showInputsBlock() {
this.show(this.$inputsBlock);
this.$submittableInputs.removeAttr('disabled');
this.$submittableInputs.attr('required', 'required');
}
/**
* Hide the password inputs block.
*
* @private
*/
hideInputsBlock() {
this.hide(this.$inputsBlock);
this.$submittableInputs.attr('disabled', 'disabled');
this.$submittableInputs.removeAttr('required');
this.$inputsBlock.find('input').val('');
this.$inputsBlock.find('.form-text').text('');
}
/**
* Hide an element.
*
* @param {jQuery} $el
*
* @private
*/
hide($el) {
$el.addClass('d-none');
}
/**
* Show hidden element.
*
* @param {jQuery} $el
*
* @private
*/
show($el) {
$el.removeClass('d-none');
}
}

View File

@@ -0,0 +1,158 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* Handles UI interactions of choice tree
*/
export default class ChoiceTree {
/**
* @param {String} treeSelector
*/
constructor(treeSelector) {
this.$container = $(treeSelector);
this.$container.on('click', '.js-input-wrapper', (event) => {
const $inputWrapper = $(event.currentTarget);
this.toggleChildTree($inputWrapper);
});
this.$container.on('click', '.js-toggle-choice-tree-action', (event) => {
const $action = $(event.currentTarget);
this.toggleTree($action);
});
return {
enableAutoCheckChildren: () => this.enableAutoCheckChildren(),
enableAllInputs: () => this.enableAllInputs(),
disableAllInputs: () => this.disableAllInputs(),
};
}
/**
* Enable automatic check/uncheck of clicked item's children.
*/
enableAutoCheckChildren() {
this.$container.on('change', 'input[type="checkbox"]', (event) => {
const $clickedCheckbox = $(event.currentTarget);
const $itemWithChildren = $clickedCheckbox.closest('li');
$itemWithChildren
.find('ul input[type="checkbox"]')
.prop('checked', $clickedCheckbox.is(':checked'));
});
}
/**
* Enable all inputs in the choice tree.
*/
enableAllInputs() {
this.$container.find('input').removeAttr('disabled');
}
/**
* Disable all inputs in the choice tree.
*/
disableAllInputs() {
this.$container.find('input').attr('disabled', 'disabled');
}
/**
* Collapse or expand sub-tree for single parent
*
* @param {jQuery} $inputWrapper
*
* @private
*/
toggleChildTree($inputWrapper) {
const $parentWrapper = $inputWrapper.closest('li');
if ($parentWrapper.hasClass('expanded')) {
$parentWrapper
.removeClass('expanded')
.addClass('collapsed');
return;
}
if ($parentWrapper.hasClass('collapsed')) {
$parentWrapper
.removeClass('collapsed')
.addClass('expanded');
}
}
/**
* Collapse or expand whole tree
*
* @param {jQuery} $action
*
* @private
*/
toggleTree($action) {
const $parentContainer = $action.closest('.js-choice-tree-container');
const action = $action.data('action');
// toggle action configuration
const config = {
addClass: {
expand: 'expanded',
collapse: 'collapsed',
},
removeClass: {
expand: 'collapsed',
collapse: 'expanded',
},
nextAction: {
expand: 'collapse',
collapse: 'expand',
},
text: {
expand: 'collapsed-text',
collapse: 'expanded-text',
},
icon: {
expand: 'collapsed-icon',
collapse: 'expanded-icon',
},
};
$parentContainer.find('li').each((index, item) => {
const $item = $(item);
if ($item.hasClass(config.removeClass[action])) {
$item.removeClass(config.removeClass[action])
.addClass(config.addClass[action]);
}
});
$action.data('action', config.nextAction[action]);
$action.find('.material-icons').text($action.data(config.icon[action]));
$action.find('.js-toggle-text').text($action.data(config.text[action]));
}
}

View File

@@ -0,0 +1,390 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import _ from 'lodash';
const {$} = window;
/**
* This is able to watch an HTML form and parse it as a Javascript object based on a configurable
* mapping. Each field from the model is mapped to a form input, or several, each input is watched
* to keep the model consistent.
*
* The model mapping used for this component is an object which uses the modelKey as a key (it represents
* the property path in the object, separated by a dot) and the input names as value (they follow Symfony
* convention naming using brackets). Here is an example of mapping:
*
* const modelMapping = {
* 'product.stock.quantity': 'product[stock][quantity]',
* 'product.price.priceTaxExcluded': [
* 'product[price][price_tax_excluded]',
* 'product[shortcuts][price][price_tax_excluded]',
* ],
* };
*
* As you can see for priceTaxExcluded it is possible to assign multiple inputs to the same modelKey, thus
* any update in one of the inputs will update the model, and all these inputs are kept in sync.
*
* With the previous configuration this component would return an object that looks like this:
*
* {
* product: {
* stock: {
* // Mapped to product[stock][quantity] input
* quantity: 200,
* },
* price: {
* // Mapped to two inputs product[price][price_tax_excluded] and product[shortcuts][price][price_tax_excluded]
* priceTaxExcluded: 20.45,
* }
* }
* }
*/
export default class FormObjectMapper {
/**
* @param {jQuery} $form - Form element to attach the mapper to
* @param {Object} modelMapping - Structure mapping a model to form names
* @param {EventEmitter} eventEmitter
* @param {Object} [config] - Event names
* @param {Object} [config.updateModel] - Name of the event to listen to trigger a refresh of the model update
* @param {Object} [config.modelUpdated] - Name of the event emitted each time the model is updated
* @param {Object} [config.modelFieldUpdated] - Name of the event emitted each time a field is updated
* @return {Object}
*/
constructor($form, modelMapping, eventEmitter, config) {
this.$form = $form;
this.fullModelMapping = modelMapping;
this.eventEmitter = eventEmitter;
const inputConfig = config || {};
// This event is registered so when it is triggered it forces the form mapping and object update,
// it can be useful when some new inputs have been added in the DOM (or removed) so that the model
// acknowledges the update
this.updateModelEventName = inputConfig.updateModel || 'updateModel';
// This event is emitted each time the object is updated (from both input change and external event)
this.modelUpdatedEventName = inputConfig.modelUpdated || 'modelUpdated';
// This event is emitted each time an object field is updated (from both input change and external event)
this.modelFieldUpdatedEventName = inputConfig.modelFieldUpdated || 'modelFieldUpdated';
// Contains callbacks identified by model keys
this.watchedProperties = {};
this.initFormMapping();
this.updateFullObject();
this.watchUpdates();
return {
/**
* Returns the model mapped to the form (current live state)
*
* @returns {*|{}}
*/
getModel: () => this.model,
/**
* Returns all inputs associated to a model field.
*
* @param {string} modelKey
*
* @returns {undefined|jQuery}
*/
getInputsFor: (modelKey) => {
if (!Object.prototype.hasOwnProperty.call(this.fullModelMapping, modelKey)) {
return undefined;
}
const inputNames = this.fullModelMapping[modelKey];
// We must loop manually to keep the order in configuration, if we use jQuery multiple selectors the collection
// will be filled respecting the order in the DOM
const inputs = [];
const domForm = this.$form.get(0);
inputNames.forEach((inputName) => {
const inputsByName = domForm.querySelectorAll(`[name="${inputName}"]`);
if (inputsByName.length) {
inputsByName.forEach((input) => {
inputs.push(input);
});
}
});
return inputs.length ? $(inputs) : undefined;
},
/**
* Set a value to a field of the object based on the model key, the object itself is updated
* of course but the mapped inputs are also synced (all of them if multiple). Events are also
* triggered to indicate the object has been updated (the general and the individual field ones).
*
* @param {string} modelKey
* @param {*|{}} value
*/
set: (modelKey, value) => {
if (!Object.prototype.hasOwnProperty.call(this.modelMapping, modelKey) || value === this.getValue(modelKey)) {
return;
}
// First update the inputs then the model, so that the event is sent at last
this.updateInputValue(modelKey, value);
this.updateObjectByKey(modelKey, value);
this.eventEmitter.emit(this.modelUpdatedEventName, this.model);
},
/**
* Alternative to the event listening, you can watch a specific field of the model and assign a callback.
* When the specified model field is updated the event is still thrown but additionally any callback assigned
* to this specific value is also called, the parameter is the same event.
*
* @param {string} modelKey
* @param {function} callback
*/
watch: (modelKey, callback) => {
if (!Object.prototype.hasOwnProperty.call(this.watchedProperties, modelKey)) {
this.watchedProperties[modelKey] = [];
}
this.watchedProperties[modelKey].push(callback);
},
};
}
/**
* Get a field from the object based on the model key, you can even get a sub part of the whole model,
* this internal method is used by both get and set public methods.
*
* @param {string} modelKey
*
* @returns {*|{}|undefined} Returns any element from the model, undefined if not found
* @private
*/
getValue(modelKey) {
const modelKeys = modelKey.split('.');
return $.serializeJSON.deepGet(this.model, modelKeys);
}
/**
* Watches if changes happens from the form or via an event.
*
* @private
*/
watchUpdates() {
this.$form.on('keyup change dp.change', ':input', _.debounce(
(event) => this.inputUpdated(event),
350,
{maxWait: 1500},
));
this.eventEmitter.on(this.updateModelEventName, () => this.updateFullObject());
}
/**
* Triggered when a form input has been changed.
*
* @param {jQuery.Event} event
*
* @private
*/
inputUpdated(event) {
const target = event.currentTarget;
// All inputs changes are watched, but not all of them are part of the mapping so we ignore them
if (!Object.prototype.hasOwnProperty.call(this.formMapping, target.name)) {
return;
}
const updatedValue = $(target).val();
const updatedModelKey = this.formMapping[target.name];
// Update the mapped input fields
this.updateInputValue(updatedModelKey, updatedValue, target.name);
// Then update model and emit event
this.updateObjectByKey(updatedModelKey, updatedValue);
this.eventEmitter.emit(this.modelUpdatedEventName, this.model);
}
/**
* Update all the inputs mapped to a model key
*
* @param {string} modelKey
* @param {*|{}} value
* @param {string|undefined} sourceInputName Source of the change (no need to update it)
*
* @private
*/
updateInputValue(modelKey, value, sourceInputName = undefined) {
const modelInputs = this.fullModelMapping[modelKey];
// Update linked inputs (when there is more than one input associated to the model field)
if (Array.isArray(modelInputs)) {
modelInputs.forEach((inputName) => {
if (sourceInputName === inputName) {
return;
}
this.updateInputByName(inputName, value);
});
} else if (sourceInputName !== modelInputs) {
this.updateInputByName(modelInputs, value);
}
}
/**
* Update individual input based on its name
*
* @param {string} inputName
* @param {*|{}} value
*
* @private
*/
updateInputByName(inputName, value) {
const $input = $(`[name="${inputName}"]`, this.$form);
if (!$input.length) {
console.error(`Input with name ${inputName} is not present in form.`);
return;
}
// This check is important to avoid infinite loops, we don't use strict equality on purpose because it would result
// into a potential infinite loop if type don't match, which can easily happen with a number value and a text input.
// eslint-disable-next-line eqeqeq
if ($input.val() != value) {
$input.val(value);
if ($input.data('toggle') === 'select2') {
// This is required for select2, because only changing the val doesn't update the wrapping component
$input.trigger('change');
}
}
}
/**
* Serializes and updates the object based on form content and the mapping configuration, finally
* emit an event for external components that may need the update.
*
* This method is called when this component initializes or when triggered by an external event.
*
* @private
*/
updateFullObject() {
const serializedForm = this.$form.serializeJSON();
this.model = {};
Object.keys(this.modelMapping).forEach((modelKey) => {
const formMapping = this.modelMapping[modelKey];
const formKeys = $.serializeJSON.splitInputNameIntoKeysArray(formMapping);
const formValue = $.serializeJSON.deepGet(serializedForm, formKeys);
this.updateObjectByKey(modelKey, formValue);
});
this.eventEmitter.emit(this.modelUpdatedEventName, this.model);
}
/**
* Update a specific field of the object.
*
* @param {string} modelKey
* @param {*|{}} value
*
* @private
*/
updateObjectByKey(modelKey, value) {
const modelKeys = modelKey.split('.');
const previousValue = $.serializeJSON.deepGet(this.model, modelKeys);
// This check has two interests, there is no point in modifying a value or emit an event for a value that did not
// change, and it avoids infinite loops when the object field are co-dependent and need to be updated dynamically
// (ex: update price tax included when price tax excluded is updated and vice versa, without this check an infinite
// loop would happen)
if (previousValue === value) {
return;
}
$.serializeJSON.deepSet(this.model, modelKeys, value);
const updateEvent = {
object: this.model,
modelKey,
value,
previousValue,
};
this.eventEmitter.emit(this.modelFieldUpdatedEventName, updateEvent);
if (Object.prototype.hasOwnProperty.call(this.watchedProperties, modelKey)) {
const propertyWatchers = this.watchedProperties[modelKey];
propertyWatchers.forEach((callback) => {
callback(updateEvent);
});
}
}
/**
* Reverse the initial mapping Model->Form to the opposite Form->Model
* This simplifies the sync in when data updates.
*
* @private
*/
initFormMapping() {
// modelMapping is a light version of the fullModelMapping, it only contains one input name which is considered
// as the default one (when full object is updated, only the default input is used)
this.modelMapping = {};
// formMapping is the inverse of modelMapping for each input name it associated the model key, it is generated for
// performance and convenience, this allows to get mapping data faster in other functions
this.formMapping = {};
Object.keys(this.fullModelMapping).forEach((modelKey) => {
const formMapping = this.fullModelMapping[modelKey];
if (Array.isArray(formMapping)) {
formMapping.forEach((aliasFormMapping) => {
this.addFormMapping(aliasFormMapping, modelKey);
});
} else {
this.addFormMapping(formMapping, modelKey);
}
});
}
/**
* @param {string} formName
* @param {string} modelMapping
*
* @private
*/
addFormMapping(formName, modelMapping) {
if (Object.prototype.hasOwnProperty.call(this.formMapping, formName)) {
console.error(`The form element ${formName} is already mapped to ${this.formMapping[formName]}`);
return;
}
this.formMapping[formName] = modelMapping;
this.modelMapping[modelMapping] = formName;
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* Component responsible for displaying form popover errors with modified width which is calculated based on the
* form group width.
*/
$(() => {
// loads form popover instance
$('[data-toggle="form-popover-error"]').popover({
html: true,
content() {
return getErrorContent(this);
},
});
/**
* Recalculates popover position so it is always aligned horizontally and width is identical
* to the child elements of the form.
* @param {Object} event
*/
const repositionPopover = (event) => {
const $element = $(event.currentTarget);
const $formGroup = $element.closest('.form-group');
const $invalidFeedbackContainer = $formGroup.find('.invalid-feedback-container');
const $errorPopover = $formGroup.find('.form-popover-error');
const localeVisibleElementWidth = $invalidFeedbackContainer.width();
$errorPopover.css('width', localeVisibleElementWidth);
const horizontalDifference = getHorizontalDifference($invalidFeedbackContainer, $errorPopover);
$errorPopover.css('left', `${horizontalDifference}px`);
};
/**
* gets horizontal difference which helps to align popover horizontally.
* @param {jQuery} $invalidFeedbackContainer
* @param {jQuery} $errorPopover
* @returns {number}
*/
const getHorizontalDifference = ($invalidFeedbackContainer, $errorPopover) => {
const inputHorizontalPosition = $invalidFeedbackContainer.offset().left;
const popoverHorizontalPosition = $errorPopover.offset().left;
return inputHorizontalPosition - popoverHorizontalPosition;
};
/**
* Gets popover error content pre-fetched in html. It used unique selector to identify which one content to render.
*
* @param popoverTriggerElement
* @returns {jQuery}
*/
const getErrorContent = (popoverTriggerElement) => {
const popoverTriggerId = $(popoverTriggerElement).data('id');
return $(`.js-popover-error-content[data-id="${popoverTriggerId}"]`).html();
};
// registers the event which displays the popover
$(document).on('shown.bs.popover', '[data-toggle="form-popover-error"]', (event) => repositionPopover(event));
});

View File

@@ -0,0 +1,44 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
export default class MultistoreConfigField {
constructor() {
this.updateMultistoreFieldOnChange();
}
updateMultistoreFieldOnChange() {
$(document).on('change', '.multistore-checkbox', function () {
const input = $(this).closest('.form-group').find(':input:not(.multistore-checkbox)');
const inputContainer = $(this).closest('.form-group').find('.input-container');
const labelContainer = $(this).closest('.form-group').find('.form-control-label');
const isChecked = $(this).is(':checked');
inputContainer.toggleClass('disabled', !isChecked);
labelContainer.toggleClass('disabled', !isChecked);
input.prop('disabled', !isChecked);
});
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* Responsible for opening another page with specified url.
* For example used in 'Save and preview' cms page create/edit actions.
*
* Usage: In selector element attr 'data-preview-url' provide page url.
* The page will be opened once provided 'open_preview' parameter in query url
*/
export default class PreviewOpener {
constructor(previewUrlSelector) {
this.previewUrl = $(previewUrlSelector).data('preview-url');
this.open();
return {};
}
/**
* Opens new page of provided url
*
* @private
*/
open() {
const urlParams = new URLSearchParams(window.location.search);
if (this.previewUrl && urlParams.has('open_preview')) {
window.open(this.previewUrl, '_blank');
}
}
}

View File

@@ -0,0 +1,228 @@
/**
* 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 <contact@prestashop.com>
* @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';
const {$} = window;
/**
* 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 {
/**
* @param {String} wrapperSelector
* @param {Function} callback
*
* @returns {{}}
*/
constructor(wrapperSelector, callback) {
this.inputSelector = '.submittable-input';
this.callback = callback;
this.wrapperSelector = wrapperSelector;
this.buttonSelector = '.check-button';
this.init();
return {};
}
/**
* @private
*/
init() {
const inputs = `${this.wrapperSelector} ${this.inputSelector}`;
const that = this;
$(document).on('focus', inputs, (e) => {
this.refreshButtonState(e.currentTarget, true);
});
$(document).on('input blur', inputs, (e) => {
this.refreshButtonState(e.currentTarget);
});
$(document).on(
'click',
`${this.wrapperSelector} ${this.buttonSelector}`,
function () {
that.submitInput(this);
},
);
$(document).on('keyup', inputs, (e) => {
if (e.keyCode === 13) {
e.preventDefault();
const button = this.findButton(e.target);
this.submitInput(button);
}
});
}
/**
* @private
*/
submitInput(button) {
const input = this.findInput(button);
this.toggleLoading(button, true);
this.callback(input)
.then((response) => {
$(input).data('initial-value', input.value);
this.toggleButtonVisibility(button, false);
if (response.message) {
showGrowl('success', response.message);
}
this.toggleLoading(button, false);
})
.catch((error) => {
this.toggleError(button, true);
this.toggleButtonVisibility(button, false);
this.toggleLoading(button, false);
if (typeof error.responseJSON.errors === 'undefined') {
return;
}
const messages = error.responseJSON.errors;
Object.keys(messages).forEach((key) => {
showGrowl('error', messages[key]);
});
});
}
/**
* @param {HTMLElement} input
* @param {Boolean|null} visible
*
* @private
*/
refreshButtonState(input, visible = null) {
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);
}
}
/**
* @param {HTMLElement} button
* @param {Boolean} active
*
* @private
*/
toggleButtonActivity(button, active) {
$(button).toggleClass('active', active);
}
/**
* @param {HTMLElement} button
* @param {Boolean} visible
*
* @private
*/
toggleButtonVisibility(button, visible) {
$(button).toggleClass('d-none', !visible);
}
/**
* @param {HTMLElement} button
* @param {Boolean} visible
*
* @private
*/
toggleLoading(button, loading) {
if (loading) {
$(button).html('<span class="spinner-border spinner-border-sm"></span>');
} else {
$(button).html('<i class="material-icons">check</i>');
}
}
/**
* @param {HTMLElement} button
* @param {Boolean} visible
*
* @private
*/
toggleError(button, error) {
const input = this.findInput(button);
$(input).toggleClass('is-invalid', error);
}
/**
* @param {HTMLElement} input
*
* @returns {HTMLElement}
*
* @private
*/
findButton(input) {
return $(input)
.closest(this.wrapperSelector)
.find(this.buttonSelector)[0];
}
/**
* @param {HTMLElement} domElement
*
* @returns {HTMLElement}
*
* @private
*/
findInput(domElement) {
return $(domElement)
.closest(this.wrapperSelector)
.find(this.inputSelector)[0];
}
/**
* @param {HTMLElement} input
*
* @returns {Boolean}
*
* @private
*/
inputValueChanged(input) {
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;
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* TextWithLengthCounter handles input with length counter UI.
*
* Usage:
*
* There must be an element that wraps both input & counter display with ".js-text-with-length-counter" class.
* Counter display must have ".js-countable-text-display" class and input must have ".js-countable-text-input" class.
* Text input must have "data-max-length" attribute.
*
* <div class="js-text-with-length-counter">
* <span class="js-countable-text"></span>
* <input class="js-countable-input" data-max-length="255">
* </div>
*
* In Javascript you must enable this component:
*
* new TextWithLengthCounter();
*/
export default class TextWithLengthCounter {
constructor() {
this.wrapperSelector = '.js-text-with-length-counter';
this.textSelector = '.js-countable-text';
this.inputSelector = '.js-countable-input';
$(document).on('input', `${this.wrapperSelector} ${this.inputSelector}`, (e) => {
const $input = $(e.currentTarget);
const remainingLength = $input.data('max-length') - $input.val().length;
$input.closest(this.wrapperSelector).find(this.textSelector).text(remainingLength);
});
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* This component is implemented to work with TextWithRecommendedLengthType,
* but can be used as standalone component as well.
*
* Usage:
*
* Define your HTML with input and counter. Example:
*
* <input id="myInput"
* class="js-recommended-length-input"
* data-recommended-length-counter="#myInput_recommended_length_counter"
* >
*
* <div id"myInput_recommended_length_counter">
* <span class="js-current-length">0</span> of 70 characters used (recommended)
* </div>
*
* NOTE: You must use exactly the same Classes, but IDs can be different!
*
* Then enable component in JavaScript:
*
* new TextWithRecommendedLengthCounter();
*/
export default class TextWithRecommendedLengthCounter {
constructor() {
$(document).on('input', '.js-recommended-length-input', (event) => {
const $input = $(event.currentTarget);
$($input.data('recommended-length-counter')).find('.js-current-length').text($input.val().length);
});
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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 <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
const {$} = window;
/**
* Component responsible for filtering select values by language selected.
*/
export default class TranslatableChoice {
constructor() {
// registers the event which displays the popover
$(document).on('change', 'select.translatable_choice_language', (event) => {
this.filterSelect(event);
});
$('select.translatable_choice_language').trigger('change');
}
filterSelect(event) {
const $element = $(event.currentTarget);
const $formGroup = $element.closest('.form-group');
const language = $element.find('option:selected').val();
// show all the languages selects
$formGroup.find(`select.translatable_choice[data-language="${language}"]`).parent().show();
const $selects = $formGroup.find('select.translatable_choice');
// Hide all the selects not corresponding to the language selected
$selects.not(`select.translatable_choice[data-language="${language}"]`).each((index, item) => {
$(item).parent().hide();
});
// Bind choice selection to fill the hidden input
this.bindValueSelection($selects);
}
bindValueSelection($selects) {
$selects.each((index, element) => {
$(element).on('change', (event) => {
const $select = $(event.currentTarget);
const selectId = $select.attr('id');
$(`#${selectId}_value`).val($select.find('option:selected').val());
});
});
}
}