update
This commit is contained in:
43
iadmin/themes/new-theme/js/pages/product/combination/edit.js
Normal file
43
iadmin/themes/new-theme/js/pages/product/combination/edit.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 ProductSuppliersManager from '@pages/product/edit/product-suppliers-manager';
|
||||
import ImageSelector from '@pages/product/combination/image-selector';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
$(() => {
|
||||
window.prestashop.component.initComponents([
|
||||
'TranslatableField',
|
||||
'TinyMCEEditor',
|
||||
'TranslatableInput',
|
||||
'EventEmitter',
|
||||
'TextWithLengthCounter',
|
||||
]);
|
||||
|
||||
new ProductSuppliersManager(ProductMap.suppliers.combinationSuppliers, false);
|
||||
new ImageSelector();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class ImageSelector {
|
||||
constructor() {
|
||||
this.$selectorContainer = $(ProductMap.combinations.images.selectorContainer);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
$(ProductMap.combinations.images.checkboxContainer, this.$selectorContainer).hide();
|
||||
this.$selectorContainer.on('click', ProductMap.combinations.images.imageChoice, (event) => {
|
||||
const $imageChoice = $(event.currentTarget);
|
||||
const $checkbox = $(ProductMap.combinations.images.checkbox, $imageChoice);
|
||||
|
||||
const isChecked = $checkbox.prop('checked');
|
||||
$imageChoice.toggleClass('selected', !isChecked);
|
||||
$checkbox.prop('checked', !isChecked);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 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 _ from 'lodash';
|
||||
|
||||
import AutoCompleteSearch from '@components/auto-complete-search';
|
||||
import Tokenizers from '@components/bloodhound/tokenizers';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import {getCategories} from '@pages/product/services/categories';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
const ProductCategoryMap = ProductMap.categories;
|
||||
|
||||
export default class CategoriesManager {
|
||||
/**
|
||||
* @param {EventEmitter} eventEmitter
|
||||
* @returns {{}}
|
||||
*/
|
||||
constructor(eventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.categoriesContainer = document.querySelector(
|
||||
ProductCategoryMap.categoriesContainer,
|
||||
);
|
||||
this.categories = [];
|
||||
this.typeaheadDatas = [];
|
||||
this.categoryTree = this.categoriesContainer.querySelector(ProductCategoryMap.categoryTree);
|
||||
this.prototypeTemplate = this.categoryTree.dataset.prototype;
|
||||
this.prototypeName = this.categoryTree.dataset.prototypeName;
|
||||
this.expandAllButton = this.categoriesContainer.querySelector(ProductCategoryMap.expandAllButton);
|
||||
this.reduceAllButton = this.categoriesContainer.querySelector(ProductCategoryMap.reduceAllButton);
|
||||
|
||||
this.initCategories();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async initCategories() {
|
||||
this.categories = await getCategories();
|
||||
|
||||
// This regexp is gonna be used to get id from checkbox name
|
||||
let regexpString = ProductCategoryMap.checkboxName('__REGEXP__');
|
||||
regexpString = _.escapeRegExp(regexpString).replace('__REGEXP__', '([0-9]+)');
|
||||
this.checkboxIdRegexp = new RegExp(regexpString);
|
||||
|
||||
// This regexp is gonna be used to get id from radio name
|
||||
regexpString = ProductCategoryMap.radioName('__REGEXP__');
|
||||
regexpString = _.escapeRegExp(regexpString).replace('__REGEXP__', '([0-9]+)');
|
||||
this.radioIdRegexp = new RegExp(regexpString);
|
||||
|
||||
this.initTypeaheadData(this.categories, '');
|
||||
this.initTypeahead();
|
||||
this.initTree();
|
||||
this.updateCategoriesTags();
|
||||
}
|
||||
|
||||
initTree() {
|
||||
const initialElements = {};
|
||||
|
||||
this.categoryTree.querySelectorAll(ProductCategoryMap.treeElement).forEach((treeElement) => {
|
||||
const checkboxInput = treeElement.querySelector(ProductCategoryMap.checkboxInput);
|
||||
const categoryId = this.getIdFromCheckbox(checkboxInput);
|
||||
initialElements[categoryId] = treeElement;
|
||||
});
|
||||
|
||||
this.categories.forEach((category) => {
|
||||
const item = this.generateCategoryTree(category, initialElements);
|
||||
this.categoryTree.append(item);
|
||||
});
|
||||
|
||||
this.expandAllButton.addEventListener('click', () => {
|
||||
this.toggleAll(true);
|
||||
});
|
||||
this.reduceAllButton.addEventListener('click', () => {
|
||||
this.toggleAll(false);
|
||||
});
|
||||
|
||||
this.categoryTree.querySelectorAll(ProductCategoryMap.checkboxInput).forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', (event) => {
|
||||
const checkboxInput = event.currentTarget;
|
||||
const parentItem = checkboxInput.parentNode.closest(ProductCategoryMap.treeElement);
|
||||
const radioInput = parentItem.querySelector(ProductCategoryMap.radioInput);
|
||||
|
||||
// If checkbox is associated to the default radio input it cannot be unchecked
|
||||
if (radioInput.checked) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.updateCheckbox(checkboxInput, true);
|
||||
} else {
|
||||
this.updateCategoriesTags();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.categoryTree.querySelectorAll(ProductCategoryMap.radioInput).forEach((radioInput) => {
|
||||
radioInput.addEventListener('click', () => {
|
||||
this.selectedDefaultCategory(radioInput);
|
||||
});
|
||||
if (radioInput.checked) {
|
||||
this.updateDefaultCheckbox(radioInput);
|
||||
}
|
||||
});
|
||||
|
||||
// Tree is initialized we can show it and hide loader
|
||||
this.categoriesContainer
|
||||
.querySelector(ProductCategoryMap.fieldset)
|
||||
.classList.remove('d-none');
|
||||
this.categoriesContainer
|
||||
.querySelector(ProductCategoryMap.loader)
|
||||
.classList.add('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to recursively create items of the category tree
|
||||
*
|
||||
* @param {Object} category
|
||||
* @param {Object} initialElements
|
||||
*/
|
||||
generateCategoryTree(category, initialElements) {
|
||||
const categoryNode = this.generateTreeElement(category, initialElements);
|
||||
const childrenList = categoryNode.querySelector(ProductCategoryMap.childrenList);
|
||||
childrenList.classList.add('d-none');
|
||||
|
||||
const hasChildren = category.children && category.children.length > 0;
|
||||
categoryNode.classList.toggle('more', hasChildren);
|
||||
if (hasChildren) {
|
||||
const inputsContainer = categoryNode.querySelector(ProductCategoryMap.treeElementInputs);
|
||||
inputsContainer.addEventListener('click', (event) => {
|
||||
// We don't want to mess with the inputs behaviour (no toggle when checkbox or radio is clicked)
|
||||
// So we only toggle when the div itself is clicked.
|
||||
if (event.target !== event.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExpanded = !childrenList.classList.contains('d-none');
|
||||
categoryNode.classList.toggle('less', !isExpanded);
|
||||
categoryNode.classList.toggle('more', isExpanded);
|
||||
childrenList.classList.toggle('d-none', isExpanded);
|
||||
});
|
||||
|
||||
// Recursively build the children trees
|
||||
category.children.forEach((childCategory) => {
|
||||
const childTree = this.generateCategoryTree(childCategory, initialElements);
|
||||
|
||||
childrenList.append(childTree);
|
||||
});
|
||||
}
|
||||
|
||||
return categoryNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the category is among the initial ones (inserted by the form on load) the existing element is used,
|
||||
* if not then it is generated based on the prototype template. In both case the element is injected with the
|
||||
* category name and click on radio is handled.
|
||||
*
|
||||
* @param {Object} category
|
||||
* @param {Object} initialElements
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
generateTreeElement(category, initialElements) {
|
||||
let categoryNode;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(initialElements, category.id)) {
|
||||
const template = this.prototypeTemplate.replace(new RegExp(this.prototypeName, 'g'), category.id);
|
||||
// Trim is important here or the first child could be some text (whitespace, or \n)
|
||||
const frag = document.createRange().createContextualFragment(template.trim());
|
||||
categoryNode = frag.firstChild;
|
||||
} else {
|
||||
categoryNode = initialElements[category.id];
|
||||
}
|
||||
|
||||
// Add category name as a text between the checkbox and the radio
|
||||
const checkboxInput = categoryNode.querySelector(ProductCategoryMap.checkboxInput);
|
||||
checkboxInput.parentNode.insertBefore(
|
||||
document.createTextNode(category.name),
|
||||
checkboxInput,
|
||||
);
|
||||
|
||||
return categoryNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} radioInput
|
||||
*/
|
||||
selectedDefaultCategory(radioInput) {
|
||||
// Uncheck all other radio inputs when one is selected
|
||||
this.categoryTree.querySelectorAll(ProductCategoryMap.radioInput).forEach((radioTreeElement) => {
|
||||
if (radioTreeElement !== radioInput) {
|
||||
radioTreeElement.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.categoryTree.querySelectorAll(ProductCategoryMap.checkboxInput).forEach((checkboxTreeElement) => {
|
||||
const materialCheckbox = checkboxTreeElement.parentNode.closest(ProductCategoryMap.materialCheckbox);
|
||||
materialCheckbox.classList.remove('disabled');
|
||||
});
|
||||
|
||||
this.updateDefaultCheckbox(radioInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} radioInput
|
||||
*/
|
||||
updateDefaultCheckbox(radioInput) {
|
||||
// If the element is selected as default it is also associated by definition
|
||||
const parentItem = radioInput.parentNode.closest(ProductCategoryMap.treeElement);
|
||||
const checkbox = parentItem.querySelector(ProductCategoryMap.checkboxInput);
|
||||
|
||||
// A default category is necessarily associated, so displayed as disabled (we do not use the disabled
|
||||
// attribute because it removes the data from the form).
|
||||
const materialCheckbox = checkbox.parentNode.closest(ProductCategoryMap.materialCheckbox);
|
||||
materialCheckbox.classList.add('disabled');
|
||||
|
||||
this.updateCheckbox(checkbox, true);
|
||||
this.updateCategoriesTags();
|
||||
this.eventEmitter.emit(ProductEventMap.updateSubmitButtonState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand/reduce the category tree
|
||||
*
|
||||
* @param {boolean} expanded Force expanding instead of toggle
|
||||
*/
|
||||
toggleAll(expanded) {
|
||||
this.expandAllButton.style.display = expanded ? 'none' : 'block';
|
||||
this.reduceAllButton.style.display = !expanded ? 'none' : 'block';
|
||||
|
||||
this.categoriesContainer
|
||||
.querySelectorAll(ProductCategoryMap.childrenList)
|
||||
.forEach((e) => {
|
||||
e.classList.toggle('d-none', !expanded);
|
||||
});
|
||||
|
||||
this.categoriesContainer
|
||||
.querySelectorAll(ProductCategoryMap.everyItems)
|
||||
.forEach((e) => {
|
||||
e.classList.toggle('more', !expanded);
|
||||
e.classList.toggle('less', expanded);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the selected category (matched by its ID) and toggle the tree by going up through the category's ancestors.
|
||||
*
|
||||
* @param {int} categoryId
|
||||
*/
|
||||
selectCategory(categoryId) {
|
||||
const checkbox = this.categoriesContainer.querySelector(
|
||||
`[name="${ProductCategoryMap.checkboxName(categoryId)}"]`,
|
||||
);
|
||||
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
this.updateCheckbox(checkbox, true);
|
||||
this.openCategoryParents(checkbox);
|
||||
this.updateCategoriesTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} checkbox
|
||||
*/
|
||||
openCategoryParents(checkbox) {
|
||||
// This is the element containing the checkbox
|
||||
let parentItem = checkbox.closest(ProductCategoryMap.treeElement);
|
||||
|
||||
if (parentItem !== null) {
|
||||
// This is the first (potential) parent element
|
||||
parentItem = parentItem.parentNode.closest(ProductCategoryMap.treeElement);
|
||||
}
|
||||
|
||||
while (parentItem !== null && this.categoryTree.contains(parentItem)) {
|
||||
const childrenList = parentItem.querySelector(ProductCategoryMap.childrenList);
|
||||
|
||||
if (childrenList.childNodes.length) {
|
||||
parentItem.classList.add('less');
|
||||
parentItem.classList.remove('more');
|
||||
parentItem.querySelector(ProductCategoryMap.childrenList).classList.remove('d-none');
|
||||
}
|
||||
|
||||
parentItem = parentItem.parentNode.closest(ProductCategoryMap.treeElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} categoryId
|
||||
*/
|
||||
unselectCategory(categoryId) {
|
||||
const checkbox = this.categoriesContainer.querySelector(
|
||||
`[name="${ProductCategoryMap.checkboxName(categoryId)}"]`,
|
||||
);
|
||||
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
this.updateCheckbox(checkbox, false);
|
||||
this.openCategoryParents(checkbox);
|
||||
this.updateCategoriesTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Typeahead data require to have only one array level, we also build the breadcrumb as we go through the
|
||||
* categories.
|
||||
*/
|
||||
initTypeaheadData(data, parentBreadcrumb) {
|
||||
data.forEach((category) => {
|
||||
category.breadcrumb = parentBreadcrumb ? `${parentBreadcrumb} > ${category.name}` : category.name;
|
||||
this.typeaheadDatas.push(category);
|
||||
|
||||
if (category.children) {
|
||||
this.initTypeaheadData(category.children, category.breadcrumb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initTypeahead() {
|
||||
const source = new Bloodhound({
|
||||
datumTokenizer: Tokenizers.obj.letters(
|
||||
'name',
|
||||
'breadcrumb',
|
||||
),
|
||||
queryTokenizer: Bloodhound.tokenizers.nonword,
|
||||
local: this.typeaheadDatas,
|
||||
});
|
||||
|
||||
const dataSetConfig = {
|
||||
source,
|
||||
display: 'breadcrumb',
|
||||
value: 'id',
|
||||
onSelect: (selectedItem, e, $searchInput) => {
|
||||
this.selectCategory(selectedItem.id);
|
||||
|
||||
// This resets the search input or else previous search is cached and can be added again
|
||||
$searchInput.typeahead('val', '');
|
||||
},
|
||||
onClose: (event, $searchInput) => {
|
||||
// This resets the search input or else previous search is cached and can be added again
|
||||
$searchInput.typeahead('val', '');
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
dataSetConfig.templates = {
|
||||
suggestion: (item) => `<div class="px-2">${item.breadcrumb}</div>`,
|
||||
};
|
||||
|
||||
new AutoCompleteSearch($(ProductCategoryMap.searchInput), dataSetConfig);
|
||||
}
|
||||
|
||||
updateCategoriesTags() {
|
||||
const checkedCheckboxes = this.categoryTree.querySelectorAll(ProductCategoryMap.checkedCheckboxInputs);
|
||||
const tagsContainer = this.categoriesContainer.querySelector(ProductCategoryMap.tagsContainer);
|
||||
tagsContainer.innerHTML = '';
|
||||
const defaultCategoryId = this.getDefaultCategoryId();
|
||||
|
||||
checkedCheckboxes.forEach((checkboxInput) => {
|
||||
const categoryId = this.getIdFromCheckbox(checkboxInput);
|
||||
const category = this.getCategoryById(categoryId);
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeCrossTemplate = defaultCategoryId !== categoryId
|
||||
? `<a class="pstaggerClosingCross" href="#" data-id="${category.id}">x</a>`
|
||||
: '';
|
||||
const template = `
|
||||
<span class="pstaggerTag">
|
||||
<span data-id="${category.id}" title="${category.breadcrumb}">${category.name}</span>
|
||||
${removeCrossTemplate}
|
||||
</span>
|
||||
`;
|
||||
|
||||
// Trim is important here or the first child could be some text (whitespace, or \n)
|
||||
const frag = document.createRange().createContextualFragment(template.trim());
|
||||
tagsContainer.append(frag.firstChild);
|
||||
});
|
||||
|
||||
tagsContainer.querySelectorAll('.pstaggerClosingCross').forEach((closeLink) => {
|
||||
closeLink.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const categoryId = Number(event.currentTarget.dataset.id);
|
||||
|
||||
if (categoryId !== defaultCategoryId) {
|
||||
this.unselectCategory(categoryId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tagsContainer.classList.toggle('d-block', checkedCheckboxes.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} categoryId
|
||||
*
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getCategoryById(categoryId) {
|
||||
return this.searchCategory(categoryId, this.categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} categoryId
|
||||
* @param {array} categories
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
searchCategory(categoryId, categories) {
|
||||
let searchedCategory = null;
|
||||
categories.forEach((category) => {
|
||||
if (categoryId === category.id) {
|
||||
searchedCategory = category;
|
||||
}
|
||||
|
||||
if (searchedCategory === null && category.children && category.children.length > 0) {
|
||||
searchedCategory = this.searchCategory(categoryId, category.children);
|
||||
}
|
||||
});
|
||||
|
||||
return searchedCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number|undefined}
|
||||
*/
|
||||
getDefaultCategoryId() {
|
||||
const radioInput = this.categoryTree.querySelector(ProductCategoryMap.defaultRadioInput);
|
||||
|
||||
if (!radioInput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.getIdFromRadio(radioInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} radioInput
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getIdFromRadio(radioInput) {
|
||||
const matches = radioInput.name.match(this.radioIdRegexp);
|
||||
|
||||
return Number(matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} checkboxInput
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getIdFromCheckbox(checkboxInput) {
|
||||
const matches = checkboxInput.name.match(this.checkboxIdRegexp);
|
||||
|
||||
return Number(matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} checkboxInput
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
updateCheckbox(checkboxInput, checked) {
|
||||
if (checkboxInput.checked !== checked) {
|
||||
checkboxInput.checked = checked;
|
||||
this.eventEmitter.emit(ProductEventMap.updateSubmitButtonState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div id="combination-edit-modal">
|
||||
<modal
|
||||
class="combination-modal"
|
||||
v-if="selectedCombinationId !== null"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div
|
||||
class="combination-loading"
|
||||
v-if="loadingCombinationForm"
|
||||
>
|
||||
<div class="spinner" />
|
||||
</div>
|
||||
<iframe
|
||||
ref="iframe"
|
||||
class="combination-iframe"
|
||||
:src="editCombinationUrl"
|
||||
@loadstart="frameLoading"
|
||||
@load="onFrameLoaded"
|
||||
vspace="0"
|
||||
hspace="0"
|
||||
scrolling="auto"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-close"
|
||||
@click.prevent.stop="tryClose"
|
||||
:aria-label="$t('modal.close')"
|
||||
:disabled="submittingCombinationForm"
|
||||
>
|
||||
{{ $t('modal.cancel') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
@click.prevent.stop="showPrevious"
|
||||
:aria-label="$t('modal.previous')"
|
||||
:disabled="
|
||||
previousCombinationId === null || submittingCombinationForm
|
||||
"
|
||||
>
|
||||
<i class="material-icons">keyboard_arrow_left</i>
|
||||
{{ $t('modal.previous') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
@click.prevent.stop="showNext"
|
||||
:aria-label="$t('modal.next')"
|
||||
:disabled="nextCombinationId === null || submittingCombinationForm"
|
||||
>
|
||||
{{ $t('modal.next') }}
|
||||
<i class="material-icons">keyboard_arrow_right</i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click.prevent.stop="submitForm"
|
||||
:aria-label="$t('modal.save')"
|
||||
:disabled="submittingCombinationForm || !isFormUpdated"
|
||||
>
|
||||
<span v-if="!submittingCombinationForm">
|
||||
{{ $t('modal.save') }}
|
||||
</span>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
v-if="submittingCombinationForm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #outside>
|
||||
<history
|
||||
:combinations-list="combinationsHistory"
|
||||
@selectCombination="selectCombination"
|
||||
:selected-combination="selectedCombinationId"
|
||||
:empty-image-url="emptyImageUrl"
|
||||
/>
|
||||
</template>
|
||||
</modal>
|
||||
<div
|
||||
class="modal-prevent-close"
|
||||
@click.prevent.stop="preventClose"
|
||||
>
|
||||
<modal
|
||||
:modal-title="$t('modal.history.confirmTitle')"
|
||||
:cancel-label="$t('modal.cancel')"
|
||||
:confirm-label="$t('modal.confirm')"
|
||||
:close-label="$t('modal.close')"
|
||||
:confirmation="true"
|
||||
v-if="showConfirm"
|
||||
@close="hideConfirmModal"
|
||||
@confirm="confirmSelection"
|
||||
>
|
||||
<template #body>
|
||||
<p
|
||||
v-html="
|
||||
$t('modal.history.confirmBody', {
|
||||
'%combinationName%': selectedCombinationName,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CombinationsService from '@pages/product/services/combinations-service';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import Modal from '@vue/components/Modal';
|
||||
import Router from '@components/router';
|
||||
import History from './History';
|
||||
|
||||
const {$} = window;
|
||||
const CombinationEvents = ProductEventMap.combinations;
|
||||
|
||||
const router = new Router();
|
||||
|
||||
export default {
|
||||
name: 'CombinationModal',
|
||||
components: {Modal, History},
|
||||
data() {
|
||||
return {
|
||||
combinationsService: null,
|
||||
combinationIds: [],
|
||||
selectedCombinationId: null,
|
||||
selectedCombinationName: null,
|
||||
previousCombinationId: null,
|
||||
nextCombinationId: null,
|
||||
editCombinationUrl: '',
|
||||
loadingCombinationForm: false,
|
||||
submittingCombinationForm: false,
|
||||
combinationList: null,
|
||||
hasSubmittedCombinations: false,
|
||||
combinationsHistory: [],
|
||||
showConfirm: false,
|
||||
temporarySelection: null,
|
||||
isFormUpdated: false,
|
||||
isClosing: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
productId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
eventEmitter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
emptyImageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.combinationList = $(ProductMap.combinations.combinationsContainer);
|
||||
this.combinationsService = new CombinationsService(this.productId);
|
||||
this.initCombinationIds();
|
||||
this.watchEditButtons();
|
||||
this.eventEmitter.on(CombinationEvents.refreshCombinationList, () => this.initCombinationIds(),
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
watchEditButtons() {
|
||||
this.combinationList.on(
|
||||
'click',
|
||||
ProductMap.combinations.editCombinationButtons,
|
||||
(event) => {
|
||||
event.stopImmediatePropagation();
|
||||
const $row = $(event.target).closest('tr');
|
||||
this.selectedCombinationId = Number(
|
||||
$row.find(ProductMap.combinations.combinationIdInputsSelector).val(),
|
||||
);
|
||||
this.hasSubmittedCombinations = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
async initCombinationIds() {
|
||||
this.combinationIds = await this.combinationsService.getCombinationIds();
|
||||
},
|
||||
frameLoading() {
|
||||
this.applyIframeStyling();
|
||||
},
|
||||
onFrameLoaded() {
|
||||
this.loadingCombinationForm = false;
|
||||
this.submittingCombinationForm = false;
|
||||
const iframeBody = this.$refs.iframe.contentDocument.body;
|
||||
this.applyIframeStyling();
|
||||
this.selectedCombinationName = iframeBody.querySelector(
|
||||
ProductMap.combinations.combinationName,
|
||||
).innerHTML;
|
||||
|
||||
const iframeInputs = iframeBody.querySelectorAll(
|
||||
ProductMap.combinations.editionFormInputs,
|
||||
);
|
||||
|
||||
iframeInputs.forEach((input) => {
|
||||
input.addEventListener('keyup', () => {
|
||||
this.isFormUpdated = true;
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
this.isFormUpdated = true;
|
||||
});
|
||||
|
||||
this.$refs.iframe.contentDocument.addEventListener('datepickerChange', () => {
|
||||
this.isFormUpdated = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
applyIframeStyling() {
|
||||
this.$refs.iframe.contentDocument.body.style.overflowX = 'hidden';
|
||||
},
|
||||
tryClose() {
|
||||
if (this.isFormUpdated) {
|
||||
this.isClosing = true;
|
||||
|
||||
this.showConfirmModal();
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
if (this.submittingCombinationForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If modifications have been made refresh the combination list
|
||||
if (this.hasSubmittedCombinations) {
|
||||
this.eventEmitter.emit(CombinationEvents.refreshPage);
|
||||
}
|
||||
this.hasSubmittedCombinations = false;
|
||||
|
||||
// This closes the modal which is conditioned to the presence of this value
|
||||
this.selectedCombinationId = null;
|
||||
|
||||
// Reset history on close
|
||||
this.combinationsHistory = [];
|
||||
},
|
||||
navigateToCombination(combinationId) {
|
||||
if (combinationId !== null) {
|
||||
if (this.isFormUpdated) {
|
||||
this.temporarySelection = combinationId;
|
||||
this.showConfirmModal();
|
||||
} else {
|
||||
this.selectedCombinationId = combinationId;
|
||||
}
|
||||
}
|
||||
},
|
||||
showPrevious() {
|
||||
this.navigateToCombination(this.previousCombinationId);
|
||||
},
|
||||
showNext() {
|
||||
this.navigateToCombination(this.nextCombinationId);
|
||||
},
|
||||
selectCombination(combination) {
|
||||
this.navigateToCombination(combination.id);
|
||||
},
|
||||
confirmSelection() {
|
||||
if (this.isClosing) {
|
||||
this.closeModal();
|
||||
this.isClosing = false;
|
||||
this.hideConfirmModal();
|
||||
} else {
|
||||
this.selectedCombinationId = this.temporarySelection;
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.submittingCombinationForm = true;
|
||||
const iframeBody = this.$refs.iframe.contentDocument.body;
|
||||
const editionForm = iframeBody.querySelector(
|
||||
ProductMap.combinations.editionForm,
|
||||
);
|
||||
editionForm.submit();
|
||||
this.hasSubmittedCombinations = true;
|
||||
const selectedCombination = {
|
||||
id: this.selectedCombinationId,
|
||||
title: iframeBody.querySelector(ProductMap.combinations.combinationName)
|
||||
.innerHTML,
|
||||
};
|
||||
|
||||
if (
|
||||
(this.combinationsHistory[0]
|
||||
&& this.combinationsHistory[0].id !== selectedCombination.id)
|
||||
|| !this.combinationsHistory.length
|
||||
) {
|
||||
this.combinationsHistory = this.combinationsHistory.filter(
|
||||
(combination) => combination.id !== selectedCombination.id,
|
||||
);
|
||||
|
||||
this.combinationsHistory.unshift(selectedCombination);
|
||||
}
|
||||
|
||||
this.isFormUpdated = false;
|
||||
},
|
||||
showConfirmModal() {
|
||||
this.showConfirm = true;
|
||||
},
|
||||
hideConfirmModal() {
|
||||
this.isClosing = false;
|
||||
this.showConfirm = false;
|
||||
},
|
||||
preventClose(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedCombinationId(combinationId) {
|
||||
this.isFormUpdated = false;
|
||||
|
||||
if (combinationId === null) {
|
||||
this.previousCombinationId = null;
|
||||
this.nextCombinationId = null;
|
||||
this.editCombinationUrl = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingCombinationForm = true;
|
||||
this.editCombinationUrl = router.generate(
|
||||
'admin_products_combinations_edit_combination',
|
||||
{
|
||||
combinationId,
|
||||
liteDisplaying: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const selectedIndex = this.combinationIds.indexOf(combinationId);
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
this.previousCombinationId = null;
|
||||
this.nextCombinationId = null;
|
||||
} else {
|
||||
this.previousCombinationId = selectedIndex === 0 ? null : this.combinationIds[selectedIndex - 1];
|
||||
this.nextCombinationId = selectedIndex === this.combinationIds.length - 1
|
||||
? null
|
||||
: this.combinationIds[selectedIndex + 1];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import '~@scss/config/_settings.scss';
|
||||
|
||||
#combination-edit-modal .combination-modal {
|
||||
.modal {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
max-width: 990px;
|
||||
width: 90%;
|
||||
height: 95%;
|
||||
margin: 0;
|
||||
|
||||
.modal-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0 1rem;
|
||||
overflow: hidden;
|
||||
|
||||
.modal-body {
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
background: #eaebec;
|
||||
|
||||
.combination-loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.combination-iframe {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
||||
.card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin: 0;
|
||||
padding: 0.6rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn-close {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
min-height: calc(100% - 3.5rem);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 95%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,218 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div
|
||||
class="card history"
|
||||
@click="preventClose"
|
||||
>
|
||||
<div class="card-header">
|
||||
{{
|
||||
$t("modal.history.editedCombination", {
|
||||
"%editedNb%": combinationsList.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="card-block">
|
||||
<ul
|
||||
class="history-list"
|
||||
v-if="areCombinationsNotEmpty"
|
||||
>
|
||||
<li
|
||||
:class="['history-item', isSelected(combination.id)]"
|
||||
v-for="(combination, key) of paginatedDatas[currentPage - 1]"
|
||||
:key="key"
|
||||
@click="selectCombination(combination)"
|
||||
>
|
||||
{{ combination.title }}
|
||||
<i class="material-icons">edit</i>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="history-empty"
|
||||
v-else
|
||||
>
|
||||
<img :src="emptyImageUrl">
|
||||
<p class="history-empty-tip">
|
||||
{{ $t("modal.history.empty") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="card-footer"
|
||||
v-if="areCombinationsNotEmpty"
|
||||
>
|
||||
<pagination
|
||||
:pagination-length="14"
|
||||
:datas="combinationsList"
|
||||
@paginated="constructDatas"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import Pagination from '@vue/components/Pagination';
|
||||
|
||||
const CombinationsEventMap = ProductEventMap.combinations;
|
||||
|
||||
export default {
|
||||
name: 'CombinationHistory',
|
||||
data() {
|
||||
return {
|
||||
paginatedDatas: [],
|
||||
currentPage: 1,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Pagination,
|
||||
},
|
||||
props: {
|
||||
combinationsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedCombination: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
emptyImageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
areCombinationsNotEmpty() {
|
||||
return this.combinationsList.length > 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$parent.$on(CombinationsEventMap.selectCombination, (id) => {
|
||||
this.selectedCombination = {id};
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Used to select combination in CombinationModal parent component
|
||||
*
|
||||
* @param {object} combination
|
||||
*/
|
||||
selectCombination(combination) {
|
||||
this.$emit(CombinationsEventMap.selectCombination, combination);
|
||||
},
|
||||
/**
|
||||
* This events comes from the pagination component as
|
||||
*/
|
||||
preventClose(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
/**
|
||||
* This events comes from the pagination component as
|
||||
* he's the one managing cutting datas into chunks
|
||||
*
|
||||
* @param {array} datas
|
||||
*/
|
||||
constructDatas(datas) {
|
||||
this.paginatedDatas = datas.paginatedDatas;
|
||||
this.currentPage = datas.currentPage;
|
||||
},
|
||||
/**
|
||||
* Used to avoid having too much logic in the markup
|
||||
*/
|
||||
isSelected(idCombination) {
|
||||
return this.selectedCombination === idCombination
|
||||
|| this.combinationsList.length === 1
|
||||
? 'selected'
|
||||
: null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import "~@scss/config/_settings.scss";
|
||||
|
||||
.history {
|
||||
&-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-block {
|
||||
padding: 0;
|
||||
height: calc(100% - 7rem);
|
||||
}
|
||||
|
||||
&-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100% - 4rem);
|
||||
|
||||
&-tip {
|
||||
color: #8a8a8a;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
max-width: 280px;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
list-style-type: none;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: 0.25s ease-out;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
i {
|
||||
color: $primary;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 1rem;
|
||||
font-size: 1.25rem;
|
||||
transition: 0.25s ease-out;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0fcfd;
|
||||
color: $primary;
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import ReplaceFormatter from '@vue/plugins/vue-i18n/replace-formatter';
|
||||
import CombinationModal from '@pages/product/components/combination-modal/CombinationModal.vue';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
/**
|
||||
* @param {string} combinationModalSelector
|
||||
* @param {int} productId
|
||||
* @param {Object} eventEmitter
|
||||
*
|
||||
* @returns {Vue|CombinedVueInstance<Vue, {eventEmitter, productId}, object, object, Record<never, any>>|null}
|
||||
*/
|
||||
export default function initCombinationModal(
|
||||
combinationModalSelector,
|
||||
productId,
|
||||
eventEmitter,
|
||||
) {
|
||||
const container = document.querySelector(combinationModalSelector);
|
||||
const {emptyImage} = container.dataset;
|
||||
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const translations = JSON.parse(container.dataset.translations);
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
formatter: new ReplaceFormatter(),
|
||||
messages: {en: translations},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: combinationModalSelector,
|
||||
template:
|
||||
'<combination-modal :productId=productId :emptyImageUrl="emptyImage" :eventEmitter=eventEmitter />',
|
||||
components: {CombinationModal},
|
||||
i18n,
|
||||
data: {
|
||||
productId,
|
||||
eventEmitter,
|
||||
emptyImage,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div id="product-images-container">
|
||||
<div
|
||||
id="product-images-dropzone"
|
||||
:class="['dropzone', 'dropzone-container', { full: files.length <= 0 }]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'dz-preview',
|
||||
'openfilemanager',
|
||||
{ 'd-none': loading || files.length <= 0 },
|
||||
]"
|
||||
>
|
||||
<div>
|
||||
<span><i class="material-icons">add_a_photo</i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'dz-default',
|
||||
'dz-message',
|
||||
'openfilemanager',
|
||||
'dz-clickable',
|
||||
{ 'd-none': loading || files.length > 0 },
|
||||
]"
|
||||
>
|
||||
<i class="material-icons">add_a_photo</i><br>
|
||||
{{ $t('window.dropImages') }}<br>
|
||||
<a>{{ $t('window.selectFiles') }}</a><br>
|
||||
<small>
|
||||
{{ $t('window.recommendedSize') }}<br>
|
||||
{{ $t('window.recommendedFormats') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="dropzone-loading"
|
||||
v-if="loading"
|
||||
>
|
||||
<div class="spinner" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dropzone-window
|
||||
class="dropzone-window"
|
||||
v-if="selectedFiles.length > 0"
|
||||
:selected-files="selectedFiles"
|
||||
:dropzone="dropzone"
|
||||
@unselectAll="unselectAll"
|
||||
@removeSelection="showModal"
|
||||
@selectAll="selectAll"
|
||||
@saveSelectedFile="saveSelectedFile"
|
||||
@replacedFile="manageReplacedFile"
|
||||
@openGallery="toggleGallery"
|
||||
:files="files"
|
||||
:locales="locales"
|
||||
:selected-locale="selectedLocale"
|
||||
:loading="buttonLoading"
|
||||
/>
|
||||
|
||||
<modal
|
||||
v-if="isModalShown"
|
||||
:confirmation="true"
|
||||
:modal-title="
|
||||
$tc('modal.title', this.selectedFiles.length, {
|
||||
'%filesNb%': this.selectedFiles.length,
|
||||
})
|
||||
"
|
||||
:confirm-label="$t('modal.accept')"
|
||||
:cancel-label="$t('modal.close')"
|
||||
@confirm="removeSelection"
|
||||
@close="hideModal"
|
||||
/>
|
||||
|
||||
<div class="dz-template d-none">
|
||||
<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-image">
|
||||
<img data-dz-thumbnail>
|
||||
</div>
|
||||
<div class="dz-progress">
|
||||
<span
|
||||
class="dz-upload"
|
||||
data-dz-uploadprogress
|
||||
/>
|
||||
</div>
|
||||
<div class="dz-success-mark">
|
||||
<span>✔</span>
|
||||
</div>
|
||||
<div class="dz-error-mark">
|
||||
<span>✘</span>
|
||||
</div>
|
||||
<div class="dz-error-message">
|
||||
<span data-dz-errormessage />
|
||||
</div>
|
||||
<div class="dz-hover">
|
||||
<i class="material-icons drag-indicator">drag_indicator</i>
|
||||
<div class="md-checkbox">
|
||||
<label>
|
||||
<input type="checkbox">
|
||||
<i class="md-checkbox-control" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iscover">
|
||||
{{ $t('window.cover') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dropzone-photo-swipe
|
||||
:files="selectedFiles"
|
||||
@closeGallery="toggleGallery"
|
||||
v-if="selectedFiles.length > 0 && galleryOpened"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Router from '@components/router';
|
||||
import {
|
||||
getProductImages,
|
||||
saveImageInformations,
|
||||
saveImagePosition,
|
||||
replaceImage,
|
||||
removeProductImage,
|
||||
} from '@pages/product/services/images';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import Modal from '@vue/components/Modal';
|
||||
import DropzoneWindow from './DropzoneWindow';
|
||||
import DropzonePhotoSwipe from './DropzonePhotoSwipe';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
const router = new Router();
|
||||
const DropzoneMap = ProductMap.dropzone;
|
||||
const DropzoneEvents = ProductEventMap.dropzone;
|
||||
|
||||
export default {
|
||||
name: 'Dropzone',
|
||||
data() {
|
||||
return {
|
||||
dropzone: null,
|
||||
configuration: {
|
||||
url: router.generate('admin_products_v2_add_image'),
|
||||
clickable: DropzoneMap.configuration.fileManager,
|
||||
previewTemplate: null,
|
||||
thumbnailWidth: 130,
|
||||
thumbnailHeight: 130,
|
||||
thumbnailMethod: 'crop',
|
||||
},
|
||||
files: [],
|
||||
selectedFiles: [],
|
||||
translations: [],
|
||||
loading: true,
|
||||
selectedLocale: null,
|
||||
buttonLoading: false,
|
||||
isModalShown: false,
|
||||
galleryOpened: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
productId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
locales: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
formName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
DropzoneWindow,
|
||||
Modal,
|
||||
DropzonePhotoSwipe,
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
this.watchLocaleChanges();
|
||||
this.initProductImages();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Watch locale changes to update the selected one
|
||||
*/
|
||||
watchLocaleChanges() {
|
||||
this.selectedLocale = this.locales[0];
|
||||
|
||||
window.prestashop.instance.eventEmitter.on(
|
||||
DropzoneEvents.languageSelected,
|
||||
(event) => {
|
||||
const {selectedLocale} = event;
|
||||
|
||||
this.locales.forEach((locale) => {
|
||||
if (locale.iso_code === selectedLocale) {
|
||||
this.selectedLocale = locale;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
/**
|
||||
* This methods is used to initialize product images we already have uploaded
|
||||
*/
|
||||
async initProductImages() {
|
||||
try {
|
||||
const images = await getProductImages(this.productId);
|
||||
|
||||
this.loading = false;
|
||||
this.initDropZone();
|
||||
|
||||
images.forEach((image) => {
|
||||
this.dropzone.displayExistingFile(image, image.image_url);
|
||||
});
|
||||
} catch (error) {
|
||||
window.$.growl.error({message: error});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method to initialize the dropzone, using the configuration's state and adding files
|
||||
* we already have in database.
|
||||
*/
|
||||
initDropZone() {
|
||||
this.configuration.previewTemplate = document.querySelector(
|
||||
DropzoneMap.dzTemplate,
|
||||
).innerHTML;
|
||||
this.configuration.paramName = `${this.formName}[file]`;
|
||||
this.configuration.method = 'POST';
|
||||
this.configuration.params = {};
|
||||
this.configuration.params[
|
||||
`${this.formName}[product_id]`
|
||||
] = this.productId;
|
||||
this.configuration.params[`${this.formName}[_token]`] = this.token;
|
||||
|
||||
this.sortableContainer = $('#product-images-dropzone');
|
||||
|
||||
this.dropzone = new window.Dropzone(
|
||||
DropzoneMap.dropzoneContainer,
|
||||
this.configuration,
|
||||
);
|
||||
|
||||
this.sortableContainer.sortable({
|
||||
items: DropzoneMap.sortableItems,
|
||||
opacity: 0.9,
|
||||
containment: 'parent',
|
||||
distance: 32,
|
||||
tolerance: 'pointer',
|
||||
cursorAt: {
|
||||
left: 64,
|
||||
top: 64,
|
||||
},
|
||||
cancel: '.disabled',
|
||||
stop: (event, ui) => {
|
||||
// Get new position (-1 because the open file manager is always first)
|
||||
const movedPosition = ui.item.index() - 1;
|
||||
this.updateImagePosition(ui.item.data('id'), movedPosition);
|
||||
},
|
||||
start: (event, ui) => {
|
||||
this.sortableContainer.find(DropzoneMap.dzPreview).css('zIndex', 1);
|
||||
ui.item.css('zIndex', 10);
|
||||
},
|
||||
});
|
||||
|
||||
this.dropzone.on(DropzoneEvents.addedFile, (file) => {
|
||||
file.previewElement.dataset.id = file.image_id;
|
||||
|
||||
if (file.is_cover) {
|
||||
file.previewElement.classList.add('is-cover');
|
||||
}
|
||||
|
||||
file.previewElement.addEventListener('click', () => {
|
||||
const input = file.previewElement.querySelector(DropzoneMap.checkbox);
|
||||
input.checked = !input.checked;
|
||||
|
||||
if (input.checked) {
|
||||
if (!this.selectedFiles.includes(file)) {
|
||||
this.selectedFiles.push(file);
|
||||
file.previewElement.classList.toggle('selected');
|
||||
}
|
||||
} else {
|
||||
this.selectedFiles = this.selectedFiles.filter((e) => e !== file);
|
||||
file.previewElement.classList.toggle('selected');
|
||||
}
|
||||
});
|
||||
this.files.push(file);
|
||||
});
|
||||
|
||||
this.dropzone.on(DropzoneEvents.error, (fileWithError, message) => {
|
||||
$.growl.error({message: message.error});
|
||||
this.dropzone.removeFile(fileWithError);
|
||||
});
|
||||
|
||||
this.dropzone.on(DropzoneEvents.success, (file, response) => {
|
||||
// Append the data required for a product image
|
||||
file.image_id = response.image_id;
|
||||
file.is_cover = response.is_cover;
|
||||
file.legends = response.legends;
|
||||
// Update dataset so that it can be selected later
|
||||
file.previewElement.dataset.id = file.image_id;
|
||||
|
||||
if (file.is_cover) {
|
||||
file.previewElement.classList.add('is-cover');
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Method to select every files by checking checkboxes and add files to the files state
|
||||
*/
|
||||
selectAll() {
|
||||
this.selectedFiles = this.files;
|
||||
|
||||
this.editCheckboxes(true);
|
||||
},
|
||||
/**
|
||||
* Method to unselect every files by unchecking checkboxes and empty files state
|
||||
*/
|
||||
unselectAll() {
|
||||
this.editCheckboxes(false);
|
||||
|
||||
this.selectedFiles = [];
|
||||
|
||||
this.removeTooltips();
|
||||
},
|
||||
/**
|
||||
* Method to remove every selected files from the dropzone
|
||||
*/
|
||||
async removeSelection() {
|
||||
let errorMessage = false;
|
||||
let isCoverImageRemoved = false;
|
||||
const nbFiles = this.selectedFiles.length;
|
||||
|
||||
await Promise.all(
|
||||
this.selectedFiles.map(async (file) => {
|
||||
try {
|
||||
await removeProductImage(file.image_id);
|
||||
this.dropzone.removeFile(file);
|
||||
|
||||
this.files = this.files.filter((e) => file !== e);
|
||||
this.selectedFiles = this.selectedFiles.filter((e) => file !== e);
|
||||
|
||||
if (file.is_cover) {
|
||||
isCoverImageRemoved = true;
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = error.responseJSON
|
||||
? error.responseJSON.error
|
||||
: error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.removeTooltips();
|
||||
|
||||
if (errorMessage) {
|
||||
$.growl.error({message: errorMessage});
|
||||
} else {
|
||||
$.growl({
|
||||
message: this.$t('delete.success', {
|
||||
'%filesNb%': nbFiles,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (isCoverImageRemoved) {
|
||||
this.resetDropzone();
|
||||
}
|
||||
|
||||
this.hideModal();
|
||||
},
|
||||
/**
|
||||
* Method to manage checkboxes of files mainly used on selectAll and unselectAll
|
||||
*/
|
||||
editCheckboxes(checked) {
|
||||
this.selectedFiles.forEach((file) => {
|
||||
const input = file.previewElement.querySelector(DropzoneMap.checkbox);
|
||||
input.checked = typeof checked !== 'undefined' ? checked : !input.checked;
|
||||
|
||||
file.previewElement.classList.toggle('selected', checked);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* We sometime need to remove tooltip because Vue kick the markup of the component
|
||||
*/
|
||||
removeTooltips() {
|
||||
$(DropzoneMap.shownTooltips).each((i, element) => {
|
||||
$(element).remove();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Save selected file
|
||||
*/
|
||||
async saveSelectedFile(captionValue, isCover) {
|
||||
if (!this.selectedFiles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buttonLoading = true;
|
||||
|
||||
const selectedFile = this.selectedFiles[0];
|
||||
|
||||
selectedFile.is_cover = isCover;
|
||||
|
||||
selectedFile.legends = captionValue;
|
||||
|
||||
try {
|
||||
const savedImage = await saveImageInformations(
|
||||
selectedFile,
|
||||
this.token,
|
||||
this.formName,
|
||||
);
|
||||
|
||||
const savedImageElement = document.querySelector(
|
||||
DropzoneMap.savedImageContainer(savedImage.image_id),
|
||||
);
|
||||
|
||||
/**
|
||||
* If the image was saved as cover, we need to replace the DOM in order to display
|
||||
* the correct one.
|
||||
*/
|
||||
if (savedImage.is_cover) {
|
||||
if (!savedImageElement.classList.contains('is-cover')) {
|
||||
const coverElement = document.querySelector(
|
||||
DropzoneMap.coveredPreview,
|
||||
);
|
||||
|
||||
if (coverElement) {
|
||||
coverElement.classList.remove('is-cover');
|
||||
}
|
||||
|
||||
savedImageElement.classList.add('is-cover');
|
||||
|
||||
this.files = this.files.map((file) => {
|
||||
if (file.image_id !== savedImage.image_id && file.is_cover) {
|
||||
file.is_cover = false;
|
||||
}
|
||||
|
||||
return file;
|
||||
});
|
||||
}
|
||||
}
|
||||
$.growl({message: this.$t('window.settingsUpdated')});
|
||||
this.buttonLoading = false;
|
||||
} catch (error) {
|
||||
$.growl.error({message: error.error});
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Used to save and manage some datas from a replaced file
|
||||
*/
|
||||
async manageReplacedFile(event) {
|
||||
const selectedFile = this.selectedFiles[0];
|
||||
this.buttonLoading = true;
|
||||
|
||||
try {
|
||||
const newImage = await replaceImage(
|
||||
selectedFile,
|
||||
event.target.files[0],
|
||||
this.formName,
|
||||
this.token,
|
||||
);
|
||||
const imageElement = document.querySelector(
|
||||
DropzoneMap.savedImage(newImage.image_id),
|
||||
);
|
||||
imageElement.src = newImage.image_url;
|
||||
|
||||
$.growl({message: this.$t('window.imageReplaced')});
|
||||
this.buttonLoading = false;
|
||||
} catch (error) {
|
||||
$.growl.error({message: error.responseJSON.error});
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
},
|
||||
async updateImagePosition(productImageId, newPosition) {
|
||||
try {
|
||||
await saveImagePosition(
|
||||
productImageId,
|
||||
newPosition,
|
||||
this.formName,
|
||||
this.token,
|
||||
);
|
||||
} catch (error) {
|
||||
this.sortableContainer.sortable('cancel');
|
||||
$.growl.error({message: error.responseJSON.error});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Mainly used when we wants to reset the whole list
|
||||
* to reset cover image for example on remove
|
||||
*/
|
||||
resetDropzone() {
|
||||
this.loading = true;
|
||||
this.files.forEach((file) => {
|
||||
this.dropzone.removeFile(file);
|
||||
});
|
||||
this.dropzone.destroy();
|
||||
this.dropzone = null;
|
||||
this.initProductImages();
|
||||
},
|
||||
showModal() {
|
||||
this.isModalShown = true;
|
||||
},
|
||||
hideModal() {
|
||||
this.isModalShown = false;
|
||||
},
|
||||
/**
|
||||
* Method used to open the photoswipe gallery
|
||||
*/
|
||||
toggleGallery() {
|
||||
this.galleryOpened = !this.galleryOpened;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import '~@scss/config/_settings.scss';
|
||||
@import '~@scss/config/_bootstrap.scss';
|
||||
|
||||
.product-page #product-images-dropzone {
|
||||
&.full {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropzone-loading {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
&.dropzone-container {
|
||||
.dz-preview {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.iscover {
|
||||
display: none;
|
||||
left: -2px;
|
||||
bottom: -3px;
|
||||
width: calc(100% + 4px);
|
||||
padding: 9px;
|
||||
}
|
||||
&.is-cover {
|
||||
.iscover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.openfilemanager) {
|
||||
border: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border: 3px solid $primary;
|
||||
}
|
||||
|
||||
.dz-image {
|
||||
border: 1px solid $gray-300;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
margin: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.openfilemanager {
|
||||
border-style: dashed;
|
||||
min-width: 130px;
|
||||
|
||||
&:hover {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
> div {
|
||||
border: none;
|
||||
|
||||
i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dz-hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
|
||||
.drag-indicator,
|
||||
.md-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.md-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dz-hover {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
width: calc(100% + 6px);
|
||||
height: calc(100% + 6px);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition: 0.25s ease-out;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
.drag-indicator {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
transition: 0.25s ease-out;
|
||||
}
|
||||
|
||||
.md-checkbox {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: 0.25s ease-out;
|
||||
|
||||
.md-checkbox-control::before {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input:checked + .md-checkbox-control::before {
|
||||
background: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-page #product-images-container {
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#product-images-dropzone.dropzone {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-radius: 4px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
|
||||
.dz-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
min-height: 100px;
|
||||
margin: 0.5rem;
|
||||
|
||||
&.openfilemanager {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.dz-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div
|
||||
class="pswp"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="pswp__bg" />
|
||||
|
||||
<div class="pswp__scroll-wrap">
|
||||
<div class="pswp__container">
|
||||
<div class="pswp__item" />
|
||||
<div class="pswp__item" />
|
||||
<div class="pswp__item" />
|
||||
</div>
|
||||
|
||||
<div class="pswp__ui pswp__ui--hidden">
|
||||
<div class="pswp__top-bar">
|
||||
<div class="pswp__counter" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--close"
|
||||
:title="$t('window.closePhotoSwipe')"
|
||||
>
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--share"
|
||||
:title="$t('window.download')"
|
||||
>
|
||||
<i class="material-icons">file_download</i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--fs"
|
||||
:title="$t('window.toggleFullscreen')"
|
||||
>
|
||||
<i class="material-icons">fullscreen</i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--zoom"
|
||||
:title="$t('window.zoomPhotoSwipe')"
|
||||
>
|
||||
<i class="material-icons">zoom_in</i>
|
||||
</button>
|
||||
|
||||
<div class="pswp__preloader">
|
||||
<div class="pswp__preloader__icn">
|
||||
<div class="pswp__preloader__cut">
|
||||
<div class="pswp__preloader__donut" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"
|
||||
>
|
||||
<div class="pswp__share-tooltip" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--arrow--left"
|
||||
:title="$t('window.previousPhotoSwipe')"
|
||||
>
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="pswp__button pswp__button--arrow--right"
|
||||
:title="$t('window.nextPhotoSwipe')"
|
||||
>
|
||||
<i class="material-icons">arrow_forward</i>
|
||||
</button>
|
||||
|
||||
<div class="pswp__caption">
|
||||
<div class="pswp__caption__center" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
import PhotoSwipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
const PhotoSwipeMap = ProductMap.dropzone.photoswipe;
|
||||
const PhotoSwipeEventMap = ProductEventMap.dropzone.photoswipe;
|
||||
|
||||
export default {
|
||||
name: 'DropzonePhotoSwipe',
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const pswpElement = document.querySelector(PhotoSwipeMap.element);
|
||||
|
||||
if (pswpElement) {
|
||||
const options = {
|
||||
index: 0,
|
||||
shareButtons: [
|
||||
{
|
||||
id: 'download',
|
||||
label: this.$t('window.downloadImage'),
|
||||
url: '{{raw_image_url}}',
|
||||
download: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// This is needed to make our files compatible for photoswipe
|
||||
const items = this.files.map((file) => {
|
||||
file.src = file.dataURL;
|
||||
file.h = file.height;
|
||||
file.w = file.width;
|
||||
|
||||
return file;
|
||||
});
|
||||
|
||||
const gallery = new PhotoSwipe(
|
||||
pswpElement,
|
||||
PhotoSwipeUIDefault,
|
||||
items,
|
||||
options,
|
||||
);
|
||||
|
||||
gallery.init();
|
||||
|
||||
// We must tell to the rich component that the gallery have been closed
|
||||
gallery.listen(PhotoSwipeEventMap.destroy, () => {
|
||||
this.$emit(PhotoSwipeEventMap.closeGallery);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import "~@scss/config/_settings.scss";
|
||||
|
||||
.product-page #product-images-container {
|
||||
.pswp__button {
|
||||
background: none;
|
||||
color: white;
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
i {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,393 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div class="dropzone-window">
|
||||
<div class="dropzone-window-header row">
|
||||
<div class="dropzone-window-header-left">
|
||||
<p
|
||||
class="dropzone-window-number"
|
||||
v-html="
|
||||
$t('window.selectedFiles', { '%filesNb%': selectedFiles.length })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropzone-window-header-right">
|
||||
<i
|
||||
class="material-icons"
|
||||
data-toggle="pstooltip"
|
||||
:data-original-title="$t('window.zoom')"
|
||||
@click="$emit('openGallery')"
|
||||
>search</i>
|
||||
<i
|
||||
class="material-icons"
|
||||
data-toggle="pstooltip"
|
||||
:data-original-title="$t('window.replaceSelection')"
|
||||
@click="openFileManager"
|
||||
v-if="selectedFile"
|
||||
>find_replace</i>
|
||||
<i
|
||||
class="material-icons"
|
||||
data-toggle="pstooltip"
|
||||
:data-original-title="$t('window.delete')"
|
||||
@click.stop="$emit('removeSelection')"
|
||||
>delete</i>
|
||||
<i
|
||||
class="material-icons"
|
||||
data-toggle="pstooltip"
|
||||
:data-original-title="$t('window.close')"
|
||||
@click="$emit('unselectAll')"
|
||||
>close</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="dropzone-window-select"
|
||||
@click="$emit('selectAll')"
|
||||
v-if="files.length > 0 && selectedFiles.length !== files.length"
|
||||
>
|
||||
{{ $t('window.selectAll') }}
|
||||
</p>
|
||||
<p
|
||||
class="dropzone-window-unselect"
|
||||
v-if="selectedFiles.length === files.length"
|
||||
@click="$emit('unselectAll')"
|
||||
>
|
||||
{{ $t('window.unselectAll') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="md-checkbox dropzone-window-checkbox"
|
||||
v-if="selectedFile !== null"
|
||||
:data-toggle="showCoverTooltip"
|
||||
:data-original-title="$t('window.cantDisableCover')"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="isCover"
|
||||
:checked="isCover"
|
||||
@change.prevent.stop="coverChanged"
|
||||
>
|
||||
<i class="md-checkbox-control" />
|
||||
{{ $t('window.useAsCover') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
class="dropzone-window-filemanager"
|
||||
@change.prevent.stop="watchFiles"
|
||||
>
|
||||
|
||||
<div
|
||||
class="dropzone-window-label"
|
||||
v-if="selectedFile !== null"
|
||||
>
|
||||
<label
|
||||
for="caption-textarea"
|
||||
class="control-label"
|
||||
>{{
|
||||
$t('window.caption')
|
||||
}}</label>
|
||||
<div
|
||||
class="dropdown"
|
||||
v-if="locales.length > 1"
|
||||
>
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-sm dropdown-toggle js-locale-btn"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
id="product_dropzone_lang"
|
||||
>
|
||||
{{ selectedLocale.iso_code }}
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-menu locale-dropdown-menu"
|
||||
aria-labelledby="form_invoice_prefix"
|
||||
>
|
||||
<span
|
||||
v-for="locale in locales"
|
||||
:key="locale.name"
|
||||
class="dropdown-item js-locale-item"
|
||||
:data-locale="locale.iso_code"
|
||||
>
|
||||
{{ locale.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="caption-textarea"
|
||||
name="caption-textarea"
|
||||
class="form-control"
|
||||
v-if="selectedFile !== null"
|
||||
v-model="captionValue[selectedLocale.id_lang]"
|
||||
@change.prevent.stop="prevent"
|
||||
@keyup.prevent.stop="prevent"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="dropzone-window-button-container"
|
||||
v-if="selectedFile"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary save-image-settings"
|
||||
@click="$emit('saveSelectedFile', captionValue, coverData)"
|
||||
>
|
||||
<span v-if="!loading">
|
||||
{{ $t('window.saveImage') }}
|
||||
</span>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
v-if="loading"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
const DropzoneMap = ProductMap.dropzone;
|
||||
|
||||
export default {
|
||||
name: 'DropzoneWindow',
|
||||
props: {
|
||||
selectedFiles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
locales: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedLocale: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
captionValue: {},
|
||||
coverData: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* We need to watch selected files to manage multilang
|
||||
* of only one file or multiple files then the value is sent
|
||||
* on save.
|
||||
*/
|
||||
selectedFiles(value) {
|
||||
if (value.length > 1) {
|
||||
this.captionValue = {};
|
||||
this.locales.forEach((locale) => {
|
||||
this.captionValue[locale] = '';
|
||||
});
|
||||
} else {
|
||||
this.captionValue = this.selectedFile.legends;
|
||||
this.coverData = this.selectedFile.is_cover;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedFile() {
|
||||
return this.selectedFiles.length === 1 ? this.selectedFiles[0] : null;
|
||||
},
|
||||
isCover() {
|
||||
return !!(this.selectedFile && this.selectedFile.is_cover);
|
||||
},
|
||||
showCoverTooltip() {
|
||||
if (this.isCover) {
|
||||
return 'pstooltip';
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.prestaShopUiKit.initToolTips();
|
||||
// We set the intial value of the first item in order to use the computed
|
||||
this.captionValue = this.selectedFile.legends;
|
||||
this.coverData = this.selectedFile.is_cover;
|
||||
},
|
||||
updated() {
|
||||
window.prestaShopUiKit.initToolTips();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Watch file change and send an event to the smart component
|
||||
*/
|
||||
watchFiles(event) {
|
||||
this.$emit('replacedFile', event);
|
||||
},
|
||||
/**
|
||||
* Used to open the native file manager
|
||||
*/
|
||||
openFileManager() {
|
||||
const fileInput = document.querySelector(DropzoneMap.windowFileManager);
|
||||
fileInput.click();
|
||||
},
|
||||
/**
|
||||
* Cache cover data
|
||||
*/
|
||||
coverChanged(event) {
|
||||
this.coverData = event.target.value;
|
||||
},
|
||||
prevent(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import '~@scss/config/_settings.scss';
|
||||
@import '~@scss/config/_bootstrap.scss';
|
||||
|
||||
.product-page {
|
||||
.dropzone-window {
|
||||
width: 45%;
|
||||
background-color: darken(#ffffff, 2%);
|
||||
align-self: stretch;
|
||||
padding: 1rem;
|
||||
min-width: 20rem;
|
||||
|
||||
&-filemanager {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
> button {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-button {
|
||||
&-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-select,
|
||||
&-unselect {
|
||||
font-weight: 600;
|
||||
font-size: 0.925rem;
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
&-number {
|
||||
font-size: 1rem;
|
||||
|
||||
span {
|
||||
color: $primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
cursor: pointer;
|
||||
color: $gray-500;
|
||||
transition: 0.25s ease-out;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0.25rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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)
|
||||
*/
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import ReplaceFormatter from '@vue/plugins/vue-i18n/replace-formatter';
|
||||
import Dropzone from './Dropzone.vue';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
export default function initDropzone(imagesContainerSelector) {
|
||||
const container = document.querySelector(imagesContainerSelector);
|
||||
|
||||
const translations = JSON.parse(container.dataset.translations);
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
formatter: new ReplaceFormatter(),
|
||||
messages: {en: translations},
|
||||
});
|
||||
|
||||
const productId = Number(container.dataset.productId);
|
||||
const locales = JSON.parse(container.dataset.locales);
|
||||
|
||||
return new Vue({
|
||||
el: imagesContainerSelector,
|
||||
template: '<dropzone :productId=productId :locales=locales :token=token :formName=formName />',
|
||||
components: {Dropzone},
|
||||
i18n,
|
||||
data: {
|
||||
locales,
|
||||
productId,
|
||||
token: container.dataset.token,
|
||||
formName: container.dataset.formName,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
|
||||
<template>
|
||||
<div class="combinations-filters-dropdown">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
:class="[
|
||||
'btn',
|
||||
'dropdown-toggle',
|
||||
selectedFilters.length > 0 ? 'btn-primary' : 'btn-outline-secondary',
|
||||
'btn',
|
||||
]"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
id="form_invoice_prefix"
|
||||
>
|
||||
{{ label }} {{ nbFiles }}
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
aria-labelledby="form_invoice_prefix"
|
||||
@click="preventClose($event)"
|
||||
>
|
||||
<div
|
||||
class="md-checkbox"
|
||||
v-for="filter in children"
|
||||
:key="filter.id"
|
||||
>
|
||||
<label class="dropdown-item">
|
||||
<div class="md-checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChecked(filter)"
|
||||
@change="toggleFilter(filter)"
|
||||
>
|
||||
<i class="md-checkbox-control" />
|
||||
{{ filter.name }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FilterDropdown',
|
||||
data() {
|
||||
return {
|
||||
selectedFilters: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
parentId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$parent.$on('clearAll', this.clear);
|
||||
},
|
||||
computed: {
|
||||
nbFiles() {
|
||||
return this.selectedFilters.length > 0
|
||||
? `(${this.selectedFilters.length})`
|
||||
: null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isChecked(filter) {
|
||||
return this.selectedFilters.includes(filter);
|
||||
},
|
||||
toggleFilter(filter) {
|
||||
if (this.selectedFilters.includes(filter)) {
|
||||
this.$emit('removeFilter', filter, this.parentId);
|
||||
this.selectedFilters = this.selectedFilters.filter(
|
||||
(item) => item.id !== filter.id,
|
||||
);
|
||||
} else {
|
||||
this.$emit('addFilter', filter, this.parentId);
|
||||
this.selectedFilters.push(filter);
|
||||
}
|
||||
},
|
||||
preventClose(event) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
clear() {
|
||||
this.selectedFilters = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import "~@scss/config/_settings.scss";
|
||||
@import "~@scss/config/_bootstrap.scss";
|
||||
|
||||
.combinations-filters-dropdown {
|
||||
margin: 0 0.35rem;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.438rem 0.938rem;
|
||||
padding-right: 1rem;
|
||||
line-height: normal;
|
||||
color: inherit;
|
||||
border-bottom: 0;
|
||||
|
||||
.md-checkbox-container {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div class="combinations-filters">
|
||||
<label
|
||||
class="control-label"
|
||||
v-if="filters.length"
|
||||
>{{ $t('filters.label') }}</label>
|
||||
|
||||
<div
|
||||
class="combinations-filters-line"
|
||||
v-if="filters.length"
|
||||
>
|
||||
<filter-dropdown
|
||||
:key="filter.id"
|
||||
v-for="filter in filters"
|
||||
:children="filter.attributes"
|
||||
:parent-id="filter.id"
|
||||
:label="filter.name"
|
||||
@addFilter="addFilter"
|
||||
@removeFilter="removeFilter"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
v-if="selectedFiltersNumber > 0"
|
||||
class="btn btn-outline-secondary combinations-filters-clear"
|
||||
@click="clearAll"
|
||||
>
|
||||
<i class="material-icons">close</i>
|
||||
{{ $tc('filters.clear', selectedFiltersNumber, { '%filtersNb%': selectedFiltersNumber }) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import FilterDropdown from '@pages/product/components/filters/FilterDropdown';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
const CombinationEvents = ProductEventMap.combinations;
|
||||
|
||||
export default {
|
||||
name: 'Filters',
|
||||
data() {
|
||||
return {
|
||||
selectedFilters: {},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
filters: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
eventEmitter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FilterDropdown,
|
||||
},
|
||||
computed: {
|
||||
selectedFiltersNumber() {
|
||||
if (!this.selectedFilters) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.values(this.selectedFilters).reduce((total, attributes) => total + attributes.length, 0);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.eventEmitter.on(CombinationEvents.clearFilters, () => this.clearAll());
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* This methods is used to initialize product filters
|
||||
*/
|
||||
addFilter(filter, parentId) {
|
||||
// If absent set new field with set method so that it's reactive
|
||||
if (!this.selectedFilters[parentId]) {
|
||||
this.$set(this.selectedFilters, parentId, []);
|
||||
}
|
||||
|
||||
this.selectedFilters[parentId].push(filter);
|
||||
this.updateFilters();
|
||||
},
|
||||
removeFilter(filter, parentId) {
|
||||
if (!this.selectedFilters[parentId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedFilters[parentId] = this.selectedFilters[parentId].filter(
|
||||
(e) => filter.id !== e.id,
|
||||
);
|
||||
this.updateFilters();
|
||||
},
|
||||
clearAll() {
|
||||
this.selectedFilters = [];
|
||||
this.$emit('clearAll');
|
||||
this.eventEmitter.emit(CombinationEvents.updateAttributeGroups, this.selectedFilters);
|
||||
},
|
||||
updateFilters() {
|
||||
this.eventEmitter.emit(CombinationEvents.updateAttributeGroups, this.selectedFilters);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import "~@scss/config/_settings.scss";
|
||||
|
||||
.combinations-filters {
|
||||
.control-label {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-botton: 1rem;
|
||||
}
|
||||
|
||||
&-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -0.35rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import Filters from '@pages/product/components/filters/Filters';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import ReplaceFormatter from '@vue/plugins/vue-i18n/replace-formatter';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
/**
|
||||
* @param {string} combinationsFiltersSelector
|
||||
* @param {EventEmitter} eventEmitter
|
||||
* @param {array} filters
|
||||
* @returns {Vue | CombinedVueInstance<Vue, {eventEmitter, filters}, object, object, Record<never, any>>}
|
||||
*/
|
||||
export default function initCombinationsFilters(combinationsFiltersSelector, eventEmitter, filters) {
|
||||
const container = document.querySelector(combinationsFiltersSelector);
|
||||
|
||||
const translations = JSON.parse(container.dataset.translations);
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
formatter: new ReplaceFormatter(),
|
||||
messages: {en: translations},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: combinationsFiltersSelector,
|
||||
template: '<filters :filters=filters :eventEmitter=eventEmitter />',
|
||||
components: {Filters},
|
||||
i18n,
|
||||
data: {
|
||||
filters,
|
||||
eventEmitter,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div class="generate-modal-content">
|
||||
<div class="tags-input d-flex flex-wrap">
|
||||
<div class="tags-wrapper">
|
||||
<template v-for="selectedGroup in selectedAttributeGroups">
|
||||
<span
|
||||
class="tag"
|
||||
:key="selectedAttribute.id"
|
||||
v-for="selectedAttribute in selectedGroup.attributes"
|
||||
>
|
||||
{{ selectedGroup.name }}: {{ selectedAttribute.name }}
|
||||
<i
|
||||
class="material-icons"
|
||||
@click.prevent.stop="
|
||||
sendRemoveEvent(selectedAttribute, selectedGroup)
|
||||
"
|
||||
>close</i>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('search.placeholder')"
|
||||
class="form-control input attributes-search"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="product-combinations-modal-content">
|
||||
<div
|
||||
id="attributes-list-selector"
|
||||
class="attributes-list-overflow"
|
||||
>
|
||||
<div class="attributes-content">
|
||||
<div
|
||||
class="attribute-group"
|
||||
v-for="attributeGroup of attributeGroups"
|
||||
:key="attributeGroup.id"
|
||||
>
|
||||
<div class="attribute-group-header">
|
||||
<div class="md-checkbox attribute-group-checkbox">
|
||||
<label>
|
||||
<input
|
||||
class="attribute-group-checkbox"
|
||||
type="checkbox"
|
||||
:name="`checkbox_${attributeGroup.id}`"
|
||||
@change.prevent.stop="toggleAll(attributeGroup)"
|
||||
:checked="checkboxList.includes(attributeGroup)"
|
||||
>
|
||||
|
||||
<i class="md-checkbox-control" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="attribute-group-name collapsed"
|
||||
data-toggle="collapse"
|
||||
:href="`#attribute-group-${attributeGroup.id}`"
|
||||
>
|
||||
{{ attributeGroup.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="attribute-group-content attributes collapse"
|
||||
:id="`attribute-group-${attributeGroup.id}`"
|
||||
>
|
||||
<label
|
||||
v-for="attribute of attributeGroup.attributes"
|
||||
:class="[
|
||||
'attribute-item',
|
||||
getSelectedClass(attribute, attributeGroup),
|
||||
]"
|
||||
:for="`attribute_${attribute.id}`"
|
||||
:key="attribute.id"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="`attribute_${attribute.id}`"
|
||||
:id="`attribute_${attribute.id}`"
|
||||
@change="sendChangeEvent(attribute, attributeGroup)"
|
||||
>
|
||||
<div class="attribute-item-content">
|
||||
<span
|
||||
class="attribute-item-color"
|
||||
v-if="attribute.color"
|
||||
:style="`background-color: ${attribute.color}`"
|
||||
/>
|
||||
<span class="attribute-item-name">{{ attribute.name }}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import isSelected from '@pages/product/mixins/is-attribute-selected';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import Bloodhound from 'typeahead.js';
|
||||
import AutoCompleteSearch from '@components/auto-complete-search';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
const CombinationsMap = ProductMap.combinations;
|
||||
|
||||
export default {
|
||||
name: 'AttributesSelector',
|
||||
props: {
|
||||
attributeGroups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedAttributeGroups: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mixins: [isSelected],
|
||||
data() {
|
||||
return {
|
||||
dataSetConfig: {},
|
||||
searchSource: {},
|
||||
scrollbar: null,
|
||||
hasGeneratedCombinations: false,
|
||||
checkboxList: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initDataSetConfig();
|
||||
this.scrollbar = new PerfectScrollbar(CombinationsMap.scrollBar);
|
||||
const $searchInput = $(CombinationsMap.searchInput);
|
||||
new AutoCompleteSearch($searchInput, this.dataSetConfig);
|
||||
},
|
||||
watch: {
|
||||
selectedAttributeGroups(value) {
|
||||
const attributes = Object.keys(value);
|
||||
|
||||
if (attributes.length <= 0) {
|
||||
this.checkboxList = [];
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initDataSetConfig() {
|
||||
const searchItems = this.getSearchableAttributes();
|
||||
this.searchSource = new Bloodhound({
|
||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(
|
||||
'name',
|
||||
'value',
|
||||
'color',
|
||||
'group_name',
|
||||
),
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
local: searchItems,
|
||||
});
|
||||
|
||||
const dataSetConfig = {
|
||||
source: this.searchSource,
|
||||
display: 'name',
|
||||
value: 'name',
|
||||
minLength: 1,
|
||||
onSelect: (attribute, e, $searchInput) => {
|
||||
const attributeGroup = {
|
||||
id: attribute.group_id,
|
||||
name: attribute.group_name,
|
||||
};
|
||||
this.sendAddEvent(attribute, attributeGroup);
|
||||
|
||||
// This resets the search input or else previous search is cached and can be added again
|
||||
$searchInput.typeahead('val', '');
|
||||
},
|
||||
onClose(event, $searchInput) {
|
||||
$searchInput.typeahead('val', '');
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
dataSetConfig.templates = {
|
||||
suggestion: (item) => `<div class="px-2">${item.group_name}: ${item.name}</div>`,
|
||||
};
|
||||
|
||||
this.dataSetConfig = dataSetConfig;
|
||||
},
|
||||
/**
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSearchableAttributes() {
|
||||
const searchableAttributes = [];
|
||||
this.attributeGroups.forEach((attributeGroup) => {
|
||||
attributeGroup.attributes.forEach((attribute) => {
|
||||
if (
|
||||
this.isSelected(
|
||||
attribute,
|
||||
attributeGroup,
|
||||
this.selectedAttributeGroups,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
attribute.group_name = attributeGroup.name;
|
||||
attribute.group_id = attributeGroup.id;
|
||||
searchableAttributes.push(attribute);
|
||||
});
|
||||
});
|
||||
|
||||
return searchableAttributes;
|
||||
},
|
||||
/**
|
||||
* @param {Object} attribute
|
||||
* @param {Object} attributeGroup
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
getSelectedClass(attribute, attributeGroup) {
|
||||
return this.isSelected(
|
||||
attribute,
|
||||
attributeGroup,
|
||||
this.selectedAttributeGroups,
|
||||
)
|
||||
? 'selected'
|
||||
: 'unselected';
|
||||
},
|
||||
sendRemoveEvent(selectedAttribute, selectedAttributeGroup) {
|
||||
this.$emit('removeSelected', {
|
||||
selectedAttribute,
|
||||
selectedAttributeGroup,
|
||||
});
|
||||
this.updateSearchableAttributes();
|
||||
this.updateCheckboxes(selectedAttributeGroup);
|
||||
},
|
||||
sendChangeEvent(selectedAttribute, attributeGroup) {
|
||||
this.$emit('changeSelected', {selectedAttribute, attributeGroup});
|
||||
this.updateSearchableAttributes();
|
||||
this.updateCheckboxes(attributeGroup);
|
||||
},
|
||||
sendAddEvent(selectedAttribute, attributeGroup) {
|
||||
this.$emit('addSelected', {selectedAttribute, attributeGroup});
|
||||
this.updateSearchableAttributes();
|
||||
this.updateCheckboxes(attributeGroup);
|
||||
},
|
||||
/**
|
||||
* Update Bloodhound engine so that it does not include already selected attributes
|
||||
*/
|
||||
updateSearchableAttributes() {
|
||||
const searchableAttributes = this.getSearchableAttributes();
|
||||
this.searchSource.clear();
|
||||
this.searchSource.add(searchableAttributes);
|
||||
},
|
||||
toggleAll(attributeGroup) {
|
||||
if (this.checkboxList.includes(attributeGroup)) {
|
||||
this.checkboxList = this.checkboxList.filter(
|
||||
(e) => e.id !== attributeGroup.id,
|
||||
);
|
||||
} else {
|
||||
this.checkboxList.push(attributeGroup);
|
||||
}
|
||||
|
||||
this.$emit('toggleAll', {
|
||||
attributeGroup,
|
||||
select: this.checkboxList.includes(attributeGroup),
|
||||
});
|
||||
},
|
||||
updateCheckboxes(attributeGroup) {
|
||||
if (
|
||||
this.selectedAttributeGroups[attributeGroup.id]
|
||||
&& !this.checkboxList.includes(attributeGroup)
|
||||
&& this.selectedAttributeGroups[attributeGroup.id].attributes.length
|
||||
=== attributeGroup.attributes.length
|
||||
) {
|
||||
this.checkboxList.push(attributeGroup);
|
||||
} else {
|
||||
this.checkboxList = this.checkboxList.filter(
|
||||
(group) => group.id !== attributeGroup.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" type="text/scss">
|
||||
@import '~@scss/config/_settings.scss';
|
||||
|
||||
#product-combinations-generate {
|
||||
.modal {
|
||||
.tags-input {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.tag {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
#attributes-list-selector {
|
||||
max-height: 50vh;
|
||||
|
||||
.attribute-group {
|
||||
position: relative;
|
||||
margin-bottom: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: 4px;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
background-color: $gray-250;
|
||||
}
|
||||
|
||||
&-content {
|
||||
border-top: 1px solid $gray-300;
|
||||
}
|
||||
|
||||
&-checkbox {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
&-name {
|
||||
width: 100%;
|
||||
padding: 0.4375rem 0.4375rem 0.4375rem 2.5rem;
|
||||
font-weight: 600;
|
||||
color: #363a41;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-item {
|
||||
margin: 0.25rem;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $gray-disabled;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-color {
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attributes {
|
||||
height: auto;
|
||||
padding: 0.4375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.product-combinations-modal-content {
|
||||
position: relative;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,322 @@
|
||||
<!--**
|
||||
* 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)
|
||||
*-->
|
||||
<template>
|
||||
<div id="product-combinations-generate">
|
||||
<modal
|
||||
v-if="isModalShown"
|
||||
:modal-title="$t('modal.title')"
|
||||
:confirmation="true"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<attributes-selector
|
||||
:attribute-groups="attributeGroups"
|
||||
:selected-attribute-groups="selectedAttributeGroups"
|
||||
@changeSelected="changeSelected"
|
||||
@removeSelected="removeSelected"
|
||||
@addSelected="addSelected"
|
||||
@toggleAll="toggleAll"
|
||||
v-if="attributeGroups"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer-confirmation>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
@click.prevent.stop="closeModal"
|
||||
:aria-label="$t('modal.close')"
|
||||
>
|
||||
{{ $t('modal.close') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click.prevent.stop="generateCombinations"
|
||||
:disabled="!generatedCombinationsNb || loading"
|
||||
>
|
||||
<span v-if="!loading">
|
||||
{{
|
||||
$tc('generator.action', generatedCombinationsNb, {
|
||||
'%combinationsNb%': generatedCombinationsNb,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
v-if="loading"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CombinationsService from '@pages/product/services/combinations-service';
|
||||
import AttributesSelector from '@pages/product/components/generator/AttributesSelector';
|
||||
import isSelected from '@pages/product/mixins/is-attribute-selected';
|
||||
import {getAllAttributeGroups} from '@pages/product/services/attribute-groups';
|
||||
import Modal from '@vue/components/Modal';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
const CombinationEvents = ProductEventMap.combinations;
|
||||
|
||||
export default {
|
||||
name: 'CombinationGenerator',
|
||||
data() {
|
||||
return {
|
||||
attributeGroups: [],
|
||||
selectedAttributeGroups: {},
|
||||
combinationsService: new CombinationsService(this.productId),
|
||||
isModalShown: false,
|
||||
preLoading: true,
|
||||
loading: false,
|
||||
scrollbar: null,
|
||||
hasGeneratedCombinations: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
productId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
eventEmitter: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mixins: [isSelected],
|
||||
components: {
|
||||
Modal,
|
||||
AttributesSelector,
|
||||
},
|
||||
computed: {
|
||||
generatedCombinationsNb() {
|
||||
const groupIds = Object.keys(this.selectedAttributeGroups);
|
||||
let combinationsNumber = 0;
|
||||
|
||||
groupIds.forEach((attributeGroupId) => {
|
||||
const {attributes} = this.selectedAttributeGroups[attributeGroupId];
|
||||
|
||||
if (!attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only start counting when at least one attribute is selected
|
||||
if (combinationsNumber === 0) {
|
||||
combinationsNumber = 1;
|
||||
}
|
||||
combinationsNumber *= this.selectedAttributeGroups[attributeGroupId]
|
||||
.attributes.length;
|
||||
});
|
||||
|
||||
return combinationsNumber;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initAttributeGroups();
|
||||
this.eventEmitter.on(CombinationEvents.openCombinationsGenerator, () => this.showModal());
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* This methods is used to initialize combinations definitions
|
||||
*/
|
||||
async initAttributeGroups() {
|
||||
try {
|
||||
this.attributeGroups = await getAllAttributeGroups();
|
||||
window.prestaShopUiKit.init();
|
||||
this.preLoading = false;
|
||||
this.eventEmitter.emit(CombinationEvents.combinationGeneratorReady);
|
||||
} catch (error) {
|
||||
window.$.growl.error({message: error});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Show the modal, and execute PerfectScrollBar and Typehead
|
||||
*/
|
||||
showModal() {
|
||||
if (this.preLoading) {
|
||||
return;
|
||||
}
|
||||
document.querySelector('body').classList.add('overflow-hidden');
|
||||
this.hasGeneratedCombinations = false;
|
||||
this.selectedAttributeGroups = {};
|
||||
this.isModalShown = true;
|
||||
},
|
||||
/**
|
||||
* Handle modal closing
|
||||
*/
|
||||
closeModal() {
|
||||
this.isModalShown = false;
|
||||
document.querySelector('body').classList.remove('overflow-hidden');
|
||||
if (this.hasGeneratedCombinations) {
|
||||
this.eventEmitter.emit(CombinationEvents.refreshCombinationList);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Used when the user clicks on the Generate button of the modal
|
||||
*/
|
||||
async generateCombinations() {
|
||||
this.loading = true;
|
||||
const data = {
|
||||
attributes: {},
|
||||
};
|
||||
Object.keys(this.selectedAttributeGroups).forEach((attributeGroupId) => {
|
||||
data.attributes[attributeGroupId] = [];
|
||||
this.selectedAttributeGroups[attributeGroupId].attributes.forEach(
|
||||
(attribute) => {
|
||||
data.attributes[attributeGroupId].push(attribute.id);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.combinationsService.generateCombinations(
|
||||
data,
|
||||
);
|
||||
$.growl({
|
||||
message: this.$t('generator.success', {
|
||||
'%combinationsNb%': response.combination_ids.length,
|
||||
}),
|
||||
});
|
||||
this.selectedAttributeGroups = {};
|
||||
this.hasGeneratedCombinations = true;
|
||||
} catch (error) {
|
||||
if (error.responseJSON && error.responseJSON.error) {
|
||||
$.growl.error({message: error.responseJSON.error});
|
||||
} else {
|
||||
$.growl.error({message: error});
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
/**
|
||||
* Remove the attribute if it's selected or add it
|
||||
*
|
||||
* @param {Object} selectedAttribute
|
||||
* @param {{id: int, name: string}} attributeGroup
|
||||
*/
|
||||
changeSelected({selectedAttribute, attributeGroup}) {
|
||||
if (
|
||||
!this.isSelected(
|
||||
selectedAttribute,
|
||||
attributeGroup,
|
||||
this.selectedAttributeGroups,
|
||||
)
|
||||
) {
|
||||
this.addSelected({selectedAttribute, attributeGroup});
|
||||
} else {
|
||||
this.removeSelected({
|
||||
selectedAttribute,
|
||||
selectedAttributeGroup: attributeGroup,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {Object} selectedAttribute
|
||||
* @param {{id: int, name: string}} attributeGroup
|
||||
*/
|
||||
addSelected({selectedAttribute, attributeGroup}) {
|
||||
// Extra check to avoid adding same attribute twice which would cause a duplicate key error
|
||||
if (
|
||||
this.isSelected(
|
||||
selectedAttribute,
|
||||
attributeGroup,
|
||||
this.selectedAttributeGroups,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add copy of attribute group in selected groups
|
||||
if (!this.selectedAttributeGroups[attributeGroup.id]) {
|
||||
const newAttributeGroup = {
|
||||
[attributeGroup.id]: {
|
||||
id: attributeGroup.id,
|
||||
name: attributeGroup.name,
|
||||
attributes: [],
|
||||
},
|
||||
};
|
||||
|
||||
// This is needed to correctly handle observation
|
||||
this.selectedAttributeGroups = {
|
||||
...this.selectedAttributeGroups,
|
||||
...newAttributeGroup,
|
||||
};
|
||||
}
|
||||
|
||||
this.selectedAttributeGroups[attributeGroup.id].attributes.push(
|
||||
selectedAttribute,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @param {Object} selectedAttribute
|
||||
* @param {Object} selectedAttributeGroup
|
||||
*/
|
||||
removeSelected({selectedAttribute, selectedAttributeGroup}) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
this.selectedAttributeGroups,
|
||||
selectedAttributeGroup.id,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = this.selectedAttributeGroups[selectedAttributeGroup.id];
|
||||
group.attributes = group.attributes.filter(
|
||||
(attribute) => attribute.id !== selectedAttribute.id,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Remove the attribute if it's selected or add it
|
||||
*
|
||||
* @param {Object} selectedAttribute
|
||||
* @param {{id: int, name: string}} attributeGroup
|
||||
*/
|
||||
toggleAll({attributeGroup, select}) {
|
||||
if (select) {
|
||||
attributeGroup.attributes.forEach((attribute) => {
|
||||
this.addSelected({selectedAttribute: attribute, attributeGroup});
|
||||
});
|
||||
} else {
|
||||
attributeGroup.attributes.forEach((attribute) => {
|
||||
this.removeSelected({
|
||||
selectedAttribute: attribute,
|
||||
selectedAttributeGroup: attributeGroup,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -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)
|
||||
*/
|
||||
import Vue from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import ReplaceFormatter from '@vue/plugins/vue-i18n/replace-formatter';
|
||||
import CombinationGenerator from '@pages/product/components/generator/CombinationGenerator.vue';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
export default function initCombinationGenerator(combinationGeneratorSelector, eventEmitter, productId) {
|
||||
const container = document.querySelector(combinationGeneratorSelector);
|
||||
|
||||
const translations = JSON.parse(container.dataset.translations);
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
formatter: new ReplaceFormatter(),
|
||||
messages: {en: translations},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el: combinationGeneratorSelector,
|
||||
template: '<combination-generator :productId=productId :eventEmitter=eventEmitter />',
|
||||
components: {CombinationGenerator},
|
||||
i18n,
|
||||
data: {
|
||||
productId,
|
||||
eventEmitter,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default function () {
|
||||
const $defaultArrowWidth = 35;
|
||||
const $arrow = $(ProductMap.jsArrow);
|
||||
const $tabs = $(ProductMap.jsTabs);
|
||||
const $navTabs = $(ProductMap.jsNavTabs);
|
||||
|
||||
let $positions;
|
||||
let $moveTo = 0;
|
||||
let $tabWidth = 0;
|
||||
let $navWidth = $defaultArrowWidth;
|
||||
let $widthWithTabs = 0;
|
||||
|
||||
$navTabs.find('li').each((index, item) => {
|
||||
$navWidth += $(item).width();
|
||||
});
|
||||
|
||||
$widthWithTabs = $navWidth + $defaultArrowWidth * 2;
|
||||
|
||||
$navTabs.width($widthWithTabs);
|
||||
|
||||
$navTabs.find(ProductMap.toggleTab).on('click', (e) => {
|
||||
if (!$(e.target).hasClass('active')) {
|
||||
$(ProductMap.formContentTab).removeClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
$arrow.on('click', (e) => {
|
||||
if ($arrow.is(':visible')) {
|
||||
$tabWidth = $tabs.width();
|
||||
$positions = $navTabs.position();
|
||||
|
||||
$moveTo = '-=0';
|
||||
if ($(e.currentTarget).hasClass('right-arrow')) {
|
||||
if ($tabWidth - $positions.left < $navWidth) {
|
||||
$moveTo = `-=${$tabWidth}`;
|
||||
}
|
||||
} else if ($positions.left < $defaultArrowWidth) {
|
||||
$moveTo = `+=${$tabWidth}`;
|
||||
}
|
||||
|
||||
$navTabs.animate(
|
||||
{
|
||||
left: $moveTo,
|
||||
},
|
||||
400,
|
||||
'easeOutQuad',
|
||||
() => {
|
||||
$(ProductMap.leftArrow).toggleClass('visible', $(e.currentTarget).hasClass('right-arrow'));
|
||||
$(ProductMap.rightArrow).toggleClass('visible', !$(e.currentTarget).hasClass('right-arrow'));
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
/**
|
||||
* Renders the list of combinations in product edit page
|
||||
*/
|
||||
export default class CombinationsGridRenderer {
|
||||
/**
|
||||
* @returns {{render: (function(*=): void)}}
|
||||
*/
|
||||
constructor() {
|
||||
this.$combinationsTable = $(ProductMap.combinations.combinationsTable);
|
||||
this.$combinationsTableBody = $(ProductMap.combinations.combinationsTableBody);
|
||||
this.$loadingSpinner = $(ProductMap.combinations.loadingSpinner);
|
||||
this.prototypeTemplate = this.$combinationsTable.data('prototype');
|
||||
this.prototypeName = this.$combinationsTable.data('prototypeName');
|
||||
|
||||
return {
|
||||
render: (data) => this.render(data),
|
||||
toggleLoading: (loading) => this.toggleLoading(loading),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data expected structure: {combinations: [{Object}, {Object}...], total: {Number}}
|
||||
*/
|
||||
render(data) {
|
||||
this.renderCombinations(data.combinations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Boolean} loading
|
||||
*/
|
||||
toggleLoading(loading) {
|
||||
this.$loadingSpinner.toggle(loading);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} combinations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
renderCombinations(combinations) {
|
||||
this.$combinationsTableBody.empty();
|
||||
|
||||
let rowIndex = 0;
|
||||
combinations.forEach((combination) => {
|
||||
const $row = $(this.getPrototypeRow(rowIndex));
|
||||
|
||||
// fill inputs
|
||||
const $combinationCheckbox = $(ProductMap.combinations.tableRow.combinationCheckbox(rowIndex), $row);
|
||||
const $combinationIdInput = $(ProductMap.combinations.tableRow.combinationIdInput(rowIndex), $row);
|
||||
const $combinationNameInput = $(ProductMap.combinations.tableRow.combinationNameInput(rowIndex), $row);
|
||||
const $quantityInput = $(ProductMap.combinations.tableRow.quantityInput(rowIndex), $row);
|
||||
const $impactOnPriceInput = $(ProductMap.combinations.tableRow.impactOnPriceInput(rowIndex), $row);
|
||||
const $referenceInput = $(ProductMap.combinations.tableRow.referenceInput(rowIndex), $row);
|
||||
// @todo final price should be calculated based on price impact and product price,
|
||||
// so it doesnt need to be in api response
|
||||
const $finalPriceInput = $(ProductMap.combinations.tableRow.finalPriceTeInput(rowIndex), $row);
|
||||
$combinationIdInput.val(combination.id);
|
||||
$combinationNameInput.val(combination.name);
|
||||
// This adds the ID in the checkbox label
|
||||
$combinationCheckbox.closest('label').append(combination.id);
|
||||
// This adds a text after the cell children (do not use text which replaces everything)
|
||||
$combinationNameInput.closest('td').append(combination.name);
|
||||
$finalPriceInput.closest('td').append(combination.finalPriceTe);
|
||||
$referenceInput.val(combination.reference);
|
||||
$referenceInput.data('initial-value', combination.reference);
|
||||
$quantityInput.val(combination.quantity);
|
||||
$quantityInput.data('initial-value', combination.quantity);
|
||||
$impactOnPriceInput.val(combination.impactOnPrice);
|
||||
$impactOnPriceInput.data('initial-value', combination.impactOnPrice);
|
||||
$(ProductMap.combinations.tableRow.editButton(rowIndex), $row).data('id', combination.id);
|
||||
$(ProductMap.combinations.tableRow.deleteButton(rowIndex), $row).data('id', combination.id);
|
||||
$(ProductMap.combinations.tableRow.combinationImg, $row)
|
||||
.attr('src', combination.imageUrl)
|
||||
.attr('alt', combination.name);
|
||||
|
||||
if (combination.isDefault) {
|
||||
$(ProductMap.combinations.tableRow.isDefaultInput(rowIndex), $row).prop('checked', true);
|
||||
}
|
||||
|
||||
this.$combinationsTableBody.append($row);
|
||||
rowIndex += 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} rowIndex
|
||||
*
|
||||
* @returns {String}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
getPrototypeRow(rowIndex) {
|
||||
return this.prototypeTemplate.replace(new RegExp(this.prototypeName, 'g'), rowIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 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 ProductMap from '@pages/product/product-map';
|
||||
import CombinationsGridRenderer from '@pages/product/edit/combinations-grid-renderer';
|
||||
import CombinationsService from '@pages/product/services/combinations-service';
|
||||
import DynamicPaginator from '@components/pagination/dynamic-paginator';
|
||||
import SubmittableInput from '@components/form/submittable-input';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import initCombinationModal from '@pages/product/components/combination-modal';
|
||||
import initFilters from '@pages/product/components/filters';
|
||||
import ConfirmModal from '@components/modal';
|
||||
import initCombinationGenerator from '@pages/product/components/generator';
|
||||
import {getProductAttributeGroups} from '@pages/product/services/attribute-groups';
|
||||
|
||||
const {$} = window;
|
||||
const CombinationEvents = ProductEventMap.combinations;
|
||||
const CombinationsMap = ProductMap.combinations;
|
||||
|
||||
export default class CombinationsManager {
|
||||
/**
|
||||
* @param {int} productId
|
||||
* @returns {{}}
|
||||
*/
|
||||
constructor(productId) {
|
||||
this.productId = productId;
|
||||
this.eventEmitter = window.prestashop.instance.eventEmitter;
|
||||
this.$productForm = $(ProductMap.productForm);
|
||||
this.$combinationsContainer = $(
|
||||
ProductMap.combinations.combinationsContainer,
|
||||
);
|
||||
this.combinationIdInputsSelector = ProductMap.combinations.combinationIdInputsSelector;
|
||||
this.$externalCombinationTab = $(
|
||||
ProductMap.combinations.externalCombinationTab,
|
||||
);
|
||||
|
||||
this.$preloader = $(ProductMap.combinations.preloader);
|
||||
this.$paginatedList = $(CombinationsMap.combinationsPaginatedList);
|
||||
this.$emptyState = $(CombinationsMap.emptyState);
|
||||
|
||||
this.paginator = null;
|
||||
this.combinationsRenderer = null;
|
||||
this.filtersApp = null;
|
||||
this.combinationModalApp = null;
|
||||
this.combinationGeneratorApp = null;
|
||||
|
||||
this.initialized = false;
|
||||
this.combinationsService = new CombinationsService(this.productId);
|
||||
this.productAttributeGroups = [];
|
||||
|
||||
this.init();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
init() {
|
||||
// Paginate to first page when tab is shown
|
||||
this.$productForm
|
||||
.find(CombinationsMap.navigationTab)
|
||||
.on('shown.bs.tab', () => this.showCombinationTab());
|
||||
this.$productForm
|
||||
.find(CombinationsMap.navigationTab)
|
||||
.on('hidden.bs.tab', () => this.hideCombinationTab());
|
||||
|
||||
// Finally watch events related to combination listing
|
||||
this.watchEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
showCombinationTab() {
|
||||
this.$externalCombinationTab.removeClass('d-none');
|
||||
this.firstInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
hideCombinationTab() {
|
||||
this.$externalCombinationTab.addClass('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
firstInit() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
this.combinationGeneratorApp = initCombinationGenerator(
|
||||
CombinationsMap.combinationsGeneratorContainer,
|
||||
this.eventEmitter,
|
||||
this.productId,
|
||||
);
|
||||
this.combinationModalApp = initCombinationModal(
|
||||
CombinationsMap.editModal,
|
||||
this.productId,
|
||||
this.eventEmitter,
|
||||
);
|
||||
this.filtersApp = initFilters(
|
||||
CombinationsMap.combinationsFiltersContainer,
|
||||
this.eventEmitter,
|
||||
this.productAttributeGroups,
|
||||
);
|
||||
this.initPaginatedList();
|
||||
|
||||
this.refreshCombinationList(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} firstTime
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async refreshCombinationList(firstTime) {
|
||||
// Preloader is only shown on first load
|
||||
this.$preloader.toggleClass('d-none', !firstTime);
|
||||
this.$paginatedList.toggleClass('d-none', firstTime);
|
||||
this.$emptyState.addClass('d-none');
|
||||
|
||||
// When attributes are refreshed we show first page
|
||||
this.paginator.paginate(1);
|
||||
|
||||
// Wait for product attributes to adapt rendering depending on their number
|
||||
this.productAttributeGroups = await getProductAttributeGroups(
|
||||
this.productId,
|
||||
);
|
||||
this.filtersApp.filters = this.productAttributeGroups;
|
||||
this.eventEmitter.emit(CombinationEvents.clearFilters);
|
||||
this.$preloader.addClass('d-none');
|
||||
|
||||
const hasCombinations = this.productAttributeGroups && this.productAttributeGroups.length;
|
||||
this.$paginatedList.toggleClass('d-none', !hasCombinations);
|
||||
|
||||
if (!hasCombinations) {
|
||||
// Empty list
|
||||
this.combinationsRenderer.render({combinations: []});
|
||||
this.$emptyState.removeClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
refreshPage() {
|
||||
this.paginator.paginate(this.paginator.getCurrentPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initPaginatedList() {
|
||||
this.combinationsRenderer = new CombinationsGridRenderer();
|
||||
this.paginator = new DynamicPaginator(
|
||||
CombinationsMap.paginationContainer,
|
||||
this.combinationsService,
|
||||
this.combinationsRenderer,
|
||||
);
|
||||
|
||||
this.initSubmittableInputs();
|
||||
|
||||
this.$combinationsContainer.on(
|
||||
'change',
|
||||
CombinationsMap.isDefaultInputsSelector,
|
||||
async (e) => {
|
||||
if (!e.currentTarget.checked) {
|
||||
return;
|
||||
}
|
||||
await this.updateDefaultCombination(e.currentTarget);
|
||||
},
|
||||
);
|
||||
|
||||
this.$combinationsContainer.on(
|
||||
'click',
|
||||
CombinationsMap.removeCombinationSelector,
|
||||
async (e) => {
|
||||
await this.removeCombination(e.currentTarget);
|
||||
},
|
||||
);
|
||||
|
||||
this.initSortingColumns();
|
||||
this.paginator.paginate(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
watchEvents() {
|
||||
/* eslint-disable */
|
||||
this.eventEmitter.on(CombinationEvents.refreshCombinationList, () =>
|
||||
this.refreshCombinationList(false)
|
||||
);
|
||||
this.eventEmitter.on(CombinationEvents.refreshPage, () =>
|
||||
this.refreshPage()
|
||||
);
|
||||
/* eslint-disable */
|
||||
this.eventEmitter.on(
|
||||
CombinationEvents.updateAttributeGroups,
|
||||
attributeGroups => {
|
||||
const currentFilters = this.combinationsService.getFilters();
|
||||
currentFilters.attributes = {};
|
||||
Object.keys(attributeGroups).forEach(attributeGroupId => {
|
||||
currentFilters.attributes[attributeGroupId] = [];
|
||||
const attributes = attributeGroups[attributeGroupId];
|
||||
attributes.forEach(attribute => {
|
||||
currentFilters.attributes[attributeGroupId].push(attribute.id);
|
||||
});
|
||||
});
|
||||
|
||||
this.combinationsService.setFilters(currentFilters);
|
||||
this.paginator.paginate(1);
|
||||
}
|
||||
);
|
||||
|
||||
this.eventEmitter.on(CombinationEvents.combinationGeneratorReady, () => {
|
||||
const $generateButtons = $(
|
||||
ProductMap.combinations.generateCombinationsButton
|
||||
);
|
||||
$generateButtons.prop('disabled', false);
|
||||
$('body').on(
|
||||
'click',
|
||||
ProductMap.combinations.generateCombinationsButton,
|
||||
event => {
|
||||
// Stop event or it will be caught by click-outside directive and automatically close the modal
|
||||
event.stopImmediatePropagation();
|
||||
this.eventEmitter.emit(CombinationEvents.openCombinationsGenerator);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initSubmittableInputs() {
|
||||
const combinationToken = this.getCombinationToken();
|
||||
const { quantityKey } = CombinationsMap.combinationItemForm;
|
||||
const { impactOnPriceKey } = CombinationsMap.combinationItemForm;
|
||||
const { referenceKey } = CombinationsMap.combinationItemForm;
|
||||
const { tokenKey } = CombinationsMap.combinationItemForm;
|
||||
|
||||
/* eslint-disable */
|
||||
new SubmittableInput(CombinationsMap.quantityInputWrapper, input =>
|
||||
this.combinationsService.updateListedCombination(
|
||||
this.findCombinationId(input),
|
||||
{
|
||||
[quantityKey]: input.value,
|
||||
[tokenKey]: combinationToken
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
new SubmittableInput(CombinationsMap.impactOnPriceInputWrapper, input =>
|
||||
this.combinationsService.updateListedCombination(
|
||||
this.findCombinationId(input),
|
||||
{
|
||||
[impactOnPriceKey]: input.value,
|
||||
[tokenKey]: combinationToken
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
new SubmittableInput(CombinationsMap.referenceInputWrapper, input =>
|
||||
this.combinationsService.updateListedCombination(
|
||||
this.findCombinationId(input),
|
||||
{
|
||||
[referenceKey]: input.value,
|
||||
[tokenKey]: combinationToken
|
||||
}
|
||||
)
|
||||
);
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initSortingColumns() {
|
||||
this.$combinationsContainer.on(
|
||||
'click',
|
||||
CombinationsMap.sortableColumns,
|
||||
(event) => {
|
||||
const $sortableColumn = $(event.currentTarget);
|
||||
const columnName = $sortableColumn.data('sortColName');
|
||||
|
||||
if (!columnName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let direction = $sortableColumn.data('sortDirection');
|
||||
|
||||
if (!direction || direction === 'desc') {
|
||||
direction = 'asc';
|
||||
} else {
|
||||
direction = 'desc';
|
||||
}
|
||||
|
||||
// Reset all columns, we need to force the attributes for CSS matching
|
||||
$(
|
||||
CombinationsMap.sortableColumns,
|
||||
this.$combinationsContainer,
|
||||
).removeData('sortIsCurrent');
|
||||
$(
|
||||
CombinationsMap.sortableColumns,
|
||||
this.$combinationsContainer,
|
||||
).removeData('sortDirection');
|
||||
$(
|
||||
CombinationsMap.sortableColumns,
|
||||
this.$combinationsContainer,
|
||||
).removeAttr('data-sort-is-current');
|
||||
$(
|
||||
CombinationsMap.sortableColumns,
|
||||
this.$combinationsContainer,
|
||||
).removeAttr('data-sort-direction');
|
||||
|
||||
// Set correct data in current column, we need to force the attributes for CSS matching
|
||||
$sortableColumn.data('sortIsCurrent', 'true');
|
||||
$sortableColumn.data('sortDirection', direction);
|
||||
$sortableColumn.attr('data-sort-is-current', 'true');
|
||||
$sortableColumn.attr('data-sort-direction', direction);
|
||||
|
||||
// Finally update list
|
||||
this.combinationsService.setOrderBy(columnName, direction);
|
||||
this.paginator.paginate(1);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} button
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async removeCombination(button) {
|
||||
try {
|
||||
const $deleteButton = $(button);
|
||||
const modal = new ConfirmModal(
|
||||
{
|
||||
id: 'modal-confirm-delete-combination',
|
||||
confirmTitle: $deleteButton.data('modal-title'),
|
||||
confirmMessage: $deleteButton.data('modal-message'),
|
||||
confirmButtonLabel: $deleteButton.data('modal-apply'),
|
||||
closeButtonLabel: $deleteButton.data('modal-cancel'),
|
||||
confirmButtonClass: 'btn-danger',
|
||||
closable: true,
|
||||
},
|
||||
async () => {
|
||||
const response = await this.combinationsService.removeCombination(
|
||||
this.findCombinationId(button),
|
||||
);
|
||||
$.growl({message: response.message});
|
||||
this.eventEmitter.emit(CombinationEvents.refreshCombinationList);
|
||||
},
|
||||
);
|
||||
modal.show();
|
||||
} catch (error) {
|
||||
const errorMessage = error.responseJSON
|
||||
? error.responseJSON.error
|
||||
: error;
|
||||
$.growl.error({message: errorMessage});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} checkedInput
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async updateDefaultCombination(checkedInput) {
|
||||
const checkedInputs = this.$combinationsContainer.find(
|
||||
`${CombinationsMap.isDefaultInputsSelector}:checked`,
|
||||
);
|
||||
const checkedDefaultId = this.findCombinationId(checkedInput);
|
||||
|
||||
await this.combinationsService.updateListedCombination(checkedDefaultId, {
|
||||
'combination_item[is_default]': checkedInput.value,
|
||||
'combination_item[_token]': this.getCombinationToken(),
|
||||
});
|
||||
|
||||
$.each(checkedInputs, (index, input) => {
|
||||
if (this.findCombinationId(input) !== checkedDefaultId) {
|
||||
$(input).prop('checked', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
*/
|
||||
getCombinationToken() {
|
||||
return $(CombinationsMap.combinationsContainer).data('combinationToken');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} input of the same table row
|
||||
*
|
||||
* @returns {Number}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
findCombinationId(input) {
|
||||
return $(input)
|
||||
.closest('tr')
|
||||
.find(this.combinationIdInputsSelector)
|
||||
.val();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
import ConfirmModal from '@components/modal';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class CustomizationsManager {
|
||||
constructor() {
|
||||
this.$customizationsContainer = $(ProductMap.customizations.customizationsContainer);
|
||||
this.$customizationFieldsList = $(ProductMap.customizations.customizationFieldsList);
|
||||
this.eventEmitter = window.prestashop.instance.eventEmitter;
|
||||
this.prototypeTemplate = this.$customizationFieldsList.data('prototype');
|
||||
this.prototypeName = this.$customizationFieldsList.data('prototypeName');
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$customizationsContainer.on('click', ProductMap.customizations.addCustomizationBtn, () => {
|
||||
this.addCustomizationField();
|
||||
});
|
||||
this.$customizationsContainer.on('click', ProductMap.customizations.removeCustomizationBtn, (e) => {
|
||||
this.removeCustomizationField(e);
|
||||
});
|
||||
}
|
||||
|
||||
addCustomizationField() {
|
||||
const index = this.getIndex();
|
||||
const newItem = this.prototypeTemplate.replace(new RegExp(this.prototypeName, 'g'), this.getIndex());
|
||||
|
||||
this.$customizationFieldsList.append(newItem);
|
||||
window.prestaShopUiKit.initToolTips();
|
||||
const {translatableInput} = window.prestashop.instance;
|
||||
translatableInput.refreshFormInputs(this.$customizationsContainer.closest('form'));
|
||||
this.eventEmitter.emit(ProductEventMap.customizations.rowAdded, {index});
|
||||
}
|
||||
|
||||
removeCustomizationField(event) {
|
||||
const $deleteButton = $(event.currentTarget);
|
||||
const modal = new ConfirmModal(
|
||||
{
|
||||
id: 'modal-confirm-delete-customization',
|
||||
confirmTitle: $deleteButton.data('modal-title'),
|
||||
confirmMessage: $deleteButton.data('modal-message'),
|
||||
confirmButtonLabel: $deleteButton.data('modal-apply'),
|
||||
closeButtonLabel: $deleteButton.data('modal-cancel'),
|
||||
confirmButtonClass: 'btn-danger',
|
||||
closable: true,
|
||||
},
|
||||
() => {
|
||||
$deleteButton
|
||||
.closest(ProductMap.customizations.customizationFieldRow)
|
||||
.remove();
|
||||
this.eventEmitter.emit(ProductEventMap.customizations.rowRemoved);
|
||||
},
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
getIndex() {
|
||||
return this.$customizationFieldsList.find(ProductMap.customizations.customizationFieldRow).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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 ProductMap from '@pages/product/product-map';
|
||||
import Router from '@components/router';
|
||||
import ConfirmModal from '@components/modal';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class FeatureValuesManager {
|
||||
/**
|
||||
* @param eventEmitter {EventEmitter}
|
||||
*/
|
||||
constructor(eventEmitter) {
|
||||
this.router = new Router();
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.$collectionContainer = $(ProductMap.featureValues.collectionContainer);
|
||||
this.$collectionRowsContainer = $(ProductMap.featureValues.collectionRowsContainer);
|
||||
|
||||
this.watchFeatureSelectors();
|
||||
this.watchCustomInputs();
|
||||
this.watchDeleteButtons();
|
||||
this.watchAddButton();
|
||||
}
|
||||
|
||||
watchAddButton() {
|
||||
$(ProductMap.featureValues.addFeatureValue).on('click', () => {
|
||||
const prototype = this.$collectionContainer.data('prototype');
|
||||
const prototypeName = this.$collectionContainer.data('prototypeName');
|
||||
const newIndex = $(ProductMap.featureValues.collectionRow, this.$collectionContainer).length;
|
||||
|
||||
const $newRow = $(prototype.replace(new RegExp(prototypeName, 'g'), newIndex));
|
||||
this.$collectionRowsContainer.append($newRow);
|
||||
$('select[data-toggle="select2"]', $newRow).select2();
|
||||
});
|
||||
}
|
||||
|
||||
watchDeleteButtons() {
|
||||
$(this.$collectionContainer).on('click', ProductMap.featureValues.deleteFeatureValue, (event) => {
|
||||
const $deleteButton = $(event.currentTarget);
|
||||
const $collectionRow = $deleteButton.closest(ProductMap.featureValues.collectionRow);
|
||||
const modal = new ConfirmModal(
|
||||
{
|
||||
id: 'modal-confirm-delete-feature-value',
|
||||
confirmTitle: $deleteButton.data('modal-title'),
|
||||
confirmMessage: $deleteButton.data('modal-message'),
|
||||
confirmButtonLabel: $deleteButton.data('modal-apply'),
|
||||
closeButtonLabel: $deleteButton.data('modal-cancel'),
|
||||
confirmButtonClass: 'btn-danger',
|
||||
closable: true,
|
||||
},
|
||||
() => {
|
||||
$collectionRow.remove();
|
||||
this.eventEmitter.emit(ProductEventMap.updateSubmitButtonState);
|
||||
},
|
||||
);
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
watchCustomInputs() {
|
||||
$(this.$collectionContainer).on('keyup change', ProductMap.featureValues.customValueInput, (event) => {
|
||||
const $changedInput = $(event.target);
|
||||
const $collectionRow = $changedInput.closest(ProductMap.featureValues.collectionRow);
|
||||
|
||||
// Check if any custom inputs has a value
|
||||
let hasCustomValue = false;
|
||||
$(ProductMap.featureValues.customValueInput, $collectionRow).each((index, input) => {
|
||||
const $input = $(input);
|
||||
|
||||
if ($input.val() !== '') {
|
||||
hasCustomValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
const $featureValueSelector = $(ProductMap.featureValues.featureValueSelect, $collectionRow).first();
|
||||
$featureValueSelector.prop('disabled', hasCustomValue);
|
||||
if (hasCustomValue) {
|
||||
$featureValueSelector.val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watchFeatureSelectors() {
|
||||
$(this.$collectionContainer).on('change', ProductMap.featureValues.featureSelect, (event) => {
|
||||
const $selector = $(event.target);
|
||||
const idFeature = $selector.val();
|
||||
const $collectionRow = $selector.closest(ProductMap.featureValues.collectionRow);
|
||||
const $featureValueSelector = $(ProductMap.featureValues.featureValueSelect, $collectionRow).first();
|
||||
const $customValueInputs = $(ProductMap.featureValues.customValueInput, $collectionRow);
|
||||
const $customFeatureIdInput = $(ProductMap.featureValues.customFeatureIdInput, $collectionRow);
|
||||
|
||||
// Reset values
|
||||
$customValueInputs.val('');
|
||||
$featureValueSelector.val('');
|
||||
$customFeatureIdInput.val('');
|
||||
|
||||
$.get(this.router.generate('admin_feature_get_feature_values', {idFeature}))
|
||||
.then((featureValuesData) => {
|
||||
$featureValueSelector.prop('disabled', featureValuesData.length === 0);
|
||||
$featureValueSelector.empty();
|
||||
$.each(featureValuesData, (index, featureValue) => {
|
||||
// The placeholder shouldn't be posted.
|
||||
if (featureValue.id === '0') {
|
||||
featureValue.id = '';
|
||||
}
|
||||
|
||||
$featureValueSelector
|
||||
.append($('<option></option>')
|
||||
.attr('value', featureValue.id)
|
||||
.text(featureValue.value));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
107
iadmin/themes/new-theme/js/pages/product/edit/index.js
Normal file
107
iadmin/themes/new-theme/js/pages/product/edit/index.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 NavbarHandler from '@components/navbar-handler';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
import CategoriesManager from '@pages/product/components/categories';
|
||||
import CombinationsManager from '@pages/product/edit/combinations-manager';
|
||||
import CustomizationsManager from '@pages/product/edit/customizations-manager';
|
||||
import FeatureValuesManager from '@pages/product/edit/feature-values-manager';
|
||||
import ProductFooterManager from '@pages/product/edit/product-footer-manager';
|
||||
import ProductFormModel from '@pages/product/edit/product-form-model';
|
||||
import ProductModulesManager from '@pages/product/edit/product-modules-manager';
|
||||
import ProductPartialUpdater from '@pages/product/edit/product-partial-updater';
|
||||
import ProductSEOManager from '@pages/product/edit/product-seo-manager';
|
||||
import ProductSuppliersManager from '@pages/product/edit/product-suppliers-manager';
|
||||
import ProductTypeManager from '@pages/product/edit/product-type-manager';
|
||||
import VirtualProductManager from '@pages/product/edit/virtual-product-manager';
|
||||
|
||||
import initDropzone from '@pages/product/components/dropzone';
|
||||
import initTabs from '@pages/product/components/nav-tabs';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
$(() => {
|
||||
window.prestashop.component.initComponents([
|
||||
'TranslatableField',
|
||||
'TinyMCEEditor',
|
||||
'TranslatableInput',
|
||||
'EventEmitter',
|
||||
'TextWithLengthCounter',
|
||||
]);
|
||||
|
||||
const $productForm = $(ProductMap.productForm);
|
||||
const productId = parseInt($productForm.data('productId'), 10);
|
||||
const productType = $productForm.data('productType');
|
||||
|
||||
// Responsive navigation tabs
|
||||
initTabs();
|
||||
|
||||
const {eventEmitter} = window.prestashop.instance;
|
||||
|
||||
// Init product model along with input watching and syncing
|
||||
const productFormModel = new ProductFormModel($productForm, eventEmitter);
|
||||
|
||||
if (productId && productType === ProductMap.productType.COMBINATIONS) {
|
||||
// Combinations manager must be initialized BEFORE nav handler, or it won't trigger the pagination if the tab is
|
||||
// selected on load, it is only initialized when productId exists though (edition mode)
|
||||
new CombinationsManager(productId);
|
||||
}
|
||||
|
||||
new NavbarHandler(ProductMap.navigationBar);
|
||||
new ProductSEOManager();
|
||||
|
||||
// Product type has strong impact on the page rendering so when it is modified it must be submitted right away
|
||||
new ProductTypeManager($(ProductMap.productTypeSelector), $productForm);
|
||||
new CategoriesManager(eventEmitter);
|
||||
new ProductFooterManager();
|
||||
new ProductModulesManager();
|
||||
|
||||
const $productFormSubmitButton = $(ProductMap.productFormSubmitButton);
|
||||
new ProductPartialUpdater(
|
||||
eventEmitter,
|
||||
$productForm,
|
||||
$productFormSubmitButton,
|
||||
).watch();
|
||||
|
||||
// Form has no productId data means that we are in creation mode
|
||||
if (!productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// From here we init component specific to edition
|
||||
initDropzone(ProductMap.dropzoneImagesContainer);
|
||||
|
||||
new FeatureValuesManager(eventEmitter);
|
||||
new CustomizationsManager();
|
||||
|
||||
if (productType !== ProductMap.productType.COMBINATIONS) {
|
||||
new ProductSuppliersManager(ProductMap.suppliers.productSuppliers, true);
|
||||
}
|
||||
if (productType === ProductMap.productType.VIRTUAL) {
|
||||
new VirtualProductManager(productFormModel);
|
||||
}
|
||||
});
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import ConfirmModal from '@components/modal';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
export default class ProductFooterManager {
|
||||
constructor() {
|
||||
this.$deleteProductButton = $(ProductMap.footer.deleteProductButton);
|
||||
this.$deleteProductButton.click(() => this.deleteProduct());
|
||||
}
|
||||
|
||||
deleteProduct() {
|
||||
const modal = new ConfirmModal(
|
||||
{
|
||||
id: 'modal-confirm-delete-product',
|
||||
confirmTitle: this.$deleteProductButton.data('modal-title'),
|
||||
confirmMessage: this.$deleteProductButton.data('modal-message'),
|
||||
confirmButtonLabel: this.$deleteProductButton.data('modal-apply'),
|
||||
closeButtonLabel: this.$deleteProductButton.data('modal-cancel'),
|
||||
confirmButtonClass: 'btn-danger',
|
||||
closable: true,
|
||||
},
|
||||
() => {
|
||||
const removeUrl = this.$deleteProductButton.data('removeUrl');
|
||||
$(ProductMap.productFormSubmitButton).prop('disabled', true);
|
||||
window.location = removeUrl;
|
||||
},
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
export default {
|
||||
'product.stock.quantity': [
|
||||
'product[stock][quantities][quantity]',
|
||||
'product[shortcuts][stock][quantity]',
|
||||
],
|
||||
'product.price.priceTaxExcluded': [
|
||||
'product[pricing][retail_price][price_tax_excluded]',
|
||||
'product[shortcuts][retail_price][price_tax_excluded]',
|
||||
],
|
||||
'product.price.priceTaxIncluded': [
|
||||
'product[pricing][retail_price][price_tax_included]',
|
||||
'product[shortcuts][retail_price][price_tax_included]',
|
||||
],
|
||||
'product.price.taxRulesGroupId': [
|
||||
'product[pricing][tax_rules_group_id]',
|
||||
'product[shortcuts][retail_price][tax_rules_group_id]',
|
||||
],
|
||||
'product.stock.hasVirtualProductFile': 'product[stock][virtual_product_file][has_file]',
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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 BigNumber from 'bignumber.js';
|
||||
import ObjectFormMapper from '@components/form/form-object-mapper';
|
||||
import ProductFormMapping from '@pages/product/edit/product-form-mapping';
|
||||
import ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
export default class ProductFormModel {
|
||||
constructor($form, eventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
|
||||
// Init form mapper
|
||||
this.mapper = new ObjectFormMapper(
|
||||
$form,
|
||||
ProductFormMapping,
|
||||
eventEmitter,
|
||||
{
|
||||
modelUpdated: ProductEventMap.productModelUpdated,
|
||||
updateModel: ProductEventMap.updatedProductModel,
|
||||
modelFieldUpdated: ProductEventMap.updatedProductField,
|
||||
},
|
||||
);
|
||||
|
||||
// For now we get precision only in the component, but maybe it would deserve a more global configuration
|
||||
// BigNumber.set({DECIMAL_PLACES: someConfig}) But where can we define/inject this global config?
|
||||
const $priceTaxExcludedInput = this.mapper.getInputsFor('product.price.priceTaxExcluded');
|
||||
this.precision = $priceTaxExcludedInput.data('displayPricePrecision');
|
||||
|
||||
// Listens to event for product modification (registered after the model is constructed, because events are
|
||||
// triggered during the initial parsing but don't need them at first).
|
||||
this.eventEmitter.on(ProductEventMap.updatedProductField, (event) => this.productFieldUpdated(event));
|
||||
|
||||
return {
|
||||
getProduct: () => this.getProduct(),
|
||||
watch: (productModelKey, callback) => this.watchProductModel(productModelKey, callback),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
getProduct() {
|
||||
return this.mapper.getModel().product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} productModelKey
|
||||
* @param {function} callback
|
||||
*/
|
||||
watchProductModel(productModelKey, callback) {
|
||||
this.mapper.watch(`product.${productModelKey}`, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles modifications that have happened in the product
|
||||
*
|
||||
* @param {Object} event
|
||||
* @private
|
||||
*/
|
||||
productFieldUpdated(event) {
|
||||
this.updateProductPrices(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific handler for modifications related to the product price
|
||||
*
|
||||
* @param {Object} event
|
||||
* @private
|
||||
*/
|
||||
updateProductPrices(event) {
|
||||
const pricesFields = [
|
||||
'product.price.priceTaxIncluded',
|
||||
'product.price.priceTaxExcluded',
|
||||
'product.price.taxRulesGroupId',
|
||||
];
|
||||
|
||||
if (!pricesFields.includes(event.modelKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $taxRulesGroupIdInput = this.mapper.getInputsFor('product.price.taxRulesGroupId');
|
||||
const $selectedTaxOption = $(':selected', $taxRulesGroupIdInput);
|
||||
|
||||
let taxRate;
|
||||
try {
|
||||
taxRate = new BigNumber($selectedTaxOption.data('taxRate'));
|
||||
} catch (error) {
|
||||
taxRate = BigNumber.NaN;
|
||||
}
|
||||
if (taxRate.isNaN()) {
|
||||
taxRate = new BigNumber(0);
|
||||
}
|
||||
|
||||
const taxRatio = taxRate.dividedBy(100).plus(1);
|
||||
|
||||
switch (event.modelKey) {
|
||||
case 'product.price.priceTaxIncluded': {
|
||||
const priceTaxIncluded = new BigNumber(this.getProduct().price.priceTaxIncluded);
|
||||
this.mapper.set(
|
||||
'product.price.priceTaxExcluded',
|
||||
priceTaxIncluded.dividedBy(taxRatio).toFixed(this.precision),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const priceTaxExcluded = new BigNumber(this.getProduct().price.priceTaxExcluded);
|
||||
this.mapper.set('product.price.priceTaxIncluded', priceTaxExcluded.times(taxRatio).toFixed(this.precision));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ProductMap from '@pages/product/product-map';
|
||||
|
||||
export default class ProductModulesManager {
|
||||
constructor() {
|
||||
this.$previewContainer = $(ProductMap.modules.previewContainer);
|
||||
this.$selectorContainer = $(ProductMap.modules.selectorContainer);
|
||||
this.$contentContainer = $(ProductMap.modules.contentContainer);
|
||||
this.$moduleSelector = $(ProductMap.modules.moduleSelector);
|
||||
this.$selectorPreviews = $(ProductMap.modules.selectorPreviews);
|
||||
this.$moduleContents = $(ProductMap.modules.moduleContents);
|
||||
|
||||
this.init();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
init() {
|
||||
this.$previewContainer.removeClass('d-none');
|
||||
this.$selectorContainer.addClass('d-none');
|
||||
this.$contentContainer.addClass('d-none');
|
||||
this.$selectorPreviews.addClass('d-none');
|
||||
this.$moduleContents.addClass('d-none');
|
||||
|
||||
this.$previewContainer.on('click', ProductMap.modules.previewButton, (event) => {
|
||||
const $button = $(event.target);
|
||||
this.selectModule($button.data('target'));
|
||||
});
|
||||
|
||||
this.$moduleSelector.on('change', () => this.showSelectedModule());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} moduleId
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
selectModule(moduleId) {
|
||||
this.$previewContainer.addClass('d-none');
|
||||
this.$selectorContainer.removeClass('d-none');
|
||||
this.$contentContainer.removeClass('d-none');
|
||||
|
||||
this.$moduleSelector.val(moduleId);
|
||||
// trigger change because this is a select2 component, and module is switched when change even triggers
|
||||
this.$moduleSelector.trigger('change');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
showSelectedModule() {
|
||||
this.$selectorPreviews.addClass('d-none');
|
||||
this.$moduleContents.addClass('d-none');
|
||||
|
||||
const moduleId = this.$moduleSelector.val();
|
||||
$(ProductMap.modules.selectorPreview(moduleId)).removeClass('d-none');
|
||||
$(ProductMap.modules.moduleContent(moduleId)).removeClass('d-none');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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 ProductEventMap from '@pages/product/product-event-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
/**
|
||||
* When product is edited we want to send only partial updates
|
||||
* so this class compares the initial data from the form computes
|
||||
* the diff when form is submitted And dynamically build another
|
||||
* form to submit only updated data (along with required fields
|
||||
* token and such).
|
||||
*
|
||||
* It also disabled the submit button as long as no data has been
|
||||
* modified by the user.
|
||||
*/
|
||||
export default class ProductPartialUpdater {
|
||||
/**
|
||||
* @param eventEmitter {EventEmitter}
|
||||
* @param $productForm {jQuery}
|
||||
* @param $productFormSubmitButton {jQuery}
|
||||
*/
|
||||
constructor(eventEmitter, $productForm, $productFormSubmitButton) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.$productForm = $productForm;
|
||||
this.$productFormSubmitButton = $productFormSubmitButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* This the public method you need to use to start this component
|
||||
* ex: new ProductPartialUpdater($productForm, $productFormSubmitButton).watch();
|
||||
*/
|
||||
watch() {
|
||||
// Avoid submitting form when pressing Enter
|
||||
this.$productForm.keypress((e) => e.which !== 13);
|
||||
this.$productFormSubmitButton.prop('disabled', true);
|
||||
this.initialData = this.getFormDataAsObject();
|
||||
this.$productForm.submit(() => this.updatePartialForm());
|
||||
// 'dp.change' event allows tracking datepicker input changes
|
||||
this.$productForm.on('keyup change dp.change', ':input', () => this.updateSubmitButtonState());
|
||||
this.eventEmitter.on(ProductEventMap.updateSubmitButtonState, () => this.updateSubmitButtonState());
|
||||
|
||||
this.watchCustomizations();
|
||||
this.initFormattedTextarea();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch events specifically related to customizations subform
|
||||
*/
|
||||
watchCustomizations() {
|
||||
this.eventEmitter.on(ProductEventMap.customizations.rowAdded, () => this.updateSubmitButtonState());
|
||||
this.eventEmitter.on(ProductEventMap.customizations.rowRemoved, () => this.updateSubmitButtonState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich editors apply a layer over initial textarea fields therefore they need to be watched differently.
|
||||
*/
|
||||
initFormattedTextarea() {
|
||||
this.eventEmitter.on('tinymceEditorSetup', (event) => {
|
||||
event.editor.on('change', () => this.updateSubmitButtonState());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods handles the form submit
|
||||
*
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
updatePartialForm() {
|
||||
const updatedData = this.getUpdatedFormData();
|
||||
|
||||
if (updatedData !== null) {
|
||||
let formMethod = this.$productForm.prop('method');
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(updatedData, '_method')) {
|
||||
// eslint-disable-next-line dot-notation
|
||||
formMethod = updatedData['_method'];
|
||||
}
|
||||
|
||||
if (formMethod !== 'PATCH') {
|
||||
// Returning true will continue submitting form as usual
|
||||
return true;
|
||||
}
|
||||
// On patch method we extract changed values and submit only them
|
||||
this.submitUpdatedData(updatedData);
|
||||
} else {
|
||||
// @todo: This is temporary we should probably use a nice modal instead, that said since the submit button is
|
||||
// disabled when no data has been modified it should never happen
|
||||
alert('no fields updated');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically build a form with provided updated data and submit this "shadow" form
|
||||
*
|
||||
* @param updatedData {Object} Contains an object with all form fields to update indexed by query parameters name
|
||||
*/
|
||||
submitUpdatedData(updatedData) {
|
||||
this.$productFormSubmitButton.prop('disabled', true);
|
||||
const $updatedForm = this.createShadowForm(updatedData);
|
||||
|
||||
$updatedForm.appendTo('body');
|
||||
$updatedForm.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param updatedData
|
||||
*
|
||||
* @returns {Object} Form clone (Jquery object)
|
||||
*/
|
||||
createShadowForm(updatedData) {
|
||||
const $updatedForm = this.$productForm.clone();
|
||||
$updatedForm.empty();
|
||||
$updatedForm.prop('class', '');
|
||||
Object.keys(updatedData).forEach((fieldName) => {
|
||||
if (Array.isArray(updatedData[fieldName])) {
|
||||
updatedData[fieldName].forEach((value) => {
|
||||
this.appendInputToForm($updatedForm, fieldName, value);
|
||||
});
|
||||
} else {
|
||||
this.appendInputToForm($updatedForm, fieldName, updatedData[fieldName]);
|
||||
}
|
||||
});
|
||||
|
||||
return $updatedForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the submit button state, as long as no data has been updated the button is disabled
|
||||
*/
|
||||
updateSubmitButtonState() {
|
||||
const updatedData = this.getUpdatedFormData();
|
||||
this.$productFormSubmitButton.prop('disabled', updatedData === null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the updated data, only fields which are different from the initial page load
|
||||
* are returned (token and method are added since they are required for a valid request).
|
||||
*
|
||||
* If no fields have been modified this method returns null.
|
||||
*
|
||||
* @returns {{}|null}
|
||||
*/
|
||||
getUpdatedFormData() {
|
||||
const currentData = this.getFormDataAsObject();
|
||||
// Loop through current form data and remove the one that did not change
|
||||
// This way only updated AND new values remain
|
||||
Object.keys(this.initialData).forEach((fieldName) => {
|
||||
const fieldValue = this.initialData[fieldName];
|
||||
|
||||
// Field is absent in the new data (it was not in the initial) we force it to empty string (not null
|
||||
// or it will be ignored)
|
||||
if (!Object.prototype.hasOwnProperty.call(currentData, fieldName)) {
|
||||
currentData[fieldName] = '';
|
||||
} else if (_.isEqual(currentData[fieldName], fieldValue)) {
|
||||
delete currentData[fieldName];
|
||||
}
|
||||
});
|
||||
// No need to loop through the field contained in currentData and not in the initial
|
||||
// they are new values so are, by fact, updated values
|
||||
|
||||
if (Object.keys(currentData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Some parameters are always needed
|
||||
const permanentParameters = [
|
||||
// We need the form CSRF token
|
||||
'product[_token]',
|
||||
// If method is not POST or GET a hidden type input is used to simulate it (like PATCH)
|
||||
'_method',
|
||||
];
|
||||
permanentParameters.forEach((permanentParameter) => {
|
||||
if (Object.prototype.hasOwnProperty.call(this.initialData, permanentParameter)) {
|
||||
currentData[permanentParameter] = this.initialData[permanentParameter];
|
||||
}
|
||||
});
|
||||
|
||||
return currentData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized form data as an Object indexed by field name
|
||||
*
|
||||
* @returns {{}}
|
||||
*/
|
||||
getFormDataAsObject() {
|
||||
const formArray = this.$productForm.serializeArray();
|
||||
const serializedForm = {};
|
||||
|
||||
formArray.forEach((formField) => {
|
||||
let {value} = formField;
|
||||
|
||||
// Input names can be identical when expressing array of values for same field (like multiselect checkboxes)
|
||||
// so we need to put these input values into single array indexed by that field name
|
||||
if (formField.name.endsWith('[]')) {
|
||||
let multiField = [];
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(serializedForm, formField.name)) {
|
||||
multiField = serializedForm[formField.name];
|
||||
}
|
||||
|
||||
multiField.push(formField.value);
|
||||
value = multiField;
|
||||
}
|
||||
|
||||
serializedForm[formField.name] = value;
|
||||
});
|
||||
|
||||
// File inputs must be handled manually
|
||||
$('input[type="file"]', this.$productForm).each((inputIndex, fileInput) => {
|
||||
$.each($(fileInput)[0].files, (fileIndex, file) => {
|
||||
serializedForm[fileInput.name] = file;
|
||||
});
|
||||
});
|
||||
|
||||
return serializedForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $form
|
||||
* @param name
|
||||
* @param value
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
appendInputToForm($form, name, value) {
|
||||
$('<input>').attr({
|
||||
name,
|
||||
type: 'hidden',
|
||||
value,
|
||||
}).appendTo($form);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 Serp from '@app/utils/serp';
|
||||
import RedirectOptionManager from '@pages/product/edit/redirect-option-manager';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class ProductSEOManager {
|
||||
constructor() {
|
||||
this.$previewButton = $(ProductMap.footer.previewUrlButton);
|
||||
|
||||
this.init();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
init() {
|
||||
// Init the product/category search field for redirection target
|
||||
const $redirectTypeInput = $(ProductMap.seo.redirectOption.typeInput);
|
||||
const $redirectTargetInput = $(ProductMap.seo.redirectOption.targetInput);
|
||||
new RedirectOptionManager($redirectTypeInput, $redirectTargetInput);
|
||||
|
||||
// Init Serp component to preview Search engine display
|
||||
const {translatableInput, translatableField} = window.prestashop.instance;
|
||||
let previewUrl = this.$previewButton.data('seoUrl');
|
||||
|
||||
if (!previewUrl) {
|
||||
previewUrl = '';
|
||||
}
|
||||
|
||||
new Serp(
|
||||
{
|
||||
container: ProductMap.seo.container,
|
||||
defaultTitle: ProductMap.seo.defaultTitle,
|
||||
watchedTitle: ProductMap.seo.watchedTitle,
|
||||
defaultDescription: ProductMap.seo.defaultDescription,
|
||||
watchedDescription: ProductMap.seo.watchedDescription,
|
||||
watchedMetaUrl: ProductMap.seo.watchedMetaUrl,
|
||||
multiLanguageInput: `${translatableInput.localeInputSelector}:not(.d-none)`,
|
||||
multiLanguageField: `${translatableField.translationFieldSelector}.active`,
|
||||
},
|
||||
previewUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 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 SuppliersMap from '@pages/product/suppliers-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class ProductSuppliersManager {
|
||||
/**
|
||||
*
|
||||
* @param {string} suppliersFormId
|
||||
* @param {boolean} forceUpdateDefault
|
||||
*
|
||||
* @returns {{}}
|
||||
*/
|
||||
constructor(suppliersFormId, forceUpdateDefault) {
|
||||
this.forceUpdateDefault = forceUpdateDefault;
|
||||
this.suppliersMap = SuppliersMap(suppliersFormId);
|
||||
this.$productSuppliersCollection = $(this.suppliersMap.productSuppliersCollection);
|
||||
this.$supplierIdsGroup = $(this.suppliersMap.supplierIdsInput).closest('.form-group');
|
||||
this.$defaultSupplierGroup = $(this.suppliersMap.defaultSupplierInput).closest('.form-group');
|
||||
this.$productsTable = $(this.suppliersMap.productSuppliersTable);
|
||||
this.$productsTableBody = $(this.suppliersMap.productsSuppliersTableBody);
|
||||
|
||||
this.suppliers = [];
|
||||
this.prototypeTemplate = this.$productSuppliersCollection.data('prototype');
|
||||
this.prototypeName = this.$productSuppliersCollection.data('prototypeName');
|
||||
this.defaultDataForSupplier = this.getDefaultDataForSupplier();
|
||||
|
||||
this.init();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
init() {
|
||||
this.memorizeCurrentSuppliers();
|
||||
this.toggleTableVisibility();
|
||||
this.refreshDefaultSupplierBlock();
|
||||
|
||||
this.$initialDefault = this.$defaultSupplierGroup.find('input:checked').first();
|
||||
if (this.$initialDefault.length) {
|
||||
this.$initialDefault
|
||||
.closest(this.suppliersMap.checkboxContainer)
|
||||
.addClass(this.suppliersMap.defaultSupplierClass);
|
||||
}
|
||||
|
||||
this.$productsTable.on('change', 'input', () => {
|
||||
this.memorizeCurrentSuppliers();
|
||||
});
|
||||
|
||||
this.$supplierIdsGroup.on('change', 'input', (e) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
if (input.checked) {
|
||||
this.addSupplier({
|
||||
supplierId: input.value,
|
||||
supplierName: input.dataset.label,
|
||||
});
|
||||
} else {
|
||||
this.removeSupplier(input.value);
|
||||
}
|
||||
|
||||
this.renderSuppliers();
|
||||
this.toggleTableVisibility();
|
||||
this.refreshDefaultSupplierBlock();
|
||||
});
|
||||
}
|
||||
|
||||
toggleTableVisibility() {
|
||||
if (this.getSelectedSuppliers().length === 0) {
|
||||
this.hideTable();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.showTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} supplier
|
||||
*/
|
||||
addSupplier(supplier) {
|
||||
if (typeof this.suppliers[supplier.supplierId] === 'undefined') {
|
||||
const newSupplier = Object.create(this.defaultDataForSupplier);
|
||||
newSupplier.supplierId = supplier.supplierId;
|
||||
newSupplier.supplierName = supplier.supplierName;
|
||||
|
||||
this.suppliers[supplier.supplierId] = newSupplier;
|
||||
} else {
|
||||
this.suppliers[supplier.supplierId].removed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} supplierId
|
||||
*/
|
||||
removeSupplier(supplierId) {
|
||||
this.suppliers[supplierId].removed = true;
|
||||
}
|
||||
|
||||
renderSuppliers() {
|
||||
this.$productsTableBody.empty();
|
||||
|
||||
// Loop through select suppliers so that we use the same order as in the select list
|
||||
this.getSelectedSuppliers().forEach((selectedSupplier) => {
|
||||
const supplier = this.suppliers[selectedSupplier.supplierId];
|
||||
|
||||
if (supplier.removed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const productSupplierRow = this.prototypeTemplate.replace(
|
||||
new RegExp(this.prototypeName, 'g'),
|
||||
supplier.supplierId,
|
||||
);
|
||||
|
||||
this.$productsTableBody.append(productSupplierRow);
|
||||
// Fill inputs
|
||||
const rowMap = this.suppliersMap.productSupplierRow;
|
||||
$(rowMap.supplierIdInput(supplier.supplierId)).val(supplier.supplierId);
|
||||
$(rowMap.supplierNamePreview(supplier.supplierId)).html(supplier.supplierName);
|
||||
$(rowMap.supplierNameInput(supplier.supplierId)).val(supplier.supplierName);
|
||||
$(rowMap.productSupplierIdInput(supplier.supplierId)).val(supplier.productSupplierId);
|
||||
$(rowMap.referenceInput(supplier.supplierId)).val(supplier.reference);
|
||||
$(rowMap.priceInput(supplier.supplierId)).val(supplier.price);
|
||||
$(rowMap.currencyIdInput(supplier.supplierId)).val(supplier.currencyId);
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedSuppliers() {
|
||||
const selectedSuppliers = [];
|
||||
this.$supplierIdsGroup.find('input:checked').each((index, input) => {
|
||||
selectedSuppliers.push({
|
||||
supplierName: input.dataset.label,
|
||||
supplierId: input.value,
|
||||
});
|
||||
});
|
||||
|
||||
return selectedSuppliers;
|
||||
}
|
||||
|
||||
refreshDefaultSupplierBlock() {
|
||||
const suppliers = this.getSelectedSuppliers();
|
||||
|
||||
if (suppliers.length === 0) {
|
||||
if (this.forceUpdateDefault) {
|
||||
this.$defaultSupplierGroup.find('input').prop('checked', false);
|
||||
}
|
||||
this.hideDefaultSuppliers();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.showDefaultSuppliers();
|
||||
const selectedSupplierIds = suppliers.map((supplier) => supplier.supplierId);
|
||||
|
||||
this.$defaultSupplierGroup.find('input').each((key, input) => {
|
||||
const isValid = selectedSupplierIds.includes(input.value);
|
||||
|
||||
if (this.forceUpdateDefault && !isValid) {
|
||||
input.checked = false;
|
||||
}
|
||||
input.disabled = !isValid;
|
||||
});
|
||||
|
||||
if (this.$defaultSupplierGroup.find('input:checked').length === 0 && this.forceUpdateDefault) {
|
||||
this.checkFirstAvailableDefaultSupplier(selectedSupplierIds);
|
||||
}
|
||||
}
|
||||
|
||||
hideDefaultSuppliers() {
|
||||
this.$defaultSupplierGroup.addClass('d-none');
|
||||
}
|
||||
|
||||
showDefaultSuppliers() {
|
||||
this.$defaultSupplierGroup.removeClass('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int[]} selectedSupplierIds
|
||||
*/
|
||||
checkFirstAvailableDefaultSupplier(selectedSupplierIds) {
|
||||
const firstSupplierId = selectedSupplierIds[0];
|
||||
this.$defaultSupplierGroup.find(`input[value="${firstSupplierId}"]`).prop('checked', true);
|
||||
}
|
||||
|
||||
showTable() {
|
||||
this.$productsTable.removeClass('d-none');
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
this.$productsTable.addClass('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Memorize suppliers to be able to re-render them later.
|
||||
* Flag `removed` allows identifying whether supplier was removed from list or should be rendered
|
||||
*/
|
||||
memorizeCurrentSuppliers() {
|
||||
this.getSelectedSuppliers().forEach((supplier) => {
|
||||
this.suppliers[supplier.supplierId] = {
|
||||
supplierId: supplier.supplierId,
|
||||
productSupplierId: $(this.suppliersMap.productSupplierRow.productSupplierIdInput(supplier.supplierId)).val(),
|
||||
supplierName: $(this.suppliersMap.productSupplierRow.supplierNameInput(supplier.supplierId)).val(),
|
||||
reference: $(this.suppliersMap.productSupplierRow.referenceInput(supplier.supplierId)).val(),
|
||||
price: $(this.suppliersMap.productSupplierRow.priceInput(supplier.supplierId)).val(),
|
||||
currencyId: $(this.suppliersMap.productSupplierRow.currencyIdInput(supplier.supplierId)).val(),
|
||||
removed: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "shadow" prototype just to parse default values set inside the input fields,
|
||||
* this allow to build an object with default values set in the FormType
|
||||
*
|
||||
* @returns {{reference, removed: boolean, price, currencyId, productSupplierId}}
|
||||
*/
|
||||
getDefaultDataForSupplier() {
|
||||
const rowPrototype = new DOMParser().parseFromString(
|
||||
this.prototypeTemplate,
|
||||
'text/html',
|
||||
);
|
||||
|
||||
return {
|
||||
removed: false,
|
||||
productSupplierId: this.getDataFromRow(this.suppliersMap.productSupplierRow.productSupplierIdInput, rowPrototype),
|
||||
reference: this.getDataFromRow(this.suppliersMap.productSupplierRow.referenceInput, rowPrototype),
|
||||
price: this.getDataFromRow(this.suppliersMap.productSupplierRow.priceInput, rowPrototype),
|
||||
currencyId: this.getDataFromRow(this.suppliersMap.productSupplierRow.currencyIdInput, rowPrototype),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param selectorGenerator {function}
|
||||
* @param rowPrototype {Document}
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getDataFromRow(selectorGenerator, rowPrototype) {
|
||||
return rowPrototype.querySelector(selectorGenerator(this.prototypeName)).value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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';
|
||||
import ProductMap from '@pages/product/product-map';
|
||||
|
||||
/**
|
||||
* This component watches for modification of the product type, when it happens it displays a modal warning
|
||||
* for the user with a warning about what is going to be deleted if he validates this change. If modification
|
||||
* is confirmed the form is submitted right away to validate the change and update the page.
|
||||
*/
|
||||
export default class ProductTypeManager {
|
||||
/**
|
||||
* @param {jQuery} $typeSelector Select element to choose the product type
|
||||
* @param {jQuery} $productForm Product form that needs to be submitted
|
||||
*/
|
||||
constructor($typeSelector, $productForm) {
|
||||
this.$typeSelector = $typeSelector;
|
||||
this.$productForm = $productForm;
|
||||
this.productId = parseInt($productForm.data('productId'), 10);
|
||||
this.initialType = $typeSelector.val();
|
||||
|
||||
this.$typeSelector.on('change', (event) => this.confirmTypeSubmit(event));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
confirmTypeSubmit() {
|
||||
let confirmMessage = this.$typeSelector.data('confirm-message');
|
||||
let confirmWarning = '';
|
||||
|
||||
// If no productId we are in creation page so no need for extra warning
|
||||
if (this.productId) {
|
||||
switch (this.initialType) {
|
||||
case ProductMap.productType.COMBINATIONS:
|
||||
confirmWarning = this.$typeSelector.data('combinations-warning');
|
||||
break;
|
||||
case ProductMap.productType.PACK:
|
||||
confirmWarning = this.$typeSelector.data('pack-warning');
|
||||
break;
|
||||
case ProductMap.productType.VIRTUAL:
|
||||
confirmWarning = this.$typeSelector.data('virtual-warning');
|
||||
break;
|
||||
case ProductMap.productType.STANDARD:
|
||||
default:
|
||||
confirmWarning = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmWarning) {
|
||||
confirmWarning = `<div class="alert alert-warning">${confirmWarning}</div>`;
|
||||
}
|
||||
confirmMessage = `<div class="alert alert-info">${confirmMessage}</div>`;
|
||||
|
||||
const modal = new ConfirmModal(
|
||||
{
|
||||
id: 'modal-confirm-product-type',
|
||||
confirmTitle: this.$typeSelector.data('modal-title'),
|
||||
confirmMessage: `${confirmMessage} ${confirmWarning}`,
|
||||
confirmButtonLabel: this.$typeSelector.data('modal-apply'),
|
||||
closeButtonLabel: this.$typeSelector.data('modal-cancel'),
|
||||
closable: false,
|
||||
},
|
||||
() => {
|
||||
$(ProductMap.productFormSubmitButton).prop('disabled', true);
|
||||
this.$productForm.submit();
|
||||
},
|
||||
() => {
|
||||
this.$typeSelector.val(this.initialType);
|
||||
},
|
||||
);
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import EntitySearchInput from '@components/entity-search-input';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
/**
|
||||
* This component is used in product page to selected where the redirection points to when the
|
||||
* product is out of stock. It is composed on two inputs:
|
||||
* - a selection of the redirection type
|
||||
* - a rich component to select a product or a category
|
||||
*
|
||||
* When the type is changed the component automatically updates the labels, remote search urls
|
||||
* and values of the target.
|
||||
*/
|
||||
export default class RedirectOptionManager {
|
||||
constructor($redirectTypeInput, $redirectTargetInput) {
|
||||
this.$redirectTypeInput = $redirectTypeInput;
|
||||
this.$redirectTargetInput = $redirectTargetInput;
|
||||
this.$redirectTargetRow = this.$redirectTargetInput.closest('.form-group');
|
||||
this.$redirectTargetLabel = $('.form-control-label', this.$redirectTargetRow).first();
|
||||
this.$redirectTargetHint = $('.typeahead-hint', this.$redirectTargetRow);
|
||||
this.buildAutoCompleteSearchInput();
|
||||
this.watchRedirectType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the selected redirection type and adapt the inputs accordingly.
|
||||
*/
|
||||
watchRedirectType() {
|
||||
this.lastSelectedType = this.$redirectTypeInput.val();
|
||||
|
||||
this.$redirectTypeInput.change(() => {
|
||||
const redirectType = this.$redirectTypeInput.val();
|
||||
|
||||
switch (redirectType) {
|
||||
case '301-category':
|
||||
case '302-category':
|
||||
this.entitySearchInput.setRemoteUrl(this.$redirectTargetInput.data('categorySearchUrl'));
|
||||
this.$redirectTargetInput.prop('placeholder', this.$redirectTargetInput.data('categoryPlaceholder'));
|
||||
this.$redirectTargetLabel.html(this.$redirectTargetInput.data('categoryLabel'));
|
||||
// If previous type was not a category we reset the selected value
|
||||
if (this.lastSelectedType !== '301-category' && this.lastSelectedType !== '302-category') {
|
||||
this.entitySearchInput.setValue(null);
|
||||
}
|
||||
this.$redirectTargetHint.html(this.$redirectTargetInput.data('categoryHelp'));
|
||||
this.showTarget();
|
||||
break;
|
||||
case '301-product':
|
||||
case '302-product':
|
||||
this.entitySearchInput.setRemoteUrl(this.$redirectTargetInput.data('productSearchUrl'));
|
||||
this.$redirectTargetInput.prop('placeholder', this.$redirectTargetInput.data('productPlaceholder'));
|
||||
this.$redirectTargetLabel.html(this.$redirectTargetInput.data('productLabel'));
|
||||
// If previous type was not a category we reset the selected value
|
||||
if (this.lastSelectedType !== '301-product' && this.lastSelectedType !== '302-product') {
|
||||
this.entitySearchInput.setValue(null);
|
||||
}
|
||||
this.$redirectTargetHint.html(this.$redirectTargetInput.data('productHelp'));
|
||||
this.showTarget();
|
||||
break;
|
||||
case '404':
|
||||
default:
|
||||
this.entitySearchInput.setValue(null);
|
||||
this.hideTarget();
|
||||
break;
|
||||
}
|
||||
this.lastSelectedType = this.$redirectTypeInput.val();
|
||||
});
|
||||
}
|
||||
|
||||
buildAutoCompleteSearchInput() {
|
||||
this.entitySearchInput = new EntitySearchInput(this.$redirectTargetInput);
|
||||
}
|
||||
|
||||
showTarget() {
|
||||
this.$redirectTargetRow.removeClass('d-none');
|
||||
}
|
||||
|
||||
hideTarget() {
|
||||
this.$redirectTargetRow.addClass('d-none');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 ProductMap from '@pages/product/product-map';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class VirtualProductManager {
|
||||
constructor(productFormModel) {
|
||||
this.productFormModel = productFormModel;
|
||||
this.$virtualProductContainer = $(ProductMap.virtualProduct.container);
|
||||
this.$fileContentContainer = $(ProductMap.virtualProduct.fileContentContainer);
|
||||
|
||||
this.init();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
init() {
|
||||
this.productFormModel.watch('stock.hasVirtualProductFile', () => this.toggleContentVisibility());
|
||||
this.toggleContentVisibility();
|
||||
}
|
||||
|
||||
toggleContentVisibility() {
|
||||
const hasVirtualFile = Number(this.productFormModel.getProduct().stock.hasVirtualProductFile) === 1;
|
||||
const hasErrors = this.$virtualProductContainer
|
||||
.find(ProductMap.invalidField)
|
||||
.length !== 0;
|
||||
|
||||
if (hasVirtualFile || hasErrors) {
|
||||
this.showContent();
|
||||
} else {
|
||||
this.hideContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
hideContent() {
|
||||
this.$fileContentContainer.addClass('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
showContent() {
|
||||
this.$fileContentContainer.removeClass('d-none');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 {
|
||||
methods: {
|
||||
/**
|
||||
* The selected attribute is provided as a parameter instead od using this reference because it helps the
|
||||
* observer work better whe this.selectedAttributeGroups is explicitly used as an argument.
|
||||
*
|
||||
* @param {Object} attribute
|
||||
* @param {Object} attributeGroup
|
||||
* @param {Object} attributeGroups
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSelected(attribute, attributeGroup, attributeGroups) {
|
||||
if (!Object.prototype.hasOwnProperty.call(attributeGroups, attributeGroup.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return attributeGroups[attributeGroup.id].attributes.includes(attribute);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
export default {
|
||||
productModelUpdated: 'productModelUpdated',
|
||||
updatedProductModel: 'updatedProductModel',
|
||||
updatedProductField: 'updatedProductField',
|
||||
updateSubmitButtonState: 'updateSubmitButtonState',
|
||||
customizations: {
|
||||
rowRemoved: 'customizationRowRemoved',
|
||||
rowAdded: 'customizationRowAdded',
|
||||
},
|
||||
dropzone: {
|
||||
addedFile: 'addedfile',
|
||||
error: 'error',
|
||||
success: 'success',
|
||||
languageSelected: 'languageSelected',
|
||||
photoswipe: {
|
||||
destroy: 'destroy',
|
||||
closeGallery: 'closeGallery',
|
||||
},
|
||||
},
|
||||
combinations: {
|
||||
refreshPage: 'refreshPage',
|
||||
refreshCombinationList: 'refreshCombinationList',
|
||||
updateAttributeGroups: 'updateAttributeGroups',
|
||||
combinationGeneratorReady: 'combinationGeneratorReady',
|
||||
openCombinationsGenerator: 'openCombinationsGenerator',
|
||||
clearFilters: 'clearFilters',
|
||||
selectCombination: 'selectCombination',
|
||||
},
|
||||
};
|
||||
197
iadmin/themes/new-theme/js/pages/product/product-map.js
Normal file
197
iadmin/themes/new-theme/js/pages/product/product-map.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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 combinationListId = '#combination_list';
|
||||
|
||||
export default {
|
||||
productForm: 'form[name=product]',
|
||||
productTypeSelector: '#product_header_type',
|
||||
productType: {
|
||||
STANDARD: 'standard',
|
||||
PACK: 'pack',
|
||||
VIRTUAL: 'virtual',
|
||||
COMBINATIONS: 'combinations',
|
||||
},
|
||||
invalidField: '.is-invalid',
|
||||
productFormSubmitButton: '.product-form-save-button',
|
||||
navigationBar: '#form-nav',
|
||||
dropzoneImagesContainer: '.product-image-dropzone',
|
||||
featureValues: {
|
||||
collectionContainer: '.feature-values-collection',
|
||||
collectionRowsContainer: '.feature-values-collection > .col-sm',
|
||||
collectionRow: 'div.row.product-feature',
|
||||
featureSelect: 'select.feature-selector',
|
||||
featureValueSelect: 'select.feature-value-selector',
|
||||
customValueInput: '.custom-values input',
|
||||
customFeatureIdInput: 'input.custom-value-id',
|
||||
deleteFeatureValue: 'button.delete-feature-value',
|
||||
addFeatureValue: '.feature-value-add-button',
|
||||
},
|
||||
customizations: {
|
||||
customizationsContainer: '.product-customizations-collection',
|
||||
customizationFieldsList: '.product-customizations-collection ul',
|
||||
addCustomizationBtn: '.add-customization-btn',
|
||||
removeCustomizationBtn: '.remove-customization-btn',
|
||||
customizationFieldRow: '.customization-field-row',
|
||||
},
|
||||
combinations: {
|
||||
navigationTab: '#combinations-tab-nav',
|
||||
externalCombinationTab: '#external-combination-tab',
|
||||
preloader: '#combinations-preloader',
|
||||
emptyState: '#combinations-empty-state',
|
||||
combinationsPaginatedList: '#combinations-paginated-list',
|
||||
combinationsContainer: `${combinationListId}`,
|
||||
combinationsFiltersContainer: '#combinations_filters',
|
||||
combinationsGeneratorContainer: '#product_combinations_generator',
|
||||
combinationsTable: `${combinationListId} table`,
|
||||
combinationsTableBody: `${combinationListId} table tbody`,
|
||||
combinationIdInputsSelector: '.combination-id-input',
|
||||
isDefaultInputsSelector: '.combination-is-default-input',
|
||||
removeCombinationSelector: '.remove-combination-item',
|
||||
combinationName: 'form .card-header span',
|
||||
paginationContainer: '#combinations-pagination',
|
||||
loadingSpinner: '#productCombinationsLoading',
|
||||
quantityInputWrapper: '.combination-quantity',
|
||||
impactOnPriceInputWrapper: '.combination-impact-on-price',
|
||||
referenceInputWrapper: '.combination-reference',
|
||||
sortableColumns: '.ps-sortable-column',
|
||||
combinationItemForm: {
|
||||
quantityKey: 'combination_item[quantity][value]',
|
||||
impactOnPriceKey: 'combination_item[impact_on_price][value]',
|
||||
referenceKey: 'combination_item[reference][value]',
|
||||
tokenKey: 'combination_item[_token]',
|
||||
},
|
||||
editionForm: 'form[name="combination_form"]',
|
||||
editionFormInputs:
|
||||
// eslint-disable-next-line
|
||||
'form[name="combination_form"] input, form[name="combination_form"] textarea, form[name="combination_form"] select',
|
||||
editCombinationButtons: '.edit-combination-item',
|
||||
tableRow: {
|
||||
combinationImg: '.combination-image',
|
||||
combinationCheckbox: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_is_selected`,
|
||||
combinationIdInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_combination_id`,
|
||||
combinationNameInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_name`,
|
||||
referenceInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_reference_value`,
|
||||
impactOnPriceInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_impact_on_price_value`,
|
||||
finalPriceTeInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_final_price_te`,
|
||||
quantityInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_quantity_value`,
|
||||
isDefaultInput: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_is_default`,
|
||||
editButton: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_edit`,
|
||||
deleteButton: (rowIndex) => `${combinationListId}_combinations_${rowIndex}_delete`,
|
||||
},
|
||||
editModal: '#combination-edit-modal',
|
||||
images: {
|
||||
selectorContainer: '.combination-images-selector',
|
||||
imageChoice: '.combination-image-choice',
|
||||
checkboxContainer: '.form-check',
|
||||
checkbox: 'input[type=checkbox]',
|
||||
},
|
||||
scrollBar: '.attributes-list-overflow',
|
||||
searchInput: '#product-combinations-generate .attributes-search',
|
||||
generateCombinationsButton: '.generate-combinations-button',
|
||||
},
|
||||
virtualProduct: {
|
||||
container: '.virtual-product-file-container',
|
||||
fileContentContainer: '.virtual-product-file-content',
|
||||
},
|
||||
dropzone: {
|
||||
configuration: {
|
||||
fileManager: '.openfilemanager',
|
||||
},
|
||||
photoswipe: {
|
||||
element: '.pswp',
|
||||
},
|
||||
dzTemplate: '.dz-template',
|
||||
dzPreview: '.dz-preview',
|
||||
sortableContainer: '#product-images-dropzone',
|
||||
sortableItems: 'div.dz-preview:not(.disabled)',
|
||||
dropzoneContainer: '.dropzone-container',
|
||||
checkbox: '.md-checkbox input',
|
||||
shownTooltips: '.tooltip.show',
|
||||
savedImageContainer: (imageId) => `.dz-preview[data-id="${imageId}"]`,
|
||||
savedImage: (imageId) => `.dz-preview[data-id="${imageId}"] img`,
|
||||
coveredPreview: '.dz-preview.is-cover',
|
||||
windowFileManager: '.dropzone-window-filemanager',
|
||||
},
|
||||
suppliers: {
|
||||
productSuppliers: '#product_options_suppliers',
|
||||
combinationSuppliers: '#combination_form_suppliers',
|
||||
},
|
||||
seo: {
|
||||
container: '#product_seo_serp',
|
||||
defaultTitle: '.serp-default-title:input',
|
||||
watchedTitle: '.serp-watched-title:input',
|
||||
defaultDescription: '.serp-default-description',
|
||||
watchedDescription: '.serp-watched-description',
|
||||
watchedMetaUrl: '.serp-watched-url:input',
|
||||
redirectOption: {
|
||||
typeInput: '#product_seo_redirect_option_type',
|
||||
targetInput: '#product_seo_redirect_option_target',
|
||||
},
|
||||
},
|
||||
jsTabs: '.js-tabs',
|
||||
jsArrow: '.js-arrow',
|
||||
jsNavTabs: '.js-nav-tabs',
|
||||
toggleTab: '[data-toggle="tab"]',
|
||||
formContentTab: '#form_content > .form-contenttab',
|
||||
leftArrow: '.left-arrow',
|
||||
rightArrow: '.right-arrow',
|
||||
footer: {
|
||||
previewUrlButton: '.preview-url-button',
|
||||
deleteProductButton: '.delete-product-button',
|
||||
},
|
||||
categories: {
|
||||
categoriesContainer: '.js-categories-container',
|
||||
categoryTree: '.js-categories-tree',
|
||||
treeElement: '.category-tree-element',
|
||||
treeElementInputs: '.category-tree-inputs',
|
||||
checkboxInput: '[type=checkbox]',
|
||||
checkedCheckboxInputs: '[type=checkbox]:checked',
|
||||
checkboxName: (categoryId) => `product[categories][product_categories][${categoryId}][is_associated]`,
|
||||
materialCheckbox: '.md-checkbox',
|
||||
radioInput: '[type=radio]',
|
||||
defaultRadioInput: '[type=radio]:checked',
|
||||
radioName: (categoryId) => `product[categories][product_categories][${categoryId}][is_default]`,
|
||||
tagsContainer: '#categories-tags-container',
|
||||
searchInput: '#ps-select-product-category',
|
||||
fieldset: '.tree-fieldset',
|
||||
loader: '.categories-tree-loader',
|
||||
childrenList: '.children-list',
|
||||
everyItems: '.less, .more',
|
||||
expandAllButton: '#categories-tree-expand',
|
||||
reduceAllButton: '#categories-tree-reduce',
|
||||
},
|
||||
modules: {
|
||||
previewContainer: '.module-render-container.all-modules',
|
||||
previewButton: '.modules-list-button',
|
||||
selectorContainer: '.module-selection',
|
||||
moduleSelector: '.modules-list-select',
|
||||
selectorPreviews: '.module-selection .module-render-container',
|
||||
selectorPreview: (moduleId) => `.module-selection .module-render-container.${moduleId}`,
|
||||
contentContainer: '.module-contents',
|
||||
moduleContents: '.module-contents .module-render-container',
|
||||
moduleContent: (moduleId) => `.module-contents .module-render-container.${moduleId}`,
|
||||
},
|
||||
};
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
import Router from '@components/router';
|
||||
|
||||
const router = new Router();
|
||||
const {$} = window;
|
||||
|
||||
export const getProductAttributeGroups = async (productId) => $.get(router.generate('admin_products_attribute_groups', {
|
||||
productId,
|
||||
}));
|
||||
|
||||
export const getAllAttributeGroups = async () => $.get(router.generate('admin_all_attribute_groups'));
|
||||
|
||||
export default {
|
||||
getProductAttributeGroups,
|
||||
getAllAttributeGroups,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 '@components/router';
|
||||
|
||||
const router = new Router();
|
||||
const {$} = window;
|
||||
|
||||
export const getCategories = async () => $.get(router.generate('admin_categories_get_categories_tree'));
|
||||
|
||||
export default {
|
||||
getCategories,
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 '@components/router';
|
||||
|
||||
const {$} = window;
|
||||
|
||||
export default class CombinationsService {
|
||||
/**
|
||||
* @param {Number} productId
|
||||
*/
|
||||
constructor(productId) {
|
||||
this.productId = productId;
|
||||
this.router = new Router();
|
||||
this.filters = {};
|
||||
this.orderBy = null;
|
||||
this.orderWay = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} offset
|
||||
* @param {Number} limit
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
fetch(offset, limit) {
|
||||
const filterId = `product_combinations_${this.productId}`;
|
||||
const requestParams = {};
|
||||
// Required for route generation
|
||||
requestParams.productId = this.productId;
|
||||
|
||||
// These are the query parameters
|
||||
requestParams[filterId] = {};
|
||||
requestParams[filterId].offset = offset;
|
||||
requestParams[filterId].limit = limit;
|
||||
requestParams[filterId].filters = this.filters;
|
||||
if (this.orderBy !== null) {
|
||||
requestParams[filterId].orderBy = this.orderBy;
|
||||
}
|
||||
if (this.orderWay !== null) {
|
||||
requestParams[filterId].sortOrder = this.orderWay;
|
||||
}
|
||||
|
||||
return $.get(this.router.generate('admin_products_combinations', requestParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} combinationId
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removeCombination(combinationId) {
|
||||
return $.ajax({
|
||||
url: this.router.generate('admin_products_combinations_remove_combination', {
|
||||
combinationId,
|
||||
}),
|
||||
type: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} combinationId
|
||||
* @param {Object} data
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateListedCombination(combinationId, data) {
|
||||
return $.ajax({
|
||||
url: this.router.generate('admin_products_combinations_update_combination_from_listing', {
|
||||
combinationId,
|
||||
}),
|
||||
data,
|
||||
type: 'PATCH',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data Attributes indexed by attributeGroupId { 1: [23, 34], 3: [45, 52]}
|
||||
*/
|
||||
generateCombinations(data) {
|
||||
return $.ajax({
|
||||
url: this.router.generate('admin_products_combinations_generate', {
|
||||
productId: this.productId,
|
||||
}),
|
||||
data,
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getCombinationIds() {
|
||||
return $.get(
|
||||
this.router.generate('admin_products_combinations_ids', {
|
||||
productId: this.productId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} orderBy
|
||||
* @param {string} orderWay
|
||||
*/
|
||||
setOrderBy(orderBy, orderWay) {
|
||||
this.orderBy = orderBy;
|
||||
this.orderWay = orderWay.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object}
|
||||
*/
|
||||
getFilters() {
|
||||
return this.filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} filters
|
||||
*/
|
||||
setFilters(filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
}
|
||||
104
iadmin/themes/new-theme/js/pages/product/services/images.js
Normal file
104
iadmin/themes/new-theme/js/pages/product/services/images.js
Normal 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)
|
||||
*/
|
||||
|
||||
import Router from '@components/router';
|
||||
|
||||
const router = new Router();
|
||||
const {$} = window;
|
||||
|
||||
export const getProductImages = async (productId) => {
|
||||
const imagesUrl = router.generate('admin_products_v2_get_images', {
|
||||
productId,
|
||||
});
|
||||
|
||||
return $.get(imagesUrl);
|
||||
};
|
||||
|
||||
export const saveImageInformations = async (selectedFile, token, formName) => {
|
||||
const saveUrl = router.generate('admin_products_v2_update_image', {
|
||||
productImageId: selectedFile.image_id,
|
||||
});
|
||||
|
||||
const data = {};
|
||||
data[`${formName}[is_cover]`] = selectedFile.is_cover ? 1 : 0;
|
||||
Object.keys(selectedFile.legends).forEach((langId) => {
|
||||
data[`${formName}[legend][${langId}]`] = selectedFile.legends[langId];
|
||||
});
|
||||
data[`${formName}[_token]`] = token;
|
||||
|
||||
return $.ajax(saveUrl, {
|
||||
method: 'PATCH',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const replaceImage = async (selectedFile, newFile, formName, token) => {
|
||||
const replaceUrl = router.generate('admin_products_v2_update_image', {
|
||||
productImageId: selectedFile.image_id,
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append(`${formName}[file]`, newFile);
|
||||
formData.append(`${formName}[_token]`, token);
|
||||
formData.append('_method', 'PATCH');
|
||||
|
||||
return $.ajax(replaceUrl, {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const saveImagePosition = async (productImageId, newPosition, formName, token) => {
|
||||
const sortUrl = router.generate('admin_products_v2_update_image', {
|
||||
productImageId,
|
||||
});
|
||||
|
||||
const data = {};
|
||||
data[`${formName}[position]`] = newPosition;
|
||||
data[`${formName}[_token]`] = token;
|
||||
|
||||
return $.ajax(sortUrl, {
|
||||
method: 'PATCH',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeProductImage = async (productImageId) => {
|
||||
const deleteUrl = router.generate('admin_products_v2_delete_image', {
|
||||
productImageId,
|
||||
});
|
||||
|
||||
return $.post(deleteUrl);
|
||||
};
|
||||
|
||||
export default {
|
||||
getProductImages,
|
||||
saveImageInformations,
|
||||
replaceImage,
|
||||
saveImagePosition,
|
||||
removeProductImage,
|
||||
};
|
||||
48
iadmin/themes/new-theme/js/pages/product/suppliers-map.js
Normal file
48
iadmin/themes/new-theme/js/pages/product/suppliers-map.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 (suppliersFormId) => {
|
||||
const productSuppliersId = `${suppliersFormId}_product_suppliers`;
|
||||
const productSupplierInputId = (supplierIndex, inputName) => `${productSuppliersId}_${supplierIndex}_${inputName}`;
|
||||
|
||||
return {
|
||||
productSuppliersCollection: `${productSuppliersId}`,
|
||||
supplierIdsInput: `${suppliersFormId}_supplier_ids`,
|
||||
defaultSupplierInput: `${suppliersFormId}_default_supplier_id`,
|
||||
productSuppliersTable: `${productSuppliersId} table`,
|
||||
productsSuppliersTableBody: `${productSuppliersId} table tbody`,
|
||||
defaultSupplierClass: 'default-supplier',
|
||||
productSupplierRow: {
|
||||
supplierIdInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'supplier_id'),
|
||||
supplierNameInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'supplier_name'),
|
||||
productSupplierIdInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'product_supplier_id'),
|
||||
referenceInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'reference'),
|
||||
priceInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'price_tax_excluded'),
|
||||
currencyIdInput: (supplierIndex) => productSupplierInputId(supplierIndex, 'currency_id'),
|
||||
supplierNamePreview: (supplierIndex) => `#product_supplier_row_${supplierIndex} .supplier_name .preview`,
|
||||
},
|
||||
checkboxContainer: '.form-check',
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user