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,102 @@
/**
* 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 connecting to addons marketplace.
* Makes an addons connect request to the server, displays error messages if it fails.
*/
export default class AddonsConnector {
constructor(
addonsConnectFormSelector,
loadingSpinnerSelector,
) {
this.addonsConnectFormSelector = addonsConnectFormSelector;
this.$loadingSpinner = $(loadingSpinnerSelector);
this.initEvents();
return {};
}
/**
* Initialize events related to connection to addons.
*
* @private
*/
initEvents() {
$('body').on(
'submit',
this.addonsConnectFormSelector,
(event) => {
const $form = $(event.currentTarget);
event.preventDefault();
event.stopPropagation();
this.connect($form.attr('action'), $form.serialize());
},
);
}
/**
* Do a POST request to connect to addons.
*
* @param {String} addonsConnectUrl
* @param {Object} formData
*
* @private
*/
connect(addonsConnectUrl, formData) {
$.ajax({
method: 'POST',
url: addonsConnectUrl,
dataType: 'json',
data: formData,
beforeSend: () => {
this.$loadingSpinner.show();
$('button.btn[type="submit"]', this.addonsConnectFormSelector).hide();
},
}).then((response) => {
if (response.success === 1) {
window.location.reload();
} else {
$.growl.error({
message: response.message,
});
this.$loadingSpinner.hide();
$('button.btn[type="submit"]', this.addonsConnectFormSelector).fadeIn();
}
}, () => {
$.growl.error({
message: $(this.addonsConnectFormSelector).data('error-message'),
});
this.$loadingSpinner.hide();
$('button.btn[type="submit"]', this.addonsConnectFormSelector).show();
});
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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)
*/
/**
* This component is an overlay of typeahead it allows to have a single config input (since
* typeahead weirdly uses two different configs). It also provides some default rendering
* functions which are, of course, overridable.
*/
export default class AutoCompleteSearch {
constructor($searchInput, config) {
this.$searchInput = $searchInput;
this.searchInputId = this.$searchInput.prop('id');
const inputConfig = config || {};
// Merge default and input config
this.config = {
minLength: 2,
highlight: true,
cache: false,
hint: false,
...inputConfig,
};
// Merge default and input dataSetConfig
this.dataSetConfig = {
display: 'name', // Which field of the object from the list is used for display (can be a string or a callback)
value: 'id', // Which field of the object from the list is used for value (can be a string or a callback)
limit: 20, // Limit the number of displayed suggestion
dataLimit: 0, // How many elements can be selected max
/* eslint-disable-next-line no-unused-vars */
onSelect(selectedItem, event) {
return true;
},
/* eslint-disable-next-line no-unused-vars */
onClose(event) {
},
...inputConfig,
};
// Merging object works fine on one level, but on two it erases sub elements even if not present, so
// we handle templates separately, these are the default rendering functions which can be overridden
const defaultTemplates = {
// Be careful that your rendering function must return HTML node not pure text so always include the
// content in a div at least
suggestion: (item) => {
let displaySuggestion = item;
if (typeof this.dataSetConfig.display === 'function') {
this.dataSetConfig.display(item);
} else if (Object.prototype.hasOwnProperty.call(item, this.dataSetConfig.display)) {
displaySuggestion = item[this.dataSetConfig.display];
}
return `<div class="px-2">${displaySuggestion}</div>`;
},
pending(query) {
return `<div class="px-2">Searching for "${query.query}"</div>`;
},
notFound(query) {
return `<div class="px-2">No results found for "${query.query}"</div>`;
},
};
if (Object.prototype.hasOwnProperty.call(inputConfig, 'templates')) {
this.dataSetConfig.templates = {...defaultTemplates, ...inputConfig.templates};
} else {
this.dataSetConfig.templates = defaultTemplates;
}
this.buildTypeahead();
}
/**
* Build the typeahead component based on provided configuration.
*/
buildTypeahead() {
this.$searchInput.typeahead(this.config, this.dataSetConfig)
.bind('typeahead:select', (e, selectedItem) => this.config.onSelect(selectedItem, e, this.$searchInput))
.bind('typeahead:close', (e) => {
this.config.onClose(e, this.$searchInput);
});
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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';
import Bloodhound from 'typeahead.js';
/**
* This comes from Bloodhound it allows to create tokenizer based on multiple fields from an object.
*
* @param tokenizer
* @returns {function(*=, ...[*]=): function(*): *[]}
*/
function getObjTokenizer(tokenizer) {
return function setKey(keys, ...args) {
const tokenizerKeys = _.isArray(keys) ? keys : [].slice.call(args, 0);
return function tokenize(val) {
let tokens = [];
tokenizerKeys.forEach((key) => {
tokens = tokens.concat(tokenizer(_.toString(val[key])));
});
return tokens;
};
};
}
/**
* Split the word into multiple tokens ok different sizes, thus allowing to search into parts of the words,
* the min length of a token is two letters though (maybe it could be configurable in the future)
*
* @param {string} val
*
* @return {array}
*/
export const letters = (val) => {
const tokens = Bloodhound.tokenizers.nonword(val);
tokens.forEach((token) => {
let i = 0;
while (i + 1 < token.length) {
tokens.push(token.substr(i, token.length));
i += 1;
}
});
return tokens;
};
export default {
letters,
obj: {
letters: getObjTokenizer(letters),
},
};

View File

@@ -0,0 +1,131 @@
/**
* 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;
/**
* Generates a password and informs about it's strength.
* You can pass a password input to watch the password strength and display feedback messages.
* You can also generate a random password into an input.
*/
export default class ChangePasswordHandler {
constructor(passwordStrengthFeedbackContainerSelector, options = {}) {
// Minimum length of the generated password.
this.minLength = options.minLength || 8;
// Feedback container holds messages representing password strength.
this.$feedbackContainer = $(passwordStrengthFeedbackContainerSelector);
return {
watchPasswordStrength: ($input) => this.watchPasswordStrength($input),
generatePassword: ($input) => this.generatePassword($input),
};
}
/**
* Watch password, which is entered in the input, strength and inform about it.
*
* @param {jQuery} $input the input to watch.
*/
watchPasswordStrength($input) {
$.passy.requirements.length.min = this.minLength;
$.passy.requirements.characters = 'DIGIT';
$input.each((index, element) => {
const $outputContainer = $('<span>');
$outputContainer.insertAfter($(element));
$(element).passy((strength, valid) => {
this.displayFeedback($outputContainer, strength, valid);
});
});
}
/**
* Generates a password and fills it to given input.
*
* @param {jQuery} $input the input to fill the password into.
*/
generatePassword($input) {
$input.passy('generate', this.minLength);
}
/**
* Display feedback about password's strength.
*
* @param {jQuery} $outputContainer a container to put feedback output into.
* @param {number} passwordStrength
* @param {boolean} isPasswordValid
*
* @private
*/
displayFeedback($outputContainer, passwordStrength, isPasswordValid) {
const feedback = this.getPasswordStrengthFeedback(passwordStrength);
$outputContainer.text(feedback.message);
$outputContainer.removeClass('text-danger text-warning text-success');
$outputContainer.addClass(feedback.elementClass);
$outputContainer.toggleClass('d-none', !isPasswordValid);
}
/**
* Get feedback that describes given password strength.
* Response contains text message and element class.
*
* @param {number} strength
*
* @private
*/
getPasswordStrengthFeedback(strength) {
switch (strength) {
case $.passy.strength.LOW:
return {
message: this.$feedbackContainer.find('.strength-low').text(),
elementClass: 'text-danger',
};
case $.passy.strength.MEDIUM:
return {
message: this.$feedbackContainer.find('.strength-medium').text(),
elementClass: 'text-warning',
};
case $.passy.strength.HIGH:
return {
message: this.$feedbackContainer.find('.strength-high').text(),
elementClass: 'text-success',
};
case $.passy.strength.EXTREME:
return {
message: this.$feedbackContainer.find('.strength-extreme').text(),
elementClass: 'text-success',
};
default:
throw new Error('Invalid password strength indicator.');
}
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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;
/**
* ChoiceTable is responsible for managing common actions in choice table form type
*/
export default class ChoiceTable {
/**
* Init constructor
*/
constructor() {
$(document).on('change', '.js-choice-table-select-all', (e) => {
this.handleSelectAll(e);
});
}
/**
* Check/uncheck all boxes in table
*
* @param {Event} event
*/
handleSelectAll(event) {
const $selectAllCheckboxes = $(event.target);
const isSelectAllChecked = $selectAllCheckboxes.is(':checked');
$selectAllCheckboxes.closest('table').find('tbody input:checkbox').prop('checked', isSelectAllChecked);
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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)
*/
export default {
multistoreDropdown: {
searchInput: '.js-multistore-dropdown-search',
scrollbar: '.js-multistore-scrollbar',
},
multistoreHeader: {
modal: '.js-multishop-modal',
headerButton: '.js-header-multishop-open-modal',
searchInput: '.js-multishop-modal-search',
jsScrollbar: '.js-multishop-scrollbar',
setContextUrl: (location, urlLetter, itemId) => {
const setContextParameter = `setShopContext=${urlLetter}-${itemId}`;
const url = new URL(location);
if (url.search === '') {
url.search = `?${setContextParameter}`;
} else {
url.search += `&${setContextParameter}`;
}
return url.toString();
},
},
};

View File

@@ -0,0 +1,77 @@
/**
* 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;
/**
* Toggle DNI input requirement on country selection
*
* Usage:
*
* <!-- Country select options must have need_dni attribute when needed -->
* <select name="id_country" id="id_country" states-url="path/to/states/api">
* ...
* <option value="6" need_dni="1">Spain</value>
* ...
* </select>
*
* In JS:
*
* new CountryDniRequiredToggler('#id_country', '#id_country_dni', 'label[for="id_country_dni"]');
*/
export default class CountryDniRequiredToggler {
constructor(countryInputSelector, countryDniInput, countryDniInputLabel) {
this.$countryDniInput = $(countryDniInput);
this.$countryDniInputLabel = $(countryDniInputLabel);
this.$countryInput = $(countryInputSelector);
this.countryInputSelectedSelector = `${countryInputSelector}>option:selected`;
this.countryDniInputLabelDangerSelector = `${countryDniInputLabel}>span.text-danger`;
// If field is required regardless of the country
// keep it required
if (this.$countryDniInput.attr('required')) {
return;
}
this.$countryInput.on('change', () => this.toggle());
// toggle on page load
this.toggle();
}
/**
* Toggles DNI input required
*
* @private
*/
toggle() {
$(this.countryDniInputLabelDangerSelector).remove();
this.$countryDniInput.prop('required', false);
if (parseInt($(this.countryInputSelectedSelector).attr('need_dni'), 10) === 1) {
this.$countryDniInput.prop('required', true);
this.$countryDniInputLabel.prepend($('<span class="text-danger">*</span>'));
}
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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;
/**
* Toggle Postcode input requirement on country selection
*
* Usage:
*
* <!-- Country select options must have need_postcode attribute when needed -->
* <select name="id_country" id="id_country" states-url="path/to/states/api">
* ...
* <option value="6" need_postcode="1">Spain</value>
* ...
* </select>
*
* In JS:
*
* new CountryPostcodeRequiredToggler('#id_country', '#id_country_postcode', 'label[for="id_country_postcode"]');
*/
export default class CountryPostcodeRequiredToggler {
constructor(countryInputSelector, countryPostcodeInput, countryPostcodeInputLabel) {
this.$countryPostcodeInput = $(countryPostcodeInput);
this.$countryPostcodeInputLabel = $(countryPostcodeInputLabel);
this.$countryInput = $(countryInputSelector);
this.countryInputSelectedSelector = `${countryInputSelector}>option:selected`;
this.countryPostcodeInputLabelDangerSelector = `${countryPostcodeInputLabel}>span.text-danger`;
// If field is required regardless of the country
// keep it required
if (this.$countryPostcodeInput.attr('required')) {
return;
}
this.$countryInput.on('change', () => this.toggle());
// toggle on page load
this.toggle();
}
/**
* Toggles Postcode input required
*
* @private
*/
toggle() {
$(this.countryPostcodeInputLabelDangerSelector).remove();
this.$countryPostcodeInput.prop('required', false);
if (parseInt($(this.countryInputSelectedSelector).attr('need_postcode'), 10) === 1) {
this.$countryPostcodeInput.prop('required', true);
this.$countryPostcodeInputLabel.prepend($('<span class="text-danger">*</span>'));
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* 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;
/**
* Displays, fills or hides State selection block depending on selected country.
*
* Usage:
*
* <!-- Country select must have unique identifier & url for states API -->
* <select name="id_country" id="id_country" states-url="path/to/states/api">
* ...
* </select>
*
* <!-- If selected country does not have states, then this block will be hidden -->
* <div class="js-state-selection-block">
* <select name="id_state">
* ...
* </select>
* </div>
*
* In JS:
*
* new CountryStateSelectionToggler('#id_country', '#id_state', '.js-state-selection-block');
*/
export default class CountryStateSelectionToggler {
constructor(countryInputSelector, countryStateSelector, stateSelectionBlockSelector) {
this.$stateSelectionBlock = $(stateSelectionBlockSelector);
this.$countryStateSelector = $(countryStateSelector);
this.$countryInput = $(countryInputSelector);
this.$countryInput.on('change', () => this.change());
return {};
}
/**
* Change State selection
*
* @private
*/
change() {
const countryId = this.$countryInput.val();
if (countryId === '') {
return;
}
$.get({
url: this.$countryInput.data('states-url'),
dataType: 'json',
data: {
id_country: countryId,
},
})
.then((response) => {
this.$countryStateSelector.empty();
Object.keys(response.states).forEach((value) => {
this.$countryStateSelector.append(
$('<option></option>')
.attr('value', response.states[value])
.text(value),
);
});
this.toggle();
})
.catch((response) => {
if (typeof response.responseJSON !== 'undefined') {
window.showErrorMessage(response.responseJSON.message);
}
});
}
toggle() {
this.$stateSelectionBlock.toggleClass('d-none', !this.$countryStateSelector.find('option').length > 0);
}
}

View File

@@ -0,0 +1,259 @@
/**
* 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 AutoCompleteSearch from '@components/auto-complete-search';
import Bloodhound from 'typeahead.js';
/**
* This component is used to search and select an entity, it is uses the AutoSearchComplete
* component which displays a list of suggestion based on an API returned response. Then when
* an element is selected it is added to the selection container and hidden inputs are created to
* send an array of entity IDs in the form request.
*
* This component is used with TypeaheadType forms, and is tightly linked to the content of this
* twig file src/PrestaShopBundle/Resources/views/Admin/TwigTemplateForm/typeahead.html.twig
*
* @todo: the component relies on this TypeaheadType because it was the historical type but it would be worth
* creating a new clean form type with better templating (the tplcollection brings nearly no value as is)
*/
export default class EntitySearchInput {
constructor($entitySearchInput, options) {
this.$entitySearchInput = $entitySearchInput;
this.entitySearchInputId = this.$entitySearchInput.prop('id');
this.$autoCompleteSearchContainer = this.$entitySearchInput.closest('.autocomplete-search');
this.$selectionContainer = $(`#${this.entitySearchInputId}-data`);
this.searchInputFullName = this.$autoCompleteSearchContainer.data('fullname');
const inputOptions = options || {};
this.options = {
value: 'id',
dataLimit: 1,
...inputOptions,
};
this.buildRemoteSource();
this.buildAutoCompleteSearch();
}
/**
* Change the remote url of the endpoint that returns suggestions.
*
* @param remoteUrl {string}
*/
setRemoteUrl(remoteUrl) {
this.entityRemoteSource.remote.url = remoteUrl;
}
/**
* Force selected values, the input is an array of object that must match the format from
* the API if you want the selected entities to be correctly displayed.
*
* @param values {array}
*/
setValue(values) {
this.clearSelectedItems();
if (!values || values.length <= 0) {
return;
}
values.each((value) => {
this.appendSelectedItem(value);
});
}
/**
* Build the AutoCompleteSearch component
*/
buildAutoCompleteSearch() {
const autoSearchConfig = {
source: this.entityRemoteSource,
dataLimit: this.options.dataLimit,
templates: {
suggestion: (entity) => {
let entityImage;
if (Object.prototype.hasOwnProperty.call(entity, 'image')) {
entityImage = `<img src="${entity.image}" /> `;
}
return `<div class="search-suggestion">${entityImage}${entity.name}</div>`;
},
},
onClose: (event) => {
this.onSelectionClose(event);
},
/* eslint-disable-next-line no-unused-vars */
onSelect: (selectedItem, event) => {
// When limit is one we cannot select additional elements so we replace them instead
if (this.options.dataLimit === 1) {
return this.replaceSelectedItem(selectedItem);
}
return this.appendSelectedItem(selectedItem);
},
};
// Can be used to format value depending on selected item
if (this.options.value !== undefined) {
autoSearchConfig.value = this.options.value;
}
this.autoSearch = new AutoCompleteSearch(this.$entitySearchInput, autoSearchConfig);
}
/**
* Build the Bloodhound remote source which will call the API. The placeholder to
* inject the query search parameter is __QUERY__ (@todo: could be configurable)
*
* @returns {Bloodhound}
*/
buildRemoteSource() {
const sourceConfig = {
mappingValue: this.$autoCompleteSearchContainer.data('mappingvalue'),
remoteUrl: this.$autoCompleteSearchContainer.data('remoteurl'),
};
this.entityRemoteSource = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify(obj) {
return obj[sourceConfig.mappingValue];
},
remote: {
url: sourceConfig.remoteUrl,
cache: false,
wildcard: '__QUERY__',
transform(response) {
if (!response) {
return [];
}
return response;
},
},
});
}
/**
* When an item is selected we empty the input search, since the selected data is stored in hidden inputs anyway
*
* @param event
*/
onSelectionClose(event) {
$(event.target).val('');
}
/**
* Removes selected items.
*/
clearSelectedItems() {
const formIdItem = $('li', this.$selectionContainer);
formIdItem.remove();
}
/**
* When the component is configured to have only one selected element on each selection
* the previous selection is removed and then replaced.
*
* @param selectedItem {Object}
* @returns {boolean}
*/
replaceSelectedItem(selectedItem) {
this.clearSelectedItems();
this.addSelectedContentToContainer(selectedItem);
return true;
}
/**
* When the component is configured to have more than one selected item on each selection
* the item is added to the list.
*
* @param selectedItem {Object}
* @returns {boolean}
*/
appendSelectedItem(selectedItem) {
// If collection length is up to limit, return
const formIdItem = $('li', this.$selectionContainer);
if (this.options.dataLimit !== 0 && formIdItem.length >= this.options.dataLimit) {
return false;
}
this.addSelectedContentToContainer(selectedItem);
return true;
}
/**
* Add the selected content to the selection container, the HTML is generated based on the render function
* then a hidden input is automatically added inside it, and finally the rendered selection is added to the list.
*
* @param selectedItem {Object}
*/
addSelectedContentToContainer(selectedItem) {
let value;
if (typeof this.options.value === 'function') {
value = this.options.value(selectedItem);
} else {
value = selectedItem[this.options.value];
}
const selectedHtml = this.renderSelected(selectedItem);
// Hidden input is added into the selected li
const $selectedNode = $(selectedHtml);
const $hiddenInput = $(`<input type="hidden" name="${this.searchInputFullName}[data][]" value="${value}" />`);
$selectedNode.append($hiddenInput);
// Then the li is added to the list
this.$selectionContainer.append($selectedNode);
// Trigger the change so that listeners detect the form data has been modified
$hiddenInput.trigger('change');
}
/**
* Render the selected element, this will be appended in the selection list (ul),
* no need to include the hidden input as it is automatically handled in addSelectedContentToContainer
*
* @param entity {Object}
*
* @returns {string}
*/
renderSelected(entity) {
// @todo: the tplcollection idea is not bad but it only contains a span for now, to fo to the end of this idea
// it should contain the whole div (with media-left media-body and all)
const $templateContainer = $(`#tplcollection-${this.entitySearchInputId}`);
const innerTemplateHtml = $templateContainer
.html()
.replace('%s', entity.name);
return `<li class="media">
<div class="media-left">
<img class="media-object image" src="${entity.image}" />
</div>
<div class="media-body media-middle">
${innerTemplateHtml}
</div>
</li>`;
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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 EventEmitterClass from 'events';
/**
* We instanciate one EventEmitter (restricted via a const) so that every components
* register/dispatch on the same one and can communicate with each other.
*/
export const EventEmitter = new EventEmitterClass();
export default EventEmitter;

View File

@@ -0,0 +1,102 @@
/**
* 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 which allows submitting very simple forms without having to use <form> element.
*
* Useful when performing actions on resource where URL contains all needed data.
* For example, to toggle category status via "POST /categories/2/toggle-status)"
* or delete cover image via "POST /categories/2/delete-cover-image".
*
* Usage example in template:
*
* <button class="js-form-submit-btn"
* data-form-submit-url="/my-custom-url" // (required) URL to which form will be submitted
* data-method="GET|POST|DELETE|PATCH" // (optional) specify the verb to use for the request.
* // POST is taken by default if not value is set
* data-form-csrf-token="my-generated-csrf-token" // (optional) to increase security
* data-form-confirm-message="Are you sure?" // (optional) to confirm action before submit
* type="button" // make sure its simple button
* // so we can avoid submitting actual form
* // when our button is defined inside form
* >
* Click me to submit form
* </button>
*
* In page specific JS you have to enable this feature:
*
* new FormSubmitButton();
*/
export default class FormSubmitButton {
constructor() {
$(document).on('click', '.js-form-submit-btn', function (event) {
event.preventDefault();
const $btn = $(this);
if ($btn.data('form-confirm-message') && window.confirm($btn.data('form-confirm-message')) === false) {
return;
}
let method = 'POST';
let addInput = null;
if ($btn.data('method')) {
const btnMethod = $btn.data('method');
const isGetOrPostMethod = ['GET', 'POST'].includes(btnMethod);
method = isGetOrPostMethod ? btnMethod : 'POST';
if (!isGetOrPostMethod) {
addInput = $('<input>', {
type: '_hidden',
name: '_method',
value: btnMethod,
});
}
}
const $form = $('<form>', {
action: $btn.data('form-submit-url'),
method,
});
if (addInput) {
$form.append(addInput);
}
if ($btn.data('form-csrf-token')) {
$form.append($('<input>', {
type: '_hidden',
name: '_csrf_token',
value: $btn.data('form-csrf-token'),
}));
}
$form.appendTo('body').submit();
});
}
}

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());
});
});
}
}

View File

@@ -0,0 +1,101 @@
/**
* 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)
*/
/**
* Generates random values for inputs.
*
* Usage:
*
* There should be a button in HTML with 2 required data-* properties:
* 1. data-target-input-id - input id for which value should be generated
* 2. data-generated-value-size -
*
* Example button: <button class="js-generator-btn"
* data-target-input-id="my-input-id"
* data-generated-value-length="16"
* >
* Generate!
* </button>
*
* In JavaScript you have to enable this functionality using GeneratableInput component like so:
*
* const generateableInput = new GeneratableInput();
* generateableInput.attachOn('.js-generator-btn'); // every time our button is clicked
* // it will generate random value of 16 characters
* // for input with id of "my-input-id"
*
* You can attach as many different buttons as you like using "attachOn()" function
* as long as 2 required data-* attributes are present at each button.
*/
export default class GeneratableInput {
constructor() {
return {
attachOn: (btnSelector) => this.attachOn(btnSelector),
};
}
/**
* Attaches event listener on button than can generate value
*
* @param {String} generatorBtnSelector
*
* @private
*/
attachOn(generatorBtnSelector) {
const generatorBtn = document.querySelector(generatorBtnSelector);
if (generatorBtn !== null) {
generatorBtn.addEventListener('click', (event) => {
const {attributes} = event.currentTarget;
const targetInputId = attributes.getNamedItem('data-target-input-id').value;
const generatedValueLength = parseInt(attributes.getNamedItem('data-generated-value-length').value, 10);
const targetInput = document.querySelector(`#${targetInputId}`);
targetInput.value = this.generateValue(generatedValueLength);
});
}
}
/**
* Generates random value for input
*
* @param {Number} length
*
* @returns {string}
*
* @private
*/
generateValue(length) {
const chars = '123456789ABCDEFGHIJKLMNPQRSTUVWXYZ';
let generatedValue = '';
for (let i = 1; i <= length; i += 1) {
generatedValue += chars.charAt(Math.floor(Math.random() * chars.length));
}
return generatedValue;
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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;
/**
* Class DeleteCategoriesBulkActionExtension handles submitting of row action
*/
export default class DeleteCategoriesBulkActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-delete-categories-bulk-action', (event) => {
event.preventDefault();
const submitUrl = $(event.currentTarget).data('categories-delete-url');
const $deleteCategoriesModal = $(`#${grid.getId()}_grid_delete_categories_modal`);
$deleteCategoriesModal.modal('show');
$deleteCategoriesModal.on('click', '.js-submit-delete-categories', () => {
const $checkboxes = grid.getContainer().find('.js-bulk-action-checkbox:checked');
const $categoriesToDeleteInputBlock = $('#delete_categories_categories_to_delete');
$checkboxes.each((i, element) => {
const $checkbox = $(element);
const categoryInput = $categoriesToDeleteInputBlock
.data('prototype')
.replace(/__name__/g, $checkbox.val());
const $input = $($.parseHTML(categoryInput)[0]);
$input.val($checkbox.val());
$categoriesToDeleteInputBlock.append($input);
});
const $form = $deleteCategoriesModal.find('form');
$form.attr('action', submitUrl);
$form.submit();
});
});
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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 bulk delete for "Customers" grid.
*/
export default class DeleteCustomersBulkActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-delete-customers-bulk-action', (event) => {
event.preventDefault();
const submitUrl = $(event.currentTarget).data('customers-delete-url');
const $modal = $(`#${grid.getId()}_grid_delete_customers_modal`);
$modal.modal('show');
$modal.on('click', '.js-submit-delete-customers', () => {
const $selectedCustomerCheckboxes = grid.getContainer().find('.js-bulk-action-checkbox:checked');
$selectedCustomerCheckboxes.each((i, checkbox) => {
const $input = $(checkbox);
this.addCustomerToDeleteCollectionInput($input.val());
});
const $form = $modal.find('form');
$form.attr('action', submitUrl);
$form.submit();
});
});
}
/**
* Create input with customer id and add it to delete collection input
*
* @private
*/
addCustomerToDeleteCollectionInput(customerId) {
const $customersInput = $('#delete_customers_customers_to_delete');
const customerInput = $customersInput
.data('prototype')
.replace(/__name__/g, customerId);
const $item = $($.parseHTML(customerInput)[0]);
$item.val(customerId);
$customersInput.append($item);
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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;
/**
* Class CategoryDeleteRowActionExtension handles submitting of row action
*/
export default class DeleteCategoryRowActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-delete-category-row-action', (event) => {
event.preventDefault();
const $deleteCategoriesModal = $(`#${grid.getId()}_grid_delete_categories_modal`);
$deleteCategoriesModal.modal('show');
$deleteCategoriesModal.on('click', '.js-submit-delete-categories', () => {
const $button = $(event.currentTarget);
const categoryId = $button.data('category-id');
const $categoriesToDeleteInputBlock = $('#delete_categories_categories_to_delete');
const categoryInput = $categoriesToDeleteInputBlock
.data('prototype')
.replace(/__name__/g, $categoriesToDeleteInputBlock.children().length);
const $item = $($.parseHTML(categoryInput)[0]);
$item.val(categoryId);
$categoriesToDeleteInputBlock.append($item);
const $form = $deleteCategoriesModal.find('form');
$form.attr('action', $button.data('category-delete-url'));
$form.submit();
});
});
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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;
/**
* Class DeleteCustomerRowActionExtension handles submitting of row action
*/
export default class DeleteCustomerRowActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-delete-customer-row-action', (event) => {
event.preventDefault();
const $deleteCustomersModal = $(`#${grid.getId()}_grid_delete_customers_modal`);
$deleteCustomersModal.modal('show');
$deleteCustomersModal.on('click', '.js-submit-delete-customers', () => {
const $button = $(event.currentTarget);
const customerId = $button.data('customer-id');
this.addCustomerInput(customerId);
const $form = $deleteCustomersModal.find('form');
$form.attr('action', $button.data('customer-delete-url'));
$form.submit();
});
});
}
/**
* Adds input for selected customer to delete form
*
* @param {integer} customerId
*
* @private
*/
addCustomerInput(customerId) {
const $customersToDeleteInputBlock = $('#delete_customers_customers_to_delete');
const customerInput = $customersToDeleteInputBlock
.data('prototype')
.replace(/__name__/g, $customersToDeleteInputBlock.children().length);
const $item = $($.parseHTML(customerInput)[0]);
$item.val(customerId);
$customersToDeleteInputBlock.append($item);
}
}

View File

@@ -0,0 +1,103 @@
/**
* 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 ConfirmModal from '@components/modal';
const {$} = window;
/**
* Class SubmitRowActionExtension handles submitting of row action
*/
export default class SubmitRowActionExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-submit-row-action', (event) => {
event.preventDefault();
const $button = $(event.currentTarget);
const confirmMessage = $button.data('confirmMessage');
const confirmTitle = $button.data('title');
const method = $button.data('method');
if (confirmTitle) {
this.showConfirmModal($button, grid, confirmMessage, confirmTitle, method);
} else {
if (confirmMessage.length && !window.confirm(confirmMessage)) {
return;
}
this.postForm($button, method);
}
});
}
postForm($button, method) {
const isGetOrPostMethod = ['GET', 'POST'].includes(method);
const $form = $('<form>', {
action: $button.data('url'),
method: isGetOrPostMethod ? method : 'POST',
}).appendTo('body');
if (!isGetOrPostMethod) {
$form.append($('<input>', {
type: '_hidden',
name: '_method',
value: method,
}));
}
$form.submit();
}
/**
* @param {jQuery} $submitBtn
* @param {Grid} grid
* @param {string} confirmMessage
* @param {string} confirmTitle
* @param {string} method
*/
showConfirmModal($submitBtn, grid, confirmMessage, confirmTitle, method) {
const confirmButtonLabel = $submitBtn.data('confirmButtonLabel');
const closeButtonLabel = $submitBtn.data('closeButtonLabel');
const confirmButtonClass = $submitBtn.data('confirmButtonClass');
const modal = new ConfirmModal({
id: `${grid.getId()}-grid-confirm-modal`,
confirmTitle,
confirmMessage,
confirmButtonLabel,
closeButtonLabel,
confirmButtonClass,
}, () => this.postForm($submitBtn, method));
modal.show();
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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;
/**
* Class BulkActionSelectCheckboxExtension
*/
export default class BulkActionCheckboxExtension {
/**
* Extend grid with bulk action checkboxes handling functionality
*
* @param {Grid} grid
*/
extend(grid) {
this.handleBulkActionCheckboxSelect(grid);
this.handleBulkActionSelectAllCheckbox(grid);
}
/**
* Handles "Select all" button in the grid
*
* @param {Grid} grid
*
* @private
*/
handleBulkActionSelectAllCheckbox(grid) {
grid.getContainer().on('change', '.js-bulk-action-select-all', (e) => {
const $checkbox = $(e.currentTarget);
const isChecked = $checkbox.is(':checked');
if (isChecked) {
this.enableBulkActionsBtn(grid);
} else {
this.disableBulkActionsBtn(grid);
}
grid.getContainer().find('.js-bulk-action-checkbox').prop('checked', isChecked);
});
}
/**
* Handles each bulk action checkbox select in the grid
*
* @param {Grid} grid
*
* @private
*/
handleBulkActionCheckboxSelect(grid) {
grid.getContainer().on('change', '.js-bulk-action-checkbox', () => {
const checkedRowsCount = grid.getContainer().find('.js-bulk-action-checkbox:checked').length;
if (checkedRowsCount > 0) {
this.enableBulkActionsBtn(grid);
} else {
this.disableBulkActionsBtn(grid);
}
});
}
/**
* Enable bulk actions button
*
* @param {Grid} grid
*
* @private
*/
enableBulkActionsBtn(grid) {
grid.getContainer().find('.js-bulk-actions-btn').prop('disabled', false);
}
/**
* Disable bulk actions button
*
* @param {Grid} grid
*
* @private
*/
disableBulkActionsBtn(grid) {
grid.getContainer().find('.js-bulk-actions-btn').prop('disabled', true);
}
}

View File

@@ -0,0 +1,86 @@
/**
* 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 Router from '../../router';
const {$} = window;
/**
* Class BulkOpenTabsExtension
*/
export default class BulkOpenTabsExtension {
constructor() {
this.router = new Router();
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid with bulk action open tabs
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-bulk-action-btn.open_tabs', (event) => {
this.openTabs(event, grid);
});
}
/**
* Handle bulk action opening tabs
*
* @param {Event} event
* @param {Grid} grid
*
* @private
*/
openTabs(event, grid) {
const $submitBtn = $(event.currentTarget);
const route = $submitBtn.data('route');
const routeParamName = $submitBtn.data('routeParamName');
const tabsBlockedMessage = $submitBtn.data('tabsBlockedMessage');
const $checkboxes = grid.getContainer().find('.js-bulk-action-checkbox:checked');
let allTabsOpened = true;
$checkboxes.each((i, element) => {
const $checkbox = $(element);
const routeParams = {};
routeParams[routeParamName] = $checkbox.val();
const handle = window.open(this.router.generate(route, routeParams));
if (handle) {
handle.blur();
window.focus();
} else {
allTabsOpened = false;
}
if (!allTabsOpened) {
alert(tabsBlockedMessage);
}
});
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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 extension enables submit functionality of the choice fields in grid.
*
* Usage of the extension:
*
* const myGrid = new Grid('myGrid');
* myGrid.addExtension(new ChoiceExtension());
*
*/
export default class ChoiceExtension {
constructor() {
this.locks = [];
}
extend(grid) {
const $choiceOptionsContainer = grid.getContainer().find('table.table .js-choice-options');
$choiceOptionsContainer.find('.js-dropdown-item').on('click', (e) => {
e.preventDefault();
const $button = $(e.currentTarget);
const $parent = $button.closest('.js-choice-options');
const url = $parent.data('url');
this.submitForm(url, $button);
});
}
/**
* Submits the form.
* @param {string} url
* @param {jQuery} $button
* @private
*/
submitForm(url, $button) {
const selectedStatusId = $button.data('value');
if (this.isLocked(url)) {
return;
}
const $form = $('<form>', {
action: url,
method: 'POST',
}).append(
$('<input>', {
name: 'value',
value: selectedStatusId,
type: 'hidden',
}));
$form.appendTo('body');
$form.submit();
this.lock(url);
}
/**
* Checks if current url is being used at the moment.
*
* @param url
* @return {boolean}
*
* @private
*/
isLocked(url) {
return this.locks.includes(url);
}
/**
* Locks the current url so it cant be used twice to execute same request
* @param url
* @private
*/
lock(url) {
this.locks.push(url);
}
}

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;
/**
* Class ReloadListExtension extends grid with "Column toggling" feature
*/
export default class ColumnTogglingExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
const $table = grid.getContainer().find('table.table');
$table.find('.ps-togglable-row').on('click', (e) => {
e.preventDefault();
this.toggleValue($(e.delegateTarget));
});
}
/**
* @param {jQuery} row
* @private
*/
toggleValue(row) {
const toggleUrl = row.data('toggleUrl');
this.submitAsForm(toggleUrl);
}
/**
* Submits request url as form
*
* @param {string} toggleUrl
* @private
*/
submitAsForm(toggleUrl) {
const $form = $('<form>', {
action: toggleUrl,
method: 'POST',
}).appendTo('body');
$form.submit();
}
}

View File

@@ -0,0 +1,166 @@
/**
* 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 'tablednd/dist/jquery.tablednd.min';
const {$} = window;
/**
* Class CategoryPositionExtension extends Grid with reorderable category positions
*/
export default class CategoryPositionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
this.grid = grid;
this.addIdsToGridTableRows();
grid.getContainer().find('.js-grid-table').tableDnD({
dragHandle: '.js-drag-handle',
onDragClass: 'dragging-row',
onDragStart: () => {
this.originalPositions = decodeURIComponent($.tableDnD.serialize());
},
onDrop: (table, row) => this.handleCategoryPositionChange(row),
});
}
/**
* When position is changed handle update
*
* @param {HTMLElement} row
*
* @private
*/
handleCategoryPositionChange(row) {
const positions = decodeURIComponent($.tableDnD.serialize());
const way = (this.originalPositions.indexOf(row.id) < positions.indexOf(row.id)) ? 1 : 0;
const $categoryPositionContainer = $(row).find(`.js-${this.grid.getId()}-position:first`);
const categoryId = $categoryPositionContainer.data('id');
const categoryParentId = $categoryPositionContainer.data('id-parent');
const positionUpdateUrl = $categoryPositionContainer.data('position-update-url');
let params = positions.replace(new RegExp(`${this.grid.getId()}_grid_table`, 'g'), 'positions');
const queryParams = {
id_category_parent: categoryParentId,
id_category_to_move: categoryId,
way,
};
if (positions.indexOf('_0&') !== -1) {
queryParams.found_first = 1;
}
params += `&${$.param(queryParams)}`;
this.updateCategoryPosition(positionUpdateUrl, params);
}
/**
* Add ID's to Grid table rows to make tableDnD.onDrop() function work.
*
* @private
*/
addIdsToGridTableRows() {
this.grid.getContainer()
.find('.js-grid-table')
.find(`.js-${this.grid.getId()}-position`)
.each((index, positionWrapper) => {
const $positionWrapper = $(positionWrapper);
const categoryId = $positionWrapper.data('id');
const categoryParentId = $positionWrapper.data('id-parent');
const position = $positionWrapper.data('position');
const id = `tr_${categoryParentId}_${categoryId}_${position}`;
$positionWrapper.closest('tr').attr('id', id);
});
}
/**
* Update categories listing with new positions
*
* @private
*/
updateCategoryIdsAndPositions() {
this.grid.getContainer()
.find('.js-grid-table')
.find(`.js-${this.grid.getId()}-position`)
.each((index, positionWrapper) => {
const $positionWrapper = $(positionWrapper);
const $row = $positionWrapper.closest('tr');
const offset = $positionWrapper.data('pagination-offset');
const newPosition = offset > 0 ? index + offset : index;
const oldId = $row.attr('id');
$row.attr('id', oldId.replace(/_[0-9]$/g, `_${newPosition}`));
$positionWrapper.find('.js-position').text(newPosition + 1);
$positionWrapper.data('position', newPosition);
});
}
/**
* Process categories positions update
*
* @param {String} url
* @param {String} params
*
* @private
*/
updateCategoryPosition(url, params) {
$.post({
url,
headers: {
'cache-control': 'no-cache',
},
data: params,
dataType: 'json',
}).then((response) => {
if (response.success) {
window.showSuccessMessage(response.message);
} else {
window.showErrorMessage(response.message);
}
this.updateCategoryIdsAndPositions();
});
}
}

View File

@@ -0,0 +1,97 @@
/**
* 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;
/**
* Class AsyncToggleColumnExtension submits toggle action using AJAX
*/
export default class AsyncToggleColumnExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid
.getContainer()
.find('.js-grid-table')
.on('click', '.ps-togglable-row', (event) => {
const $button = $(event.currentTarget);
if (!$button.hasClass('ps-switch')) {
event.preventDefault();
}
$.post({
url: $button.data('toggle-url'),
})
.then((response) => {
if (response.status) {
window.showSuccessMessage(response.message);
this.toggleButtonDisplay($button);
return;
}
window.showErrorMessage(response.message);
})
.catch((error) => {
const response = error.responseJSON;
window.showErrorMessage(response.message);
});
});
}
/**
* Toggle button display from enabled to disabled and other way around
*
* @param {jQuery} $button
*
* @private
*/
toggleButtonDisplay($button) {
const isActive = $button.hasClass('grid-toggler-icon-valid');
const classToAdd = isActive ? 'grid-toggler-icon-not-valid' : 'grid-toggler-icon-valid';
const classToRemove = isActive ? 'grid-toggler-icon-valid' : 'grid-toggler-icon-not-valid';
const icon = isActive ? 'clear' : 'check';
$button.removeClass(classToRemove);
$button.addClass(classToAdd);
if ($button.hasClass('material-icons')) {
$button.text(icon);
}
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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;
/**
* Class ExportToSqlManagerExtension extends grid with exporting query to SQL Manager
*/
export default class ExportToSqlManagerExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getHeaderContainer().on('click', '.js-common_show_query-grid-action', () => this.onShowSqlQueryClick(grid));
grid.getHeaderContainer().on(
'click',
'.js-common_export_sql_manager-grid-action',
() => this.onExportSqlManagerClick(grid),
);
}
/**
* Invoked when clicking on the "show sql query" toolbar button
*
* @param {Grid} grid
*
* @private
*/
onShowSqlQueryClick(grid) {
const $sqlManagerForm = $(`#${grid.getId()}_common_show_query_modal_form`);
this.fillExportForm($sqlManagerForm, grid);
const $modal = $(`#${grid.getId()}_grid_common_show_query_modal`);
$modal.modal('show');
$modal.on('click', '.btn-sql-submit', () => $sqlManagerForm.submit());
}
/**
* Invoked when clicking on the "export to the sql query" toolbar button
*
* @param {Grid} grid
*
* @private
*/
onExportSqlManagerClick(grid) {
const $sqlManagerForm = $(`#${grid.getId()}_common_show_query_modal_form`);
this.fillExportForm($sqlManagerForm, grid);
$sqlManagerForm.submit();
}
/**
* Fill export form with SQL and it's name
*
* @param {jQuery} $sqlManagerForm
* @param {Grid} grid
*
* @private
*/
fillExportForm($sqlManagerForm, grid) {
const query = grid.getContainer().find('.js-grid-table').data('query');
$sqlManagerForm.find('textarea[name="sql"]').val(query);
$sqlManagerForm.find('input[name="name"]').val(this.getNameFromBreadcrumb());
}
/**
* Get export name from page's breadcrumb
*
* @return {String}
*
* @private
*/
getNameFromBreadcrumb() {
const $breadcrumbs = $('.header-toolbar').find('.breadcrumb-item');
let name = '';
$breadcrumbs.each((i, item) => {
const $breadcrumb = $(item);
const breadcrumbTitle = $breadcrumb.find('a').length > 0
? $breadcrumb.find('a').text()
: $breadcrumb.text();
if (name.length > 0) {
name = name.concat(' > ');
}
name = name.concat(breadcrumbTitle);
});
return name;
}
}

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)
*/
import resetSearch from '@app/utils/reset_search';
const {$} = window;
/**
* Class FiltersResetExtension extends grid with filters resetting
*/
export default class FiltersResetExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-reset-search', (event) => {
resetSearch($(event.currentTarget).data('url'), $(event.currentTarget).data('redirect'));
});
}
}

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)
*/
/**
* Responsible for grid filters search and reset button availability when filter inputs changes.
*/
export default class FiltersSubmitButtonEnablerExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
const $filtersRow = grid.getContainer().find('.column-filters');
$filtersRow.find('.grid-search-button').prop('disabled', true);
$filtersRow.find('input:not(.js-bulk-action-select-all), select').on('input dp.change', () => {
$filtersRow.find('.grid-search-button').prop('disabled', false);
$filtersRow.find('.js-grid-reset-button').prop('hidden', false);
});
}
}

View File

@@ -0,0 +1,95 @@
/**
* 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;
/**
* Class LinkRowActionExtension handles link row actions
*/
export default class LinkRowActionExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
this.initRowLinks(grid);
this.initConfirmableActions(grid);
}
/**
* Extend grid
*
* @param {Grid} grid
*/
initConfirmableActions(grid) {
grid.getContainer().on('click', '.js-link-row-action', (event) => {
const confirmMessage = $(event.currentTarget).data('confirm-message');
if (confirmMessage.length && !window.confirm(confirmMessage)) {
event.preventDefault();
}
});
}
/**
* Add a click event on rows that matches the first link action (if present)
*
* @param {Grid} grid
*/
initRowLinks(grid) {
$('tr', grid.getContainer()).each(function initEachRow() {
const $parentRow = $(this);
$('.js-link-row-action[data-clickable-row=1]:first', $parentRow).each(function propagateFirstLinkAction() {
const $rowAction = $(this);
const $parentCell = $rowAction.closest('td');
const clickableCells = $('td.clickable', $parentRow).not($parentCell);
let isDragging = false;
clickableCells.addClass('cursor-pointer').mousedown(() => {
$(window).mousemove(() => {
isDragging = true;
$(window).unbind('mousemove');
});
});
clickableCells.mouseup(() => {
const wasDragging = isDragging;
isDragging = false;
$(window).unbind('mousemove');
if (!wasDragging) {
const confirmMessage = $rowAction.data('confirm-message');
if (!confirmMessage.length || window.confirm(confirmMessage)) {
document.location = $rowAction.attr('href');
}
}
});
});
});
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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;
/**
* Allows submitting form inside modals.
* Form must be inside modal, see example structure below:
*
* <div class="modal" id="uniqueModalId">
* <form data-bulk-inputs-id="bulkInputs">
* <div class="d-none">
* <div id="bulkInputs" data-prototype="<input type="hidden" name="__name__"/>"></div>
* </div>
* </form>
* </div>
*
* Note that "data-prototype" is required to add checked items to the form. "__name__"
* will be replaced with value of bulk checkbox.
*/
export default class ModalFormSubmitExtension {
extend(grid) {
grid.getContainer().on('click', '.js-bulk-modal-form-submit-btn', (event) => {
const modalId = $(event.target).data('modal-id');
const $modal = $(`#${modalId}`);
$modal.modal('show');
$modal.find('.js-submit-modal-form-btn').on('click', () => {
const $form = $modal.find('form');
const $bulkInputsBlock = $form.find(`#${$form.data('bulk-inputs-id')}`);
const $checkboxes = grid.getContainer().find('.js-bulk-action-checkbox:checked');
$checkboxes.each((i, element) => {
const $checkbox = $(element);
const input = $bulkInputsBlock
.data('prototype')
.replace(/__name__/g, $checkbox.val());
const $input = $($.parseHTML(input)[0]);
$input.val($checkbox.val());
$form.append($input);
});
$form.submit();
});
});
}
}

View File

@@ -0,0 +1,204 @@
/**
* 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 'tablednd/dist/jquery.tablednd.min';
const {$} = window;
/**
* Class PositionExtension extends Grid with reorderable positions
*/
export default class PositionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
this.grid = grid;
this.addIdsToGridTableRows();
grid.getContainer().find('.js-grid-table').tableDnD({
onDragClass: 'position-row-while-drag',
dragHandle: '.js-drag-handle',
onDrop: (table, row) => this.handlePositionChange(row),
});
grid.getContainer().find('.js-drag-handle').hover(
function () {
$(this).closest('tr').addClass('hover');
},
function () {
$(this).closest('tr').removeClass('hover');
},
);
}
/**
* When position is changed handle update
*
* @param {HTMLElement} row
*
* @private
*/
handlePositionChange(row) {
const $rowPositionContainer = $(row).find(`.js-${this.grid.getId()}-position:first`);
const updateUrl = $rowPositionContainer.data('update-url');
const method = $rowPositionContainer.data('update-method');
const positions = this.getRowsPositions();
const params = {positions};
this.updatePosition(updateUrl, params, method);
}
/**
* Returns the current table positions
* @returns {Array}
* @private
*/
getRowsPositions() {
const tableData = JSON.parse($.tableDnD.jsonize());
const rowsData = tableData[`${this.grid.getId()}_grid_table`];
const completeRowsData = [];
let trData;
// retrieve dragAndDropOffset offset to have all needed data
// for positions mapping evolution over time
for (let i = 0; i < rowsData.length; i += 1) {
trData = this.grid.getContainer()
.find(`#${rowsData[i]}`);
completeRowsData.push({
rowMarker: rowsData[i],
offset: trData.data('dragAndDropOffset'),
});
}
return this.computeMappingBetweenOldAndNewPositions(completeRowsData);
}
/**
* Add ID's to Grid table rows to make tableDnD.onDrop() function work.
*
* @private
*/
addIdsToGridTableRows() {
let counter = 0;
this.grid.getContainer()
.find(`.js-grid-table .js-${this.grid.getId()}-position`)
.each((index, positionWrapper) => {
const $positionWrapper = $(positionWrapper);
const rowId = $positionWrapper.data('id');
const position = $positionWrapper.data('position');
const id = `row_${rowId}_${position}`;
$positionWrapper.closest('tr').attr('id', id);
$positionWrapper.closest('td').addClass('js-drag-handle');
$positionWrapper.closest('tr').data('dragAndDropOffset', counter);
counter += 1;
});
}
/**
* Process rows positions update
*
* @param {String} url
* @param {Object} params
* @param {String} method
*
* @private
*/
updatePosition(url, params, method) {
const isGetOrPostMethod = ['GET', 'POST'].includes(method);
const $form = $('<form>', {
action: url,
method: isGetOrPostMethod ? method : 'POST',
}).appendTo('body');
const positionsNb = params.positions.length;
let position;
for (let i = 0; i < positionsNb; i += 1) {
position = params.positions[i];
$form.append(
$('<input>', {
type: 'hidden',
name: `positions[${i}][rowId]`,
value: position.rowId,
}),
$('<input>', {
type: 'hidden',
name: `positions[${i}][oldPosition]`,
value: position.oldPosition,
}),
$('<input>', {
type: 'hidden',
name: `positions[${i}][newPosition]`,
value: position.newPosition,
}),
);
}
// This _method param is used by Symfony to simulate DELETE and PUT methods
if (!isGetOrPostMethod) {
$form.append($('<input>', {
type: 'hidden',
name: '_method',
value: method,
}));
}
$form.submit();
}
/**
* Rows have been reordered. This function
* finds, for each row ID: the old position, the new position
*
* @returns {Array}
* @private
*/
computeMappingBetweenOldAndNewPositions(rowsData) {
const regex = /^row_(\d+)_(\d+)$/;
const mapping = Array(rowsData.length).fill().map(Object);
for (let i = 0; i < rowsData.length; i += 1) {
const [, rowId, oldPosition] = regex.exec(rowsData[i].rowMarker);
mapping[i].rowId = rowId;
mapping[i].oldPosition = parseInt(oldPosition, 10);
// This row will have as a new position the old position of the current one
mapping[rowsData[i].offset].newPosition = mapping[i].oldPosition;
}
return mapping;
}
}

View File

@@ -0,0 +1,230 @@
/**
* 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;
/**
* Extends grid with preview functionality.
*/
export default class PreviewExtension {
constructor(previewCustomization) {
this.locks = [];
this.expandSelector = '.js-expand';
this.collapseSelector = '.js-collapse';
this.previewOpenClass = 'preview-open';
this.previewToggleSelector = '.preview-toggle';
this.previewCustomization = previewCustomization;
}
/**
* Extends provided grid with preview functionality
*
* @param grid
*/
extend(grid) {
this.$gridContainer = $(grid.getContainer);
this.$gridContainer.find('tbody tr').on('mouseover mouseleave', (event) => this.handleIconHovering(event));
this.$gridContainer.find(this.previewToggleSelector).on('click', (event) => this.togglePreview(event));
}
/**
* Shows/hides preview toggling icons
*
* @param event
* @private
*/
handleIconHovering(event) {
const $previewToggle = $(event.currentTarget).find(this.previewToggleSelector);
if (event.type === 'mouseover' && !$(event.currentTarget).hasClass(this.previewOpenClass)) {
this.showExpandIcon($previewToggle);
} else {
this.hideExpandIcon($previewToggle);
}
}
/**
* Shows/hides preview
*
* @param event
* @private
*/
togglePreview(event) {
const $previewToggle = $(event.currentTarget);
const $columnRow = $previewToggle.closest('tr');
if ($columnRow.hasClass(this.previewOpenClass)) {
$columnRow.next('.preview-row').remove();
$columnRow.removeClass(this.previewOpenClass);
this.showExpandIcon($columnRow);
this.hideCollapseIcon($columnRow);
return;
}
this.closeOpenedPreviews();
const dataUrl = $(event.currentTarget).data('preview-data-url');
if (this.isLocked(dataUrl)) {
return;
}
// Prevents loading preview multiple times.
// Uses "dataUrl" as lock key.
this.lock(dataUrl);
$.ajax({
url: dataUrl,
method: 'GET',
dataType: 'json',
complete: () => {
this.unlock(dataUrl);
},
}).then((response) => {
this.renderPreviewContent($columnRow, response.preview);
}).catch((e) => {
window.showErrorMessage(e.responseJSON.message);
});
}
/**
* Renders preview content
*
* @param $columnRow
* @param content
*
* @private
*/
renderPreviewContent($columnRow, content) {
const rowColumnCount = $columnRow.find('td').length;
const $previewTemplate = $(`
<tr class="preview-row">
<td colspan="${rowColumnCount}">${content}</td>
</tr>
`);
$columnRow.addClass(this.previewOpenClass);
this.showCollapseIcon($columnRow);
this.hideExpandIcon($columnRow);
if (typeof this.previewCustomization === 'function') {
this.previewCustomization($previewTemplate);
}
$columnRow.after($previewTemplate);
}
/**
* Shows preview expanding icon
*
* @param parent
* @private
*/
showExpandIcon(parent) {
parent.find(this.expandSelector).removeClass('d-none');
}
/**
* Hides preview expanding icon
*
* @param parent
* @private
*/
hideExpandIcon(parent) {
parent.find(this.expandSelector).addClass('d-none');
}
/**
* Shows preview collapsing icon
*
* @param parent
* @private
*/
showCollapseIcon(parent) {
parent.find(this.collapseSelector).removeClass('d-none');
}
/**
* Hides preview collapsing icon
*
* @param parent
* @private
*/
hideCollapseIcon(parent) {
parent.find(this.collapseSelector).addClass('d-none');
}
isLocked(key) {
return this.locks.indexOf(key) !== -1;
}
lock(key) {
if (this.isLocked(key)) {
return;
}
this.locks.push(key);
}
unlock(key) {
const index = this.locks.indexOf(key);
if (index === -1) {
return;
}
this.locks.splice(index, 1);
}
/**
* Close all previews that are open.
*
* @private
*/
closeOpenedPreviews() {
const $rows = this.$gridContainer.find('.grid-table tbody').find('tr:not(.preview-row)');
$.each($rows, (i, row) => {
const $row = $(row);
if (!$row.hasClass(this.previewOpenClass)) {
return;
}
const $previewRow = $row.next();
if (!$previewRow.hasClass('preview-row')) {
return;
}
$previewRow.remove();
$row.removeClass(this.previewOpenClass);
this.hideCollapseIcon($row);
});
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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)
*/
/**
* Class ReloadListExtension extends grid with "List reload" action
*/
export default class ReloadListExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
grid.getHeaderContainer().on('click', '.js-common_refresh_list-grid-action', () => {
window.location.reload();
});
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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 TableSorting from '@app/utils/table-sorting';
/**
* Class ReloadListExtension extends grid with "List reload" action
*/
export default class SortingExtension {
/**
* Extend grid
*
* @param {Grid} grid
*/
extend(grid) {
const $sortableTable = grid.getContainer().find('table.table');
new TableSorting($sortableTable).attach();
}
}

View File

@@ -0,0 +1,109 @@
/**
* 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 ConfirmModal from '@components/modal';
const {$} = window;
/**
* Handles submit of grid actions
*/
export default class SubmitBulkActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
/**
* Extend grid with bulk action submitting
*
* @param {Grid} grid
*/
extend(grid) {
grid.getContainer().on('click', '.js-bulk-action-submit-btn', (event) => {
this.submit(event, grid);
});
}
/**
* Handle bulk action submitting
*
* @param {Event} event
* @param {Grid} grid
*
* @private
*/
submit(event, grid) {
const $submitBtn = $(event.currentTarget);
const confirmMessage = $submitBtn.data('confirm-message');
const confirmTitle = $submitBtn.data('confirmTitle');
if (confirmMessage !== undefined && confirmMessage.length > 0) {
if (confirmTitle !== undefined) {
this.showConfirmModal($submitBtn, grid, confirmMessage, confirmTitle);
} else if (window.confirm(confirmMessage)) {
this.postForm($submitBtn, grid);
}
} else {
this.postForm($submitBtn, grid);
}
}
/**
* @param {jQuery} $submitBtn
* @param {Grid} grid
* @param {string} confirmMessage
* @param {string} confirmTitle
*/
showConfirmModal($submitBtn, grid, confirmMessage, confirmTitle) {
const confirmButtonLabel = $submitBtn.data('confirmButtonLabel');
const closeButtonLabel = $submitBtn.data('closeButtonLabel');
const confirmButtonClass = $submitBtn.data('confirmButtonClass');
const modal = new ConfirmModal({
id: `${grid.getId()}-grid-confirm-modal`,
confirmTitle,
confirmMessage,
confirmButtonLabel,
closeButtonLabel,
confirmButtonClass,
}, () => this.postForm($submitBtn, grid));
modal.show();
}
/**
* @param {jQuery} $submitBtn
* @param {Grid} grid
*/
postForm($submitBtn, grid) {
const $form = $(`#${grid.getId()}_filter_form`);
$form.attr('action', $submitBtn.data('form-url'));
$form.attr('method', $submitBtn.data('form-method'));
$form.submit();
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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;
/**
* Class SubmitGridActionExtension handles grid action submits
*/
export default class SubmitGridActionExtension {
constructor() {
return {
extend: (grid) => this.extend(grid),
};
}
extend(grid) {
grid.getHeaderContainer().on('click', '.js-grid-action-submit-btn', (event) => {
this.handleSubmit(event, grid);
});
}
/**
* Handle grid action submit.
* It uses grid form to submit actions.
*
* @param {Event} event
* @param {Grid} grid
*
* @private
*/
handleSubmit(event, grid) {
const $submitBtn = $(event.currentTarget);
const confirmMessage = $submitBtn.data('confirm-message');
if (typeof confirmMessage !== 'undefined' && confirmMessage.length > 0 && !window.confirm(confirmMessage)) {
return;
}
const $form = $(`#${grid.getId()}_filter_form`);
$form.attr('action', $submitBtn.data('url'));
$form.attr('method', $submitBtn.data('method'));
$form.find(`input[name="${grid.getId()}[_token]"]`).val($submitBtn.data('csrf'));
$form.submit();
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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;
/**
* Class is responsible for handling Grid events
*/
export default class Grid {
/**
* Grid id
*
* @param {string} id
*/
constructor(id) {
this.id = id;
this.$container = $(`#${this.id}_grid`);
}
/**
* Get grid id
*
* @returns {string}
*/
getId() {
return this.id;
}
/**
* Get grid container
*
* @returns {jQuery}
*/
getContainer() {
return this.$container;
}
/**
* Get grid header container
*
* @returns {jQuery}
*/
getHeaderContainer() {
return this.$container.closest('.js-grid-panel').find('.js-grid-header');
}
/**
* Extend grid with external extensions
*
* @param {object} extension
*/
addExtension(extension) {
extension.extend(this);
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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;
/**
* Takes link from clicked item and redirects to it.
*/
export default class LinkableItem {
constructor() {
$(document).on('click', '.js-linkable-item', (event) => {
window.location = $(event.currentTarget).data('linkable-href');
});
}
}

View File

@@ -0,0 +1,163 @@
/**
* 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;
/**
* ConfirmModal component
*
* @param {String} id
* @param {String} confirmTitle
* @param {String} confirmMessage
* @param {String} closeButtonLabel
* @param {String} confirmButtonLabel
* @param {String} confirmButtonClass
* @param {Array} customButtons
* @param {Boolean} closable
* @param {Function} confirmCallback
* @param {Function} cancelCallback
*
*/
export default function ConfirmModal(params, confirmCallback, cancelCallback) {
// Construct the modal
const {id, closable} = params;
this.modal = Modal(params);
// jQuery modal object
this.$modal = $(this.modal.container);
this.show = () => {
this.$modal.modal();
};
this.modal.confirmButton.addEventListener('click', confirmCallback);
this.$modal.modal({
backdrop: closable ? true : 'static',
keyboard: closable !== undefined ? closable : true,
closable: closable !== undefined ? closable : true,
show: false,
});
this.$modal.on('hidden.bs.modal', () => {
document.querySelector(`#${id}`).remove();
if (cancelCallback) {
cancelCallback();
}
});
document.body.appendChild(this.modal.container);
}
/**
* Modal component to improve lisibility by constructing the modal outside the main function
*
* @param {Object} params
*
*/
function Modal({
id = 'confirm-modal',
confirmTitle,
confirmMessage = '',
closeButtonLabel = 'Close',
confirmButtonLabel = 'Accept',
confirmButtonClass = 'btn-primary',
customButtons = [],
}) {
const modal = {};
// Main modal element
modal.container = document.createElement('div');
modal.container.classList.add('modal', 'fade');
modal.container.id = id;
// Modal dialog element
modal.dialog = document.createElement('div');
modal.dialog.classList.add('modal-dialog');
// Modal content element
modal.content = document.createElement('div');
modal.content.classList.add('modal-content');
// Modal header element
modal.header = document.createElement('div');
modal.header.classList.add('modal-header');
// Modal title element
if (confirmTitle) {
modal.title = document.createElement('h4');
modal.title.classList.add('modal-title');
modal.title.innerHTML = confirmTitle;
}
// Modal close button icon
modal.closeIcon = document.createElement('button');
modal.closeIcon.classList.add('close');
modal.closeIcon.setAttribute('type', 'button');
modal.closeIcon.dataset.dismiss = 'modal';
modal.closeIcon.innerHTML = '×';
// Modal body element
modal.body = document.createElement('div');
modal.body.classList.add('modal-body', 'text-left', 'font-weight-normal');
// Modal message element
modal.message = document.createElement('p');
modal.message.classList.add('confirm-message');
modal.message.innerHTML = confirmMessage;
// Modal footer element
modal.footer = document.createElement('div');
modal.footer.classList.add('modal-footer');
// Modal close button element
modal.closeButton = document.createElement('button');
modal.closeButton.setAttribute('type', 'button');
modal.closeButton.classList.add('btn', 'btn-outline-secondary', 'btn-lg');
modal.closeButton.dataset.dismiss = 'modal';
modal.closeButton.innerHTML = closeButtonLabel;
// Modal confirm button element
modal.confirmButton = document.createElement('button');
modal.confirmButton.setAttribute('type', 'button');
modal.confirmButton.classList.add('btn', confirmButtonClass, 'btn-lg', 'btn-confirm-submit');
modal.confirmButton.dataset.dismiss = 'modal';
modal.confirmButton.innerHTML = confirmButtonLabel;
// Constructing the modal
if (confirmTitle) {
modal.header.append(modal.title, modal.closeIcon);
} else {
modal.header.appendChild(modal.closeIcon);
}
modal.body.appendChild(modal.message);
modal.footer.append(modal.closeButton, ...customButtons, modal.confirmButton);
modal.content.append(modal.header, modal.body, modal.footer);
modal.dialog.appendChild(modal.content);
modal.container.appendChild(modal.dialog);
return modal;
}

View File

@@ -0,0 +1,431 @@
/**
* 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 ConfirmModal from '@components/modal';
const {$} = window;
const BOEvent = {
on(eventName, callback, context) {
document.addEventListener(eventName, (event) => {
if (typeof context !== 'undefined') {
callback.call(context, event);
} else {
callback(event);
}
});
},
emitEvent(eventName, eventType) {
const event = document.createEvent(eventType);
// true values stand for: can bubble, and is cancellable
event.initEvent(eventName, true, true);
document.dispatchEvent(event);
},
};
/**
* Class is responsible for handling Module Card behavior
*
* This is a port of admin-dev/themes/default/js/bundle/module/module_card.js
*/
export default class ModuleCard {
constructor() {
/* Selectors for module action links (uninstall, reset, etc...) to add a confirm popin */
this.moduleActionMenuLinkSelector = 'button.module_action_menu_';
this.moduleActionMenuInstallLinkSelector = 'button.module_action_menu_install';
this.moduleActionMenuEnableLinkSelector = 'button.module_action_menu_enable';
this.moduleActionMenuUninstallLinkSelector = 'button.module_action_menu_uninstall';
this.moduleActionMenuDisableLinkSelector = 'button.module_action_menu_disable';
this.moduleActionMenuEnableMobileLinkSelector = 'button.module_action_menu_enable_mobile';
this.moduleActionMenuDisableMobileLinkSelector = 'button.module_action_menu_disable_mobile';
this.moduleActionMenuResetLinkSelector = 'button.module_action_menu_reset';
this.moduleActionMenuUpdateLinkSelector = 'button.module_action_menu_upgrade';
this.moduleItemListSelector = '.module-item-list';
this.moduleItemGridSelector = '.module-item-grid';
this.moduleItemActionsSelector = '.module-actions';
/* Selectors only for modal buttons */
this.moduleActionModalDisableLinkSelector = 'a.module_action_modal_disable';
this.moduleActionModalResetLinkSelector = 'a.module_action_modal_reset';
this.moduleActionModalUninstallLinkSelector = 'a.module_action_modal_uninstall';
this.forceDeletionOption = '#force_deletion';
this.initActionButtons();
}
initActionButtons() {
const self = this;
$(document).on('click', this.forceDeletionOption, function () {
const btn = $(
self.moduleActionModalUninstallLinkSelector,
$(`div.module-item-list[data-tech-name='${$(this).attr('data-tech-name')}']`),
);
if ($(this).prop('checked') === true) {
btn.attr('data-deletion', 'true');
} else {
btn.removeAttr('data-deletion');
}
});
$(document).on('click', this.moduleActionMenuInstallLinkSelector, function () {
if ($('#modal-prestatrust').length) {
$('#modal-prestatrust').modal('hide');
}
return (
self.dispatchPreEvent('install', this)
&& self.confirmAction('install', this)
&& self.requestToController('install', $(this))
);
});
$(document).on('click', this.moduleActionMenuEnableLinkSelector, function () {
return (
self.dispatchPreEvent('enable', this)
&& self.confirmAction('enable', this)
&& self.requestToController('enable', $(this))
);
});
$(document).on('click', this.moduleActionMenuUninstallLinkSelector, function () {
return (
self.dispatchPreEvent('uninstall', this)
&& self.confirmAction('uninstall', this)
&& self.requestToController('uninstall', $(this))
);
});
$(document).on('click', this.moduleActionMenuDisableLinkSelector, function () {
return (
self.dispatchPreEvent('disable', this)
&& self.confirmAction('disable', this)
&& self.requestToController('disable', $(this))
);
});
$(document).on('click', this.moduleActionMenuEnableMobileLinkSelector, function () {
return (
self.dispatchPreEvent('enable_mobile', this)
&& self.confirmAction('enable_mobile', this)
&& self.requestToController('enable_mobile', $(this))
);
});
$(document).on('click', this.moduleActionMenuDisableMobileLinkSelector, function () {
return (
self.dispatchPreEvent('disable_mobile', this)
&& self.confirmAction('disable_mobile', this)
&& self.requestToController('disable_mobile', $(this))
);
});
$(document).on('click', this.moduleActionMenuResetLinkSelector, function () {
return (
self.dispatchPreEvent('reset', this)
&& self.confirmAction('reset', this)
&& self.requestToController('reset', $(this))
);
});
$(document).on('click', this.moduleActionMenuUpdateLinkSelector, function (event) {
event.preventDefault();
const modal = $(`#${$(this).data('confirm_modal')}`);
const isMaintenanceMode = window.isShopMaintenance;
if (modal.length !== 1) {
// Modal body element
const maintenanceLink = document.createElement('a');
maintenanceLink.classList.add('btn', 'btn-primary', 'btn-lg');
maintenanceLink.setAttribute('href', window.moduleURLs.maintenancePage);
maintenanceLink.innerHTML = window.moduleTranslations.moduleModalUpdateMaintenance;
const updateConfirmModal = new ConfirmModal(
{
id: 'confirm-module-update-modal',
confirmTitle: window.moduleTranslations.singleModuleModalUpdateTitle,
closeButtonLabel: window.moduleTranslations.moduleModalUpdateCancel,
confirmButtonLabel: isMaintenanceMode
? window.moduleTranslations.moduleModalUpdateUpgrade
: window.moduleTranslations.upgradeAnywayButtonText,
confirmButtonClass: isMaintenanceMode ? 'btn-primary' : 'btn-secondary',
confirmMessage: isMaintenanceMode ? '' : window.moduleTranslations.moduleModalUpdateConfirmMessage,
closable: true,
customButtons: isMaintenanceMode ? [] : [maintenanceLink],
},
() => self.dispatchPreEvent('update', this)
&& self.confirmAction('update', this)
&& self.requestToController('update', $(this)),
);
updateConfirmModal.show();
} else {
return (
self.dispatchPreEvent('update', this)
&& self.confirmAction('update', this)
&& self.requestToController('update', $(this))
);
}
return false;
});
$(document).on('click', this.moduleActionModalDisableLinkSelector, function () {
return self.requestToController(
'disable',
$(
self.moduleActionMenuDisableLinkSelector,
$(`div.module-item-list[data-tech-name='${$(this).attr('data-tech-name')}']`),
),
);
});
$(document).on('click', this.moduleActionModalResetLinkSelector, function () {
return self.requestToController(
'reset',
$(
self.moduleActionMenuResetLinkSelector,
$(`div.module-item-list[data-tech-name='${$(this).attr('data-tech-name')}']`),
),
);
});
$(document).on('click', this.moduleActionModalUninstallLinkSelector, (e) => {
$(e.target)
.parents('.modal')
.on('hidden.bs.modal', () => self.requestToController(
'uninstall',
$(
self.moduleActionMenuUninstallLinkSelector,
$(`div.module-item-list[data-tech-name='${$(e.target).attr('data-tech-name')}']`),
),
$(e.target).attr('data-deletion'),
),
);
});
}
getModuleItemSelector() {
if ($(this.moduleItemListSelector).length) {
return this.moduleItemListSelector;
}
return this.moduleItemGridSelector;
}
confirmAction(action, element) {
const modal = $(`#${$(element).data('confirm_modal')}`);
if (modal.length !== 1) {
return true;
}
modal.first().modal('show');
return false; // do not allow a.href to reload the page. The confirm modal dialog will do it async if needed.
}
/**
* Update the content of a modal asking a confirmation for PrestaTrust and open it
*
* @param {array} result containing module data
* @return {void}
*/
confirmPrestaTrust(result) {
const that = this;
const modal = this.replacePrestaTrustPlaceholders(result);
modal
.find('.pstrust-install')
.off('click')
.on('click', () => {
// Find related form, update it and submit it
const installButton = $(
that.moduleActionMenuInstallLinkSelector,
`.module-item[data-tech-name="${result.module.attributes.name}"]`,
);
const form = installButton.parent('form');
$('<input>')
.attr({
type: 'hidden',
value: '1',
name: 'actionParams[confirmPrestaTrust]',
})
.appendTo(form);
installButton.click();
modal.modal('hide');
});
modal.modal();
}
replacePrestaTrustPlaceholders(result) {
const modal = $('#modal-prestatrust');
const module = result.module.attributes;
if (result.confirmation_subject !== 'PrestaTrust' || !modal.length) {
return false;
}
const alertClass = module.prestatrust.status ? 'success' : 'warning';
if (module.prestatrust.check_list.property) {
modal.find('#pstrust-btn-property-ok').show();
modal.find('#pstrust-btn-property-nok').hide();
} else {
modal.find('#pstrust-btn-property-ok').hide();
modal.find('#pstrust-btn-property-nok').show();
modal
.find('#pstrust-buy')
.attr('href', module.url)
.toggle(module.url !== null);
}
modal.find('#pstrust-img').attr({src: module.img, alt: module.name});
modal.find('#pstrust-name').text(module.displayName);
modal.find('#pstrust-author').text(module.author);
modal
.find('#pstrust-label')
.attr('class', `text-${alertClass}`)
.text(module.prestatrust.status ? 'OK' : 'KO');
modal.find('#pstrust-message').attr('class', `alert alert-${alertClass}`);
modal.find('#pstrust-message > p').text(module.prestatrust.message);
return modal;
}
dispatchPreEvent(action, element) {
const event = jQuery.Event('module_card_action_event');
$(element).trigger(event, [action]);
if (event.isPropagationStopped() !== false || event.isImmediatePropagationStopped() !== false) {
return false; // if all handlers have not been called, then stop propagation of the click event.
}
return event.result !== false; // explicit false must be set from handlers to stop propagation of the click event.
}
requestToController(action, element, forceDeletion, disableCacheClear, callback) {
const self = this;
const jqElementObj = element.closest(this.moduleItemActionsSelector);
const form = element.closest('form');
const spinnerObj = $('<button class="btn-primary-reverse onclick unbind spinner "></button>');
const url = `//${window.location.host}${form.attr('action')}`;
const actionParams = form.serializeArray();
if (forceDeletion === 'true' || forceDeletion === true) {
actionParams.push({name: 'actionParams[deletion]', value: true});
}
if (disableCacheClear === 'true' || disableCacheClear === true) {
actionParams.push({name: 'actionParams[cacheClearEnabled]', value: 0});
}
$.ajax({
url,
dataType: 'json',
method: 'POST',
data: actionParams,
beforeSend() {
jqElementObj.hide();
jqElementObj.after(spinnerObj);
},
})
.done((result) => {
if (result === undefined) {
$.growl.error({
message: 'No answer received from server',
fixed: true,
});
return;
}
if (typeof result.status !== 'undefined' && result.status === false) {
$.growl.error({message: result.msg, fixed: true});
return;
}
const moduleTechName = Object.keys(result)[0];
if (result[moduleTechName].status === false) {
if (typeof result[moduleTechName].confirmation_subject !== 'undefined') {
self.confirmPrestaTrust(result[moduleTechName]);
}
$.growl.error({message: result[moduleTechName].msg, fixed: true});
return;
}
$.growl({
message: result[moduleTechName].msg,
duration: 6000,
});
const alteredSelector = self.getModuleItemSelector().replace('.', '');
let mainElement = null;
if (action === 'uninstall') {
mainElement = jqElementObj.closest(`.${alteredSelector}`);
mainElement.remove();
BOEvent.emitEvent('Module Uninstalled', 'CustomEvent');
} else if (action === 'disable') {
mainElement = jqElementObj.closest(`.${alteredSelector}`);
mainElement.addClass(`${alteredSelector}-isNotActive`);
mainElement.attr('data-active', '0');
BOEvent.emitEvent('Module Disabled', 'CustomEvent');
} else if (action === 'enable') {
mainElement = jqElementObj.closest(`.${alteredSelector}`);
mainElement.removeClass(`${alteredSelector}-isNotActive`);
mainElement.attr('data-active', '1');
BOEvent.emitEvent('Module Enabled', 'CustomEvent');
}
jqElementObj.replaceWith(result[moduleTechName].action_menu_html);
})
.fail(() => {
const moduleItem = jqElementObj.closest('module-item-list');
const techName = moduleItem.data('techName');
$.growl.error({
message: `Could not perform action ${action} for module ${techName}`,
fixed: true,
});
})
.always(() => {
jqElementObj.fadeIn();
spinnerObj.remove();
if (callback) {
callback();
}
});
return false;
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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)
*/
/**
* Encapsulates selectors for multi store restriction component
*/
export default {
multiStoreRestrictionCheckbox: '.js-multi-store-restriction-checkbox',
multiStoreRestrictionSwitch: '.js-multi-store-restriction-switch',
};

View File

@@ -0,0 +1,96 @@
/**
* 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 multiStoreRestrictionFieldMap from './multi-store-restriction-field-map';
const {$} = window;
/**
* Enables multi store functionality for the page. It includes switch functionality and checkboxes
*/
export default class MultiStoreRestrictionField {
constructor() {
$(document).on(
'change',
multiStoreRestrictionFieldMap.multiStoreRestrictionCheckbox,
(e) => this.multiStoreRestrictionCheckboxFieldChangeEvent(e),
);
$(document).on(
'change',
multiStoreRestrictionFieldMap.multiStoreRestrictionSwitch,
(e) => this.multiStoreRestrictionSwitchFieldChangeEvent(e),
);
}
/**
* Toggles the checkbox field and enables or disables its related field.
*
* @param {Event} e
* @private
*/
multiStoreRestrictionCheckboxFieldChangeEvent(e) {
const $currentItem = $(e.currentTarget);
this.toggleSourceFieldByTargetElement($currentItem, !$currentItem.is(':checked'));
}
/**
* Mass updates multi-store checkbox fields - it enables or disabled the switch and after that
* it calls the function
* which handles the toggle update related form field by its current state.
* @param {Event} e
* @private
*/
multiStoreRestrictionSwitchFieldChangeEvent(e) {
const $currentItem = $(e.currentTarget);
const isSelected = parseInt($currentItem.val(), 10) === 1;
const targetFormName = $currentItem.data('targetFormName');
$(`form[name="${targetFormName}"]`)
.find(multiStoreRestrictionFieldMap.multiStoreRestrictionCheckbox)
.each((index, el) => {
const $el = $(el);
$el.prop('checked', isSelected);
this.toggleSourceFieldByTargetElement($el, !isSelected);
});
}
/**
* Changes related form fields state to disabled or enabled.
* It also toggles class disabled since for some fields
* this class is used instead of the native disabled attribute.
*
* @param {jquery} $targetElement
* @param {boolean} isDisabled
* @private
*/
toggleSourceFieldByTargetElement($targetElement, isDisabled) {
const targetValue = $targetElement.data('shopRestrictionTarget');
const $sourceFieldSelector = $(`[data-shop-restriction-source="${targetValue}"]`);
$sourceFieldSelector.prop('disabled', isDisabled);
$sourceFieldSelector.toggleClass('disabled', isDisabled);
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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;
/**
* MultipleChoiceTable is responsible for managing common actions in multiple choice table form type
*/
export default class MultipleChoiceTable {
/**
* Init constructor
*/
constructor() {
$(document).on('click', '.js-multiple-choice-table-select-column', (e) => this.handleSelectColumn(e));
}
/**
* Check/uncheck all boxes in column
*
* @param {Event} event
*/
handleSelectColumn(event) {
event.preventDefault();
const $selectColumnBtn = $(event.target);
const checked = $selectColumnBtn.data('column-checked');
$selectColumnBtn.data('column-checked', !checked);
const $table = $selectColumnBtn.closest('table');
$table
.find(`tbody tr td:nth-child(${$selectColumnBtn.data('column-num')}) input[type=checkbox]`)
.prop('checked', !checked);
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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 Bloodhound from 'typeahead.js';
import Router from '@components/router';
import AutoCompleteSearch from '@components/auto-complete-search';
import PerfectScrollbar from 'perfect-scrollbar';
import ComponentsMap from '@components/components-map';
import 'perfect-scrollbar/css/perfect-scrollbar.css';
const {$} = window;
const initMultistoreDropdown = () => {
const MultistoreDropdownMap = ComponentsMap.multistoreDropdown;
const $searchInput = $(MultistoreDropdownMap.searchInput);
const router = new Router();
const route = router.generate('admin_shops_search', {
searchTerm: '__QUERY__',
});
if ($(MultistoreDropdownMap.scrollbar).length > 0) {
new PerfectScrollbar(MultistoreDropdownMap.scrollbar);
}
const source = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: route,
wildcard: '__QUERY__',
},
});
const dataSetConfig = {
display: 'name',
value: 'id',
source,
/* eslint-disable-next-line no-unused-vars */
onSelect(selectedItem, event) {
const contextUrlLetter = typeof selectedItem.groupName !== 'undefined' ? 's' : 'g';
window.location.href = ComponentsMap.multistoreHeader.setContextUrl(
window.location.href,
contextUrlLetter,
selectedItem.id,
);
return true;
},
/* eslint-disable-next-line no-unused-vars */
onClose(event) {},
};
new AutoCompleteSearch($searchInput, dataSetConfig);
};
$(() => {
initMultistoreDropdown();
});

View File

@@ -0,0 +1,77 @@
/**
* 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 Bloodhound from 'typeahead.js';
import Router from '@components/router';
import AutoCompleteSearch from '@components/auto-complete-search';
import PerfectScrollbar from 'perfect-scrollbar';
import ComponentsMap from '@components/components-map';
import 'perfect-scrollbar/css/perfect-scrollbar.css';
const {$} = window;
const initMultistoreHeader = () => {
const MultistoreHeaderMap = ComponentsMap.multistoreHeader;
const headerButton = document.querySelector(MultistoreHeaderMap.headerButton);
const modalMultishop = document.querySelector(MultistoreHeaderMap.modal);
const $searchInput = $(MultistoreHeaderMap.searchInput);
const router = new Router();
const route = router.generate('admin_shops_search', {
searchTerm: '__QUERY__',
});
new PerfectScrollbar(MultistoreHeaderMap.jsScrollbar);
const source = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: route,
wildcard: '__QUERY__',
},
});
const dataSetConfig = {
source,
onSelect(selectedItem) {
const contextUrlLetter = typeof selectedItem.groupName !== 'undefined' ? 's' : 'g';
const setContextUrl = MultistoreHeaderMap.setContextUrl(window.location.href, contextUrlLetter, selectedItem.id);
window.location.href = setContextUrl;
return true;
},
};
new AutoCompleteSearch($searchInput, dataSetConfig);
headerButton.addEventListener('click', () => {
modalMultishop.classList.toggle('multishop-modal-hidden');
headerButton.classList.toggle('active');
});
};
$(() => {
initMultistoreHeader();
});

View File

@@ -0,0 +1,111 @@
/**
* 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)
*/
/**
* This component watches a navigation bar and is able to link it alternative links, and automatic switch
* on page load.
*
* You can add button with class tab-link when they are clicked the tab target is fetched
* from the button's data property targetTab (so data-target-tab), it then search for a tab
* which targets matches in the navbar and simulates a click on it.
*
* Alternatively it also checks on page load if a hash matches a tab and activates it if one is found,
* and of course the hash is kept in sync when the navbar or alternative links are used.
*/
export default class NavbarHandler {
constructor($navigationContainer, tabPrefix) {
// We use a tab prefix for hastag so that on reload the page doesn't auto scroll to the anchored element
this.tabPrefix = tabPrefix || 'tab-';
this.$navigationContainer = $navigationContainer;
this.watchNavbar();
this.watchTabLinks();
this.switchOnPageLoad();
}
switchToTarget(target) {
if (!target) {
return;
}
const matchingTabs = $(`[href="${target}"]`, this.$navigationContainer);
if (matchingTabs.length <= 0) {
return;
}
const tabLink = matchingTabs.first();
this.switchToTab(tabLink);
}
switchToTab(tab) {
tab.click();
this.updateBrowserHash(tab.attr('href'));
}
updateBrowserHash(target) {
const hashName = target.replace('#', `#${this.tabPrefix}`);
if (window.history.pushState) {
window.history.pushState(null, null, hashName);
} else {
window.location.hash = hashName;
}
}
watchNavbar() {
$(this.$navigationContainer).on('shown.bs.tab', (event) => {
if (event.target.hash) {
this.updateBrowserHash(event.target.hash);
}
});
}
watchTabLinks() {
$('.tab-link').click((event) => {
event.preventDefault();
const target = $(event.target).attr('href');
if (!target) {
return;
}
this.switchToTarget(`${target}`);
});
}
switchOnPageLoad() {
const errorTabs = $('.has-error', this.$navigationContainer);
if (errorTabs.length) {
const errorTab = $('a[role="tab"]', errorTabs.first()).first();
this.switchToTab(errorTab);
} else {
const {hash} = document.location;
const target = hash.replace(`#${this.tabPrefix}`, '#');
this.switchToTarget(target);
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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)
*/
/**
* Toggle a class on $mainMenu after the end of an event (transition, animation...)
* @param {jQuery element} $navBar - The navbar item which get a css transition property.
* @param {jQuery element} $mainMenu - The menu inside the $navBar element.
* @param {string} endTransitionEvent - The name of the event.
* @param {jQuery element} $body - The body of the page.
* @method showNavBarContent - Toggle the class based on event and if body got a class.
* @method toggle - Add the listener if there is no transition launched yet.
* @return {Object} The object with methods wich permit to toggle on specific event.
*/
function NavbarTransitionHandler($navBar, $mainMenu, endTransitionEvent, $body) {
this.$body = $body;
this.transitionFired = false;
this.$navBar = $navBar.get(0);
this.$mainMenu = $mainMenu;
this.endTransitionEvent = endTransitionEvent;
this.showNavBarContent = (event) => {
if (event.propertyName !== 'width') {
return;
}
this.$navBar.removeEventListener(this.endTransitionEvent, this.showNavBarContent);
const isSidebarClosed = this.$body.hasClass('page-sidebar-closed');
this.$mainMenu.toggleClass('sidebar-closed', isSidebarClosed);
this.transitionFired = false;
};
this.toggle = () => {
if (!this.transitionFired) {
this.$navBar.addEventListener(this.endTransitionEvent, this.showNavBarContent.bind(this));
} else {
this.$navBar.removeEventListener(this.endTransitionEvent, this.showNavBarContent);
}
this.transitionFired = !this.transitionFired;
};
}
export default NavbarTransitionHandler;

View File

@@ -0,0 +1,266 @@
/**
* 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;
/**
* Related html template src/PrestaShopBundle/Resources/views/Admin/Common/javascript_pagination.html.twig
*
* Usage
*```
* $paginator new DynamicPaginator(
* '#foo-container',
* FooDataService,
* FooRenderer
* );
* this.eventEmitter.on('fooEventThatShouldTriggerPagination', () => $paginator.paginate(1));
*```
* You can also provide the starting page to initiate it automatically on page load:
*```
* $paginator new DynamicPaginator(
* '#foo-container',
* FooDataService,
* FooRenderer,
* 1
* );
*```
* There is also a possibility to provide custom selectorsMap as 5th argument. See this.setSelectorsMap().
*
* Pagination service must have a method fetch(offset, limit) which returns data.{any resources name} & data.total
* e.g.
* ```
* class FooDataService {
* fetch(offset, limit) {
* return $.get(this.router.generate('admin_products_combinations', {
* productId: this.productId,
* page,
* limit,
* }));
* }
* }
*```
* * In this case the action of route `admin_products_combinations` returns following json:
* ```
* {
* total: 100,
* combinations: [{combinationId: 1, name: foo...}, {combinationId: 2, name: bar...}]
* }
*```
*
* The renderer must have a method render(data) which accepts the data from PaginationService
* and renders it depending on needs
*/
export default class DynamicPaginator {
/**
* @param {String} containerSelector
* @param {Object} paginationService
* @param {Object} renderer
* @param {Number|null} startingPage If provided it will load the provided page data on page load
* @param {Object|null} selectorsMap If provided it will override css selectors used for all the actions.
*/
constructor(containerSelector, paginationService, renderer, startingPage = null, selectorsMap = null) {
this.$paginationContainer = $(containerSelector);
this.paginationService = paginationService;
this.renderer = renderer;
this.setSelectorsMap(selectorsMap);
this.init();
this.currentPage = 1;
if (startingPage !== null) {
this.paginate(startingPage);
}
return {
paginate: (page) => this.paginate(page),
getCurrentPage: () => this.currentPage,
};
}
/**
* Initiates the pagination component
*
* @private
*/
init() {
this.$paginationContainer.on('click', this.selectorsMap.pageLink, (e) => {
this.paginate(Number($(e.currentTarget).data('page')));
});
this.$paginationContainer.find(this.selectorsMap.jumpToPageInput).keypress((e) => {
if (e.which === 13) {
e.preventDefault();
const page = this.getValidPageNumber(Number(e.currentTarget.value));
this.paginate(page);
}
});
this.$paginationContainer.on('change', this.selectorsMap.limitSelect, () => {
this.paginate(1);
});
}
/**
* @param {Number} page
*/
async paginate(page) {
this.currentPage = page;
this.renderer.toggleLoading(true);
const limit = this.getLimit();
const data = await this.paginationService.fetch(this.calculateOffset(page, limit), limit);
$(this.selectorsMap.jumpToPageInput).val(page);
this.countPages(data.total);
this.refreshButtonsData(page);
this.refreshInfoLabel(page, data.total);
this.toggleTargetAvailability(this.selectorsMap.firstPageItem, page > 1);
this.toggleTargetAvailability(this.selectorsMap.previousPageItem, page > 1);
this.toggleTargetAvailability(this.selectorsMap.nextPageItem, page < this.pagesCount);
this.toggleTargetAvailability(this.selectorsMap.lastPageItem, page < this.pagesCount);
this.renderer.render(data);
this.renderer.toggleLoading(false);
window.prestaShopUiKit.initToolTips();
}
/**
* @param page
* @param limit
*
* @returns {Number}
*/
calculateOffset(page, limit) {
return (page - 1) * limit;
}
/**
* @param {Number} page
*
* @private
*/
refreshButtonsData(page) {
this.$paginationContainer.find(this.selectorsMap.nextPageBtn).data('page', page + 1);
this.$paginationContainer.find(this.selectorsMap.previousPageBtn).data('page', page - 1);
this.$paginationContainer.find(this.selectorsMap.lastPageBtn).data('page', this.pagesCount);
}
/**
* @param {Number} page
* @param {Number} total
*/
refreshInfoLabel(page, total) {
const infoLabel = this.$paginationContainer.find(this.selectorsMap.paginationInfoLabel);
const limit = this.getLimit();
const from = page === 1 ? 1 : Math.round((page - 1) * limit);
const to = page === this.pagesCount ? total : Math.round(page * limit);
const modifiedInfoText = infoLabel
.data('pagination-info')
.replace(/%from%/g, from)
.replace(/%to%/g, to)
.replace(/%total%/g, total)
.replace(/%current_page%/g, page)
.replace(/%page_count%/g, this.pagesCount);
infoLabel.text(modifiedInfoText);
}
/**
* @param {String} targetSelector
* @param {Boolean} enable
*
* @private
*/
toggleTargetAvailability(targetSelector, enable) {
const target = this.$paginationContainer.find(targetSelector);
if (enable) {
target.removeClass('disabled');
} else {
target.addClass('disabled');
}
}
/**
* @param {Number} total
*
* @private
*/
countPages(total) {
this.pagesCount = Math.ceil(total / this.getLimit());
const lastPageItem = this.$paginationContainer.find(this.selectorsMap.lastPageBtn);
lastPageItem.data('page', this.pagesCount);
lastPageItem.text(this.pagesCount);
}
/**
* @returns {Number}
*
* @private
*/
getLimit() {
return this.$paginationContainer.find(this.selectorsMap.limitSelect).val();
}
/**
*
* @param page
*
* @returns {Number}
*/
getValidPageNumber(page) {
if (page > this.pagesCount) {
return this.pagesCount;
}
if (page < 1) {
return 1;
}
return page;
}
/**
* @param {Object} selectorsMap
*/
setSelectorsMap(selectorsMap) {
if (selectorsMap) {
this.selectorsMap = selectorsMap;
return;
}
this.selectorsMap = {
jumpToPageInput: 'input[name="paginator-jump-page"]',
firstPageBtn: 'button.page-link.first',
firstPageItem: 'li.page-item.first',
nextPageBtn: 'button.page-link.next',
nextPageItem: 'li.page-item.next',
previousPageBtn: 'button.page-link.previous',
previousPageItem: 'li.page-item.previous',
lastPageItem: 'li.page-item.last',
lastPageBtn: 'button.page-link.last',
pageLink: 'button.page-link',
limitSelect: '#paginator-limit',
paginationInfoLabel: '#pagination-info',
};
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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)
*/
/**
* Class responsible for checking password's validity.
* Can validate entered password's length against min/max values.
* If password confirmation input is provided, can validate if entered password is matching confirmation.
*/
export default class PasswordValidator {
/**
* @param {String} passwordInputSelector selector of the password input.
* @param {String|null} confirmPasswordInputSelector (optional) selector for the password confirmation input.
* @param {Object} options allows overriding default options.
*/
constructor(passwordInputSelector, confirmPasswordInputSelector = null, options = {}) {
this.newPasswordInput = document.querySelector(passwordInputSelector);
this.confirmPasswordInput = document.querySelector(confirmPasswordInputSelector);
// Minimum allowed length for entered password
this.minPasswordLength = options.minPasswordLength || 8;
// Maximum allowed length for entered password
this.maxPasswordLength = options.maxPasswordLength || 255;
}
/**
* Check if the password is valid.
*
* @returns {boolean}
*/
isPasswordValid() {
if (this.confirmPasswordInput && !this.isPasswordMatchingConfirmation()) {
return false;
}
return this.isPasswordLengthValid();
}
/**
* Check if password's length is valid.
*
* @returns {boolean}
*/
isPasswordLengthValid() {
return !this.isPasswordTooShort() && !this.isPasswordTooLong();
}
/**
* Check if password is matching it's confirmation.
*
* @returns {boolean}
*/
isPasswordMatchingConfirmation() {
if (!this.confirmPasswordInput) {
throw new Error('Confirm password input is not provided for the password validator.');
}
if (this.confirmPasswordInput.value === '') {
return true;
}
return this.newPasswordInput.value === this.confirmPasswordInput.value;
}
/**
* Check if password is too short.
*
* @returns {boolean}
*/
isPasswordTooShort() {
return this.newPasswordInput.value.length < this.minPasswordLength;
}
/**
* Check if password is too long.
*
* @returns {boolean}
*/
isPasswordTooLong() {
return this.newPasswordInput.value.length > this.maxPasswordLength;
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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 Routing from 'fos-routing';
import routes from '@js/fos_js_routes.json';
const {$} = window;
/**
* Wraps FOSJsRoutingbundle with exposed routes.
* To expose route add option `expose: true` in .yml routing config
*
* e.g.
*
* `my_route
* path: /my-path
* options:
* expose: true
* `
* And run `bin/console fos:js-routing:dump --format=json --target=admin-dev/themes/new-theme/js/fos_js_routes.json`
*/
export default class Router {
constructor() {
if (window.prestashop && window.prestashop.customRoutes) {
Object.assign(routes.routes, window.prestashop.customRoutes);
}
Routing.setData(routes);
Routing.setBaseUrl(
$(document)
.find('body')
.data('base-url'),
);
return this;
}
/**
* Decorated "generate" method, with predefined security token in params
*
* @param route
* @param params
*
* @returns {String}
*/
generate(route, params = {}) {
const tokenizedParams = Object.assign(params, {
_token: $(document)
.find('body')
.data('token'),
});
return Routing.generate(route, tokenizedParams);
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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;
/**
* Class ShowcaseCardCloseExtension is responsible for providing helper block closing behavior
*/
export default class ShowcaseCardCloseExtension {
/**
* Extend helper block.
*
* @param {ShowcaseCard} helperBlock
*/
extend(helperBlock) {
const container = helperBlock.getContainer();
container.on('click', '.js-remove-helper-block', (evt) => {
container.remove();
const $btn = $(evt.target);
const url = $btn.data('closeUrl');
const cardName = $btn.data('cardName');
if (url) {
// notify the card was closed
$.post(
url,
{
close: 1,
name: cardName,
},
);
}
});
}
}

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;
/**
* Class ShowcaseCard is responsible for handling events related with showcase card.
*/
export default class ShowcaseCard {
/**
* Showcase card id.
*
* @param {string} id
*/
constructor(id) {
this.id = id;
this.$container = $(`#${this.id}`);
}
/**
* Get showcase card container.
*
* @returns {jQuery}
*/
getContainer() {
return this.$container;
}
/**
* Extend showcase card with external extensions.
*
* @param {object} extension
*/
addExtension(extension) {
extension.extend(this);
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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;
/**
* class TaggableField is responsible for providing functionality from bootstrap-tokenfield plugin.
* It allows to have taggable fields which are split in separate blocks once you click enter. Values originally saved
* in comma split strings.
*/
export default class TaggableField {
/**
* @param {string} tokenFieldSelector - a selector which is used within jQuery object.
* @param {object} options - extends basic tokenField behavior with additional options such as minLength, delimiter,
* allow to add token on focus out action. See bootstrap-tokenfield docs for more information.
*/
constructor({tokenFieldSelector, options = {}}) {
$(tokenFieldSelector).tokenfield(options);
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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 which allows to copy regular text to url friendly text
*
* Usage example in template:
*
* <input name="source-input"
* class="js-link-rewrite-copier-source"> // The original text will be taken from this element
* <input name="destination-input"
* class="js-link-rewrite-copier-destination"> // Modified text will be added to this input
*
* in javascript:
*
* textToLinkRewriteCopier({
* sourceElementSelector: '.js-link-rewrite-copier-source'
* destinationElementSelector: '.js-link-rewrite-copier-destination',
* });
*
* If the source-input has value "test name" the link rewrite value will be "test-name".
* If the source-input has value "test name #$" link rewrite will be "test-name-" since #$
* are un allowed characters in url.
*
* You can also pass additional options to change the event name, or encoding format:
*
* textToLinkRewriteCopier({
* sourceElementSelector: '.js-link-rewrite-copier-source'
* destinationElementSelector: '.js-link-rewrite-copier-destination',
* options: {
* eventName: 'change', // default is 'input'
* }
* });
*
*/
const textToLinkRewriteCopier = (
{
sourceElementSelector,
destinationElementSelector,
options = {eventName: 'input'},
},
) => {
$(document).on(options.eventName, `${sourceElementSelector}`, (event) => {
$(destinationElementSelector).val(window.str2url($(event.currentTarget).val(), 'UTF-8'));
});
};
export default textToLinkRewriteCopier;

View File

@@ -0,0 +1,285 @@
/**
* 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 {EventEmitter} from './event-emitter';
const {$} = window;
/**
* This class init TinyMCE instances in the back-office. It is wildly inspired by
* the scripts from js/admin And it actually loads TinyMCE from the js/tiny_mce
* folder along with its modules. One improvement could be to install TinyMCE via
* npm and fully integrate in the back-office theme.
*/
class TinyMCEEditor {
constructor(options) {
const opts = options || {};
this.tinyMCELoaded = false;
if (typeof opts.baseAdminUrl === 'undefined') {
if (typeof window.baseAdminDir !== 'undefined') {
opts.baseAdminUrl = window.baseAdminDir;
} else {
const pathParts = window.location.pathname.split('/');
pathParts.every((pathPart) => {
if (pathPart !== '') {
opts.baseAdminUrl = `/${pathPart}/`;
return false;
}
return true;
});
}
}
if (typeof opts.langIsRtl === 'undefined') {
opts.langIsRtl = typeof window.lang_is_rtl !== 'undefined' ? window.lang_is_rtl === '1' : false;
}
this.setupTinyMCE(opts);
}
/**
* Initial setup which checks if the tinyMCE library is already loaded.
*
* @param config
*/
setupTinyMCE(config) {
if (typeof tinyMCE === 'undefined') {
this.loadAndInitTinyMCE(config);
} else {
this.initTinyMCE(config);
}
}
/**
* Prepare the config and init all TinyMCE editors
*
* @param config
*/
initTinyMCE(config) {
const cfg = {
selector: '.rte',
plugins: 'align colorpicker link image filemanager table media placeholder lists advlist code table autoresize',
browser_spellcheck: true,
toolbar1:
/* eslint-disable-next-line max-len */
'code,colorpicker,bold,italic,underline,strikethrough,blockquote,link,align,bullist,numlist,table,image,media,formatselect',
toolbar2: '',
language: window.iso_user,
external_filemanager_path: `${config.baseAdminUrl}filemanager/`,
filemanager_title: 'File manager',
external_plugins: {
filemanager: `${config.baseAdminUrl}filemanager/plugin.min.js`,
},
content_style: config.langIsRtl ? 'body {direction:rtl;}' : '',
skin: 'prestashop',
mobile: {
theme: 'mobile',
plugins: ['lists', 'align', 'link', 'table', 'placeholder', 'advlist', 'code'],
toolbar:
/* eslint-disable-next-line max-len */
'undo code colorpicker bold italic underline strikethrough blockquote link align bullist numlist table formatselect styleselect',
},
menubar: false,
statusbar: false,
relative_urls: false,
convert_urls: false,
entity_encoding: 'raw',
extended_valid_elements: 'em[class|name|id],@[role|data-*|aria-*]',
valid_children: '+*[*]',
valid_elements: '*[*]',
rel_list: [{title: 'nofollow', value: 'nofollow'}],
editor_selector: 'autoload_rte',
init_instance_callback: () => {
this.changeToMaterial();
},
setup: (editor) => {
this.setupEditor(editor);
},
...config,
};
if (typeof window.defaultTinyMceConfig !== 'undefined') {
Object.assign(cfg, window.defaultTinyMceConfig);
}
if (typeof cfg.editor_selector !== 'undefined') {
cfg.selector = `.${cfg.editor_selector}`;
}
// Change icons in popups
$('body').on('click', '.mce-btn, .mce-open, .mce-menu-item', () => {
this.changeToMaterial();
});
window.tinyMCE.init(cfg);
this.watchTabChanges(cfg);
}
/**
* Setup TinyMCE editor once it has been initialized
*
* @param editor
*/
setupEditor(editor) {
editor.on('loadContent', (event) => {
this.handleCounterTiny(event.target.id);
});
editor.on('change', (event) => {
window.tinyMCE.triggerSave();
this.handleCounterTiny(event.target.id);
});
editor.on('blur', () => {
window.tinyMCE.triggerSave();
});
EventEmitter.emit('tinymceEditorSetup', {
editor,
});
}
/**
* When the editor is inside a tab it can cause a bug on tab switching.
* So we check if the editor is contained in a navigation and refresh the editor when its
* parent tab is shown.
*
* @param config
*/
watchTabChanges(config) {
$(config.selector).each((index, textarea) => {
const translatedField = $(textarea).closest('.translation-field');
const tabContainer = $(textarea).closest('.translations.tabbable');
if (translatedField.length && tabContainer.length) {
const textareaLocale = translatedField.data('locale');
const textareaLinkSelector = `.nav-item a[data-locale="${textareaLocale}"]`;
$(textareaLinkSelector, tabContainer).on('shown.bs.tab', () => {
const form = $(textarea).closest('form');
const editor = window.tinyMCE.get(textarea.id);
if (editor) {
// Reset content to force refresh of editor
editor.setContent(editor.getContent());
}
EventEmitter.emit('languageSelected', {
selectedLocale: textareaLocale,
form,
});
});
}
});
EventEmitter.on('languageSelected', (data) => {
const textareaLinkSelector = `.nav-item a[data-locale="${data.selectedLocale}"]`;
$(textareaLinkSelector).click();
});
}
/**
* Loads the TinyMCE javascript library and then init the editors
*
* @param config
*/
loadAndInitTinyMCE(config) {
if (this.tinyMCELoaded) {
return;
}
this.tinyMCELoaded = true;
const pathArray = config.baseAdminUrl.split('/');
pathArray.splice(pathArray.length - 2, 2);
const finalPath = pathArray.join('/');
window.tinyMCEPreInit = {};
window.tinyMCEPreInit.base = `${finalPath}/js/tiny_mce`;
window.tinyMCEPreInit.suffix = '.min';
$.getScript(`${finalPath}/js/tiny_mce/tinymce.min.js`, () => {
this.setupTinyMCE(config);
});
}
/**
* Replace initial TinyMCE icons with material icons
*/
changeToMaterial() {
const materialIconAssoc = {
'mce-i-code': '<i class="material-icons">code</i>',
'mce-i-none': '<i class="material-icons">format_color_text</i>',
'mce-i-bold': '<i class="material-icons">format_bold</i>',
'mce-i-italic': '<i class="material-icons">format_italic</i>',
'mce-i-underline': '<i class="material-icons">format_underlined</i>',
'mce-i-strikethrough': '<i class="material-icons">format_strikethrough</i>',
'mce-i-blockquote': '<i class="material-icons">format_quote</i>',
'mce-i-link': '<i class="material-icons">link</i>',
'mce-i-alignleft': '<i class="material-icons">format_align_left</i>',
'mce-i-aligncenter': '<i class="material-icons">format_align_center</i>',
'mce-i-alignright': '<i class="material-icons">format_align_right</i>',
'mce-i-alignjustify': '<i class="material-icons">format_align_justify</i>',
'mce-i-bullist': '<i class="material-icons">format_list_bulleted</i>',
'mce-i-numlist': '<i class="material-icons">format_list_numbered</i>',
'mce-i-image': '<i class="material-icons">image</i>',
'mce-i-table': '<i class="material-icons">grid_on</i>',
'mce-i-media': '<i class="material-icons">video_library</i>',
'mce-i-browse': '<i class="material-icons">attachment</i>',
'mce-i-checkbox': '<i class="mce-ico mce-i-checkbox"></i>',
};
$.each(materialIconAssoc, (index, value) => {
$(`.${index}`).replaceWith(value);
});
}
/**
* Updates the characters counter. This counter is used for front but if you don't want to encounter Validation
* problems you should be in sync with the TinyMceMaxLengthValidator PHP class. Both codes must behave the same
* way.
*
* @param id
*/
handleCounterTiny(id) {
const textarea = $(`#${id}`);
const counter = textarea.attr('counter');
const counterType = textarea.attr('counter_type');
const editor = window.tinyMCE.get(id);
const max = editor.getBody() ? editor.getBody().textContent.length : 0;
textarea
.parent()
.find('span.currentLength')
.text(max);
if (counterType !== 'recommended' && max > counter) {
textarea
.parent()
.find('span.maxLength')
.addClass('text-danger');
} else {
textarea
.parent()
.find('span.maxLength')
.removeClass('text-danger');
}
}
}
export default TinyMCEEditor;

View File

@@ -0,0 +1,110 @@
/**
* 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 {EventEmitter} from './event-emitter';
const {$} = window;
/**
* This class is used to automatically toggle translated fields (displayed with tabs
* using the TranslateType Symfony form type).
* Also compatible with TranslatableInput changes.
*/
class TranslatableField {
constructor(options) {
const opts = options || {};
this.localeButtonSelector = opts.localeButtonSelector || '.translationsLocales.nav .nav-item a[data-toggle="tab"]';
this.localeNavigationSelector = opts.localeNavigationSelector || '.translationsLocales.nav';
this.translationFieldSelector = opts.translationFieldSelector || '.translation-field';
this.selectedLocale = $('.nav-item a.active', $(this.localeNavigationSelector)).data('locale');
$('body').on('shown.bs.tab', this.localeButtonSelector, this.toggleLanguage.bind(this));
EventEmitter.on('languageSelected', this.toggleFields.bind(this));
return {
localeButtonSelector: this.localeButtonSelector,
localeNavigationSelector: this.localeNavigationSelector,
translationFieldSelector: this.translationFieldSelector,
/**
* @param {jQuery} form
*/
refreshFormInputs: (form) => { this.refreshInputs(form); },
/**
* @returns {string|undefined}
*/
getSelectedLocale: () => this.selectedLocale,
};
}
/**
* @param form
*
* @private
*/
refreshInputs(form) {
EventEmitter.emit('languageSelected', {
selectedLocale: this.selectedLocale,
form,
});
}
/**
* Dispatch event on language selection to update inputs and other components which depend on the locale.
*
* @param event
*
* @private
*/
toggleLanguage(event) {
const localeLink = $(event.target);
const form = localeLink.closest('form');
this.selectedLocale = localeLink.data('locale');
this.refreshInputs(form);
}
/**
* Toggle all transtation fields to the selected locale
*
* @param event
*
* @private
*/
toggleFields(event) {
this.selectedLocale = event.selectedLocale;
$(this.localeNavigationSelector).each((index, navigation) => {
const selectedLink = $('.nav-item a.active', navigation);
const selectedLocale = selectedLink.data('locale');
if (this.selectedLocale !== selectedLocale) {
$(`.nav-item a[data-locale="${this.selectedLocale}"]`, navigation).tab('show');
}
});
}
}
export default TranslatableField;

View File

@@ -0,0 +1,140 @@
/**
* 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 {EventEmitter} from './event-emitter';
const {$} = window;
/**
* This class is used to automatically toggle translated inputs (displayed with one
* input and a language selector using the TranslatableType Symfony form type).
* Also compatible with TranslatableField changes.
*/
class TranslatableInput {
constructor(options) {
const opts = options || {};
this.localeItemSelector = opts.localeItemSelector || '.js-locale-item';
this.localeButtonSelector = opts.localeButtonSelector || '.js-locale-btn';
this.localeInputSelector = opts.localeInputSelector || '.js-locale-input';
this.selectedLocale = $(this.localeItemSelector).data('locale');
$('body').on(
'click',
this.localeItemSelector,
this.toggleLanguage.bind(this),
);
EventEmitter.on('languageSelected', this.toggleInputs.bind(this));
return {
localeItemSelector: this.localeItemSelector,
localeButtonSelector: this.localeButtonSelector,
localeInputSelector: this.localeInputSelector,
/**
* @param {jQuery} form
*/
refreshFormInputs: (form) => { this.refreshInputs(form); },
/**
* @returns {string|undefined}
*/
getSelectedLocale: () => this.selectedLocale,
};
}
/**
* @param {jQuery} form
*
* @private
*/
refreshInputs(form) {
if (!this.selectedLocale) {
return;
}
EventEmitter.emit('languageSelected', {
selectedLocale: this.selectedLocale,
form,
});
}
/**
* Dispatch event on language selection to update inputs and other components which depend on the locale.
*
* @param event
*
* @private
*/
toggleLanguage(event) {
const localeItem = $(event.target);
const form = localeItem.closest('form');
this.selectedLocale = localeItem.data('locale');
this.refreshInputs(form);
}
/**
* Toggle all translatable inputs in form in which locale was changed
*
* @param {Event} event
*
* @private
*/
toggleInputs(event) {
const {form} = event;
this.selectedLocale = event.selectedLocale;
const localeButton = form.find(this.localeButtonSelector);
const changeLanguageUrl = localeButton.data('change-language-url');
localeButton.text(this.selectedLocale);
form.find(this.localeInputSelector).addClass('d-none');
form
.find(`${this.localeInputSelector}.js-locale-${this.selectedLocale}`)
.removeClass('d-none');
if (changeLanguageUrl) {
this.saveSelectedLanguage(changeLanguageUrl, this.selectedLocale);
}
}
/**
* Save language choice for employee forms.
*
* @param {String} changeLanguageUrl
* @param {String} selectedLocale
*
* @private
*/
saveSelectedLanguage(changeLanguageUrl, selectedLocale) {
$.post({
url: changeLanguageUrl,
data: {
language_iso_code: selectedLocale,
},
});
}
}
export default TranslatableInput;