feat(main): Add base theme: This is the falcon theme out of the box.
This is falcon v3.1.2
This commit is contained in:
BIN
_dev/js/theme/.DS_Store
vendored
Normal file
BIN
_dev/js/theme/.DS_Store
vendored
Normal file
Binary file not shown.
21
_dev/js/theme/components/Lazyload.js
Normal file
21
_dev/js/theme/components/Lazyload.js
Normal file
@ -0,0 +1,21 @@
|
||||
import LazyLoad from 'vanilla-lazyload';
|
||||
|
||||
class PageLazyLoad {
|
||||
constructor({ selector = '.lazyload' } = {}) {
|
||||
this.selector = selector;
|
||||
this.lazyLoadInstance = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.lazyLoadInstance = new LazyLoad({
|
||||
elements_selector: this.selector,
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.lazyLoadInstance.update();
|
||||
}
|
||||
}
|
||||
|
||||
export default PageLazyLoad;
|
||||
17
_dev/js/theme/components/PageLoader.js
Normal file
17
_dev/js/theme/components/PageLoader.js
Normal file
@ -0,0 +1,17 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
class PageLoader {
|
||||
constructor() {
|
||||
this.$body = $('body');
|
||||
}
|
||||
|
||||
showLoader() {
|
||||
this.$body.addClass('page-loader-active');
|
||||
}
|
||||
|
||||
hideLoader() {
|
||||
this.$body.removeClass('page-loader-active');
|
||||
}
|
||||
}
|
||||
|
||||
export default PageLoader;
|
||||
30
_dev/js/theme/components/TopMenu.js
Normal file
30
_dev/js/theme/components/TopMenu.js
Normal file
@ -0,0 +1,30 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
export default class TopMenu {
|
||||
constructor(el) {
|
||||
this.$el = $(el);
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
self.$el.hoverIntent({
|
||||
over: self.toggleClassSubMenu,
|
||||
out: self.toggleClassSubMenu,
|
||||
selector: ' > li',
|
||||
timeout: 300,
|
||||
});
|
||||
}
|
||||
|
||||
toggleClassSubMenu() {
|
||||
const $item = $(this);
|
||||
let expanded = $item.attr('aria-expanded');
|
||||
|
||||
if (typeof expanded !== 'undefined') {
|
||||
expanded = expanded.toLowerCase() === 'true';
|
||||
$item.toggleClass('main-menu__item--active').attr('aria-expanded', !expanded);
|
||||
$('.main-menu__sub', $item)
|
||||
.attr('aria-expanded', !expanded)
|
||||
.attr('aria-hidden', expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
_dev/js/theme/components/cart/block-cart.js
Normal file
48
_dev/js/theme/components/cart/block-cart.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 Academic Free License 3.0 (AFL-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/AFL-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/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
|
||||
*/
|
||||
import prestashop from 'prestashop';
|
||||
import $ from 'jquery';
|
||||
|
||||
prestashop.blockcart = prestashop.blockcart || {};
|
||||
|
||||
prestashop.blockcart.showModal = (html) => {
|
||||
function getBlockCartModal() {
|
||||
return $('#blockcart-modal');
|
||||
}
|
||||
|
||||
const $blockCartModal = getBlockCartModal();
|
||||
|
||||
if ($blockCartModal.length) {
|
||||
$blockCartModal.hide();
|
||||
}
|
||||
|
||||
$('body').append(html);
|
||||
|
||||
getBlockCartModal()
|
||||
.modal('show')
|
||||
.on('hidden.bs.modal', (e) => {
|
||||
$(e.currentTarget).remove();
|
||||
});
|
||||
};
|
||||
366
_dev/js/theme/components/cart/cart.js
Normal file
366
_dev/js/theme/components/cart/cart.js
Normal file
@ -0,0 +1,366 @@
|
||||
import $ from 'jquery';
|
||||
import prestashop from 'prestashop';
|
||||
import debounce from '@js/theme/utils/debounce';
|
||||
|
||||
prestashop.cart = prestashop.cart || {};
|
||||
|
||||
prestashop.cart.active_inputs = null;
|
||||
|
||||
const spinnerSelector = 'input[name="product-quantity-spin"]';
|
||||
let hasError = false;
|
||||
let isUpdateOperation = false;
|
||||
let errorMsg = '';
|
||||
|
||||
const CheckUpdateQuantityOperations = {
|
||||
switchErrorStat: () => {
|
||||
/**
|
||||
* if errorMsg is not empty or if notifications are shown, we have error to display
|
||||
* if hasError is true, quantity was not updated : we don't disable checkout button
|
||||
*/
|
||||
const $checkoutBtn = $(prestashop.themeSelectors.checkout.btn);
|
||||
|
||||
if ($(prestashop.themeSelectors.notifications.dangerAlert).length || (errorMsg !== '' && !hasError)) {
|
||||
$checkoutBtn.addClass('disabled');
|
||||
}
|
||||
|
||||
if (errorMsg !== '') {
|
||||
const strError = `
|
||||
<article class="alert alert-danger" role="alert" data-alert="danger">
|
||||
<ul class="mb-0">
|
||||
<li>${errorMsg}</li>
|
||||
</ul>
|
||||
</article>
|
||||
`;
|
||||
$(prestashop.themeSelectors.notifications.container).html(strError);
|
||||
errorMsg = '';
|
||||
isUpdateOperation = false;
|
||||
if (hasError) {
|
||||
// if hasError is true, quantity was not updated : allow checkout
|
||||
$checkoutBtn.removeClass('disabled');
|
||||
}
|
||||
} else if (!hasError && isUpdateOperation) {
|
||||
hasError = false;
|
||||
isUpdateOperation = false;
|
||||
$(prestashop.themeSelectors.notifications.container).html('');
|
||||
$checkoutBtn.removeClass('disabled');
|
||||
}
|
||||
},
|
||||
checkUpdateOperation: (resp) => {
|
||||
/**
|
||||
* resp.hasError can be not defined but resp.errors not empty: quantity is updated but order cannot be placed
|
||||
* when resp.hasError=true, quantity is not updated
|
||||
*/
|
||||
const { hasError: hasErrorOccurred, errors: errorData } = resp;
|
||||
hasError = hasErrorOccurred ?? false;
|
||||
const errors = errorData ?? '';
|
||||
|
||||
// 1.7.2.x returns errors as string, 1.7.3.x returns array
|
||||
if (errors instanceof Array) {
|
||||
errorMsg = errors.join(' ');
|
||||
} else {
|
||||
errorMsg = errors;
|
||||
}
|
||||
|
||||
isUpdateOperation = true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach Bootstrap TouchSpin event handlers
|
||||
*/
|
||||
function createSpin() {
|
||||
$.each($(spinnerSelector), (index, spinner) => {
|
||||
$(spinner).TouchSpin({
|
||||
verticalupclass: 'material-icons touchspin-up',
|
||||
verticaldownclass: 'material-icons touchspin-down',
|
||||
buttondown_class: 'btn btn-touchspin js-touchspin js-increase-product-quantity',
|
||||
buttonup_class: 'btn btn-touchspin js-touchspin js-decrease-product-quantity',
|
||||
min: parseInt($(spinner).attr('min'), 10),
|
||||
max: 1000000,
|
||||
});
|
||||
});
|
||||
|
||||
CheckUpdateQuantityOperations.switchErrorStat();
|
||||
}
|
||||
|
||||
const preventCustomModalOpen = (event) => {
|
||||
if (window.shouldPreventModal) {
|
||||
event.preventDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$(() => {
|
||||
const productLineInCartSelector = prestashop.themeSelectors.cart.productLineQty;
|
||||
const promises = [];
|
||||
|
||||
prestashop.on('updateCart', () => {
|
||||
$(prestashop.themeSelectors.cart.quickview).modal('hide');
|
||||
$('body').addClass('cart-loading');
|
||||
});
|
||||
|
||||
prestashop.on('updatedCart', () => {
|
||||
window.shouldPreventModal = false;
|
||||
|
||||
$(prestashop.themeSelectors.product.customizationModal).on('show.bs.modal', (modalEvent) => {
|
||||
preventCustomModalOpen(modalEvent);
|
||||
});
|
||||
|
||||
createSpin();
|
||||
$('body').removeClass('cart-loading');
|
||||
});
|
||||
|
||||
createSpin();
|
||||
|
||||
const $body = $('body');
|
||||
|
||||
function isTouchSpin(namespace) {
|
||||
return namespace === 'on.startupspin' || namespace === 'on.startdownspin';
|
||||
}
|
||||
|
||||
function shouldIncreaseProductQuantity(namespace) {
|
||||
return namespace === 'on.startupspin';
|
||||
}
|
||||
|
||||
function findCartLineProductQuantityInput($target) {
|
||||
const $input = $target.parents(prestashop.themeSelectors.cart.touchspin).find(productLineInCartSelector);
|
||||
|
||||
if ($input.is(':focus')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
function camelize(subject) {
|
||||
const actionTypeParts = subject.split('-');
|
||||
let i;
|
||||
let part;
|
||||
let camelizedSubject = '';
|
||||
|
||||
for (i = 0; i < actionTypeParts.length; i += 1) {
|
||||
part = actionTypeParts[i];
|
||||
|
||||
if (i !== 0) {
|
||||
part = part.substring(0, 1).toUpperCase() + part.substring(1);
|
||||
}
|
||||
|
||||
camelizedSubject += part;
|
||||
}
|
||||
|
||||
return camelizedSubject;
|
||||
}
|
||||
|
||||
function parseCartAction($target, namespace) {
|
||||
if (!isTouchSpin(namespace)) {
|
||||
return {
|
||||
url: $target.attr('href'),
|
||||
type: camelize($target.data('link-action')),
|
||||
};
|
||||
}
|
||||
|
||||
const $input = findCartLineProductQuantityInput($target);
|
||||
|
||||
let cartAction = {};
|
||||
|
||||
if ($input) {
|
||||
if (shouldIncreaseProductQuantity(namespace)) {
|
||||
cartAction = {
|
||||
url: $input.data('up-url'),
|
||||
type: 'increaseProductQuantity',
|
||||
};
|
||||
} else {
|
||||
cartAction = {
|
||||
url: $input.data('down-url'),
|
||||
type: 'decreaseProductQuantity',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return cartAction;
|
||||
}
|
||||
|
||||
const abortPreviousRequests = () => {
|
||||
let promise;
|
||||
while (promises.length > 0) {
|
||||
promise = promises.pop();
|
||||
promise.abort();
|
||||
}
|
||||
};
|
||||
|
||||
const getTouchSpinInput = ($button) => $($button.parents(prestashop.themeSelectors.cart.touchspin).find('input'));
|
||||
|
||||
$(prestashop.themeSelectors.product.customizationModal).on('show.bs.modal', (modalEvent) => {
|
||||
preventCustomModalOpen(modalEvent);
|
||||
});
|
||||
|
||||
const handleCartAction = (event) => {
|
||||
event.preventDefault();
|
||||
window.shouldPreventModal = true;
|
||||
|
||||
const $target = $(event.currentTarget);
|
||||
const { dataset } = event.currentTarget;
|
||||
|
||||
const cartAction = parseCartAction($target, event.namespace);
|
||||
const requestData = {
|
||||
ajax: '1',
|
||||
action: 'update',
|
||||
};
|
||||
|
||||
if (typeof cartAction === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: cartAction.url,
|
||||
method: 'POST',
|
||||
data: requestData,
|
||||
dataType: 'json',
|
||||
beforeSend: (jqXHR) => {
|
||||
promises.push(jqXHR);
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
const $quantityInput = getTouchSpinInput($target);
|
||||
CheckUpdateQuantityOperations.checkUpdateOperation(resp);
|
||||
$quantityInput.val(resp.quantity);
|
||||
|
||||
// Refresh cart preview
|
||||
prestashop.emit('updateCart', {
|
||||
reason: dataset,
|
||||
resp,
|
||||
});
|
||||
})
|
||||
.fail((resp) => {
|
||||
prestashop.emit('handleError', {
|
||||
eventType: 'updateProductInCart',
|
||||
resp,
|
||||
cartAction: cartAction.type,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$body.on('click', prestashop.themeSelectors.cart.actions, handleCartAction);
|
||||
|
||||
function sendUpdateQuantityInCartRequest(updateQuantityInCartUrl, requestData, $target) {
|
||||
abortPreviousRequests();
|
||||
window.shouldPreventModal = true;
|
||||
|
||||
return $.ajax({
|
||||
url: updateQuantityInCartUrl,
|
||||
method: 'POST',
|
||||
data: requestData,
|
||||
dataType: 'json',
|
||||
beforeSend: (jqXHR) => {
|
||||
promises.push(jqXHR);
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
CheckUpdateQuantityOperations.checkUpdateOperation(resp);
|
||||
|
||||
$target.val(resp.quantity);
|
||||
const dataset = ($target && $target.dataset) ? $target.dataset : resp;
|
||||
|
||||
// Refresh cart preview
|
||||
prestashop.emit('updateCart', {
|
||||
reason: dataset,
|
||||
resp,
|
||||
});
|
||||
})
|
||||
.fail((resp) => {
|
||||
prestashop.emit('handleError', {
|
||||
eventType: 'updateProductQuantityInCart',
|
||||
resp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getQuantityChangeType($quantity) {
|
||||
return $quantity > 0 ? 'up' : 'down';
|
||||
}
|
||||
|
||||
function getRequestData(quantity) {
|
||||
return {
|
||||
ajax: '1',
|
||||
qty: Math.abs(quantity),
|
||||
action: 'update',
|
||||
op: getQuantityChangeType(quantity),
|
||||
};
|
||||
}
|
||||
|
||||
function updateProductQuantityInCart(event) {
|
||||
const $target = $(event.currentTarget);
|
||||
const updateQuantityInCartUrl = $target.data('update-url');
|
||||
const baseValue = $target.attr('value');
|
||||
|
||||
// There should be a valid product quantity in cart
|
||||
const targetValue = $target.val();
|
||||
|
||||
/* eslint-disable */
|
||||
if (targetValue != parseInt(targetValue, 10) || targetValue < 0 || Number.isNaN(targetValue)) {
|
||||
window.shouldPreventModal = false;
|
||||
$target.val(baseValue);
|
||||
return;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
// There should be a new product quantity in cart
|
||||
const qty = targetValue - baseValue;
|
||||
|
||||
if (qty === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetValue === '0') {
|
||||
$target.closest('.product-line-actions').find('[data-link-action="delete-from-cart"]').click();
|
||||
} else {
|
||||
$target.attr('value', targetValue);
|
||||
sendUpdateQuantityInCartRequest(updateQuantityInCartUrl, getRequestData(qty), $target);
|
||||
}
|
||||
}
|
||||
|
||||
$body.on('touchspin.on.stopspin', spinnerSelector, debounce(updateProductQuantityInCart));
|
||||
|
||||
$body.on(
|
||||
'focusout keyup',
|
||||
productLineInCartSelector,
|
||||
(event) => {
|
||||
if (event.type === 'keyup') {
|
||||
if (event.keyCode === 13) {
|
||||
isUpdateOperation = true;
|
||||
updateProductQuantityInCart(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isUpdateOperation) {
|
||||
updateProductQuantityInCart(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
$body.on(
|
||||
'click',
|
||||
prestashop.themeSelectors.cart.discountCode,
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const $code = $(event.currentTarget);
|
||||
const $discountInput = $('[name=discount_name]');
|
||||
const $discountForm = $discountInput.closest('form');
|
||||
|
||||
$discountInput.val($code.text());
|
||||
// Show promo code field
|
||||
$discountForm.trigger('submit');
|
||||
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
18
_dev/js/theme/components/customer.js
Normal file
18
_dev/js/theme/components/customer.js
Normal file
@ -0,0 +1,18 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
function initRmaItemSelector() {
|
||||
$(`${prestashop.themeSelectors.order.returnForm} table thead input[type=checkbox]`).on('click', ({ currentTarget }) => {
|
||||
const checked = $(currentTarget).prop('checked');
|
||||
$(`${prestashop.themeSelectors.order.returnForm} table tbody input[type=checkbox]`).each((_, checkbox) => {
|
||||
$(checkbox).prop('checked', checked);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupCustomerScripts() {
|
||||
if ($('body#order-detail')) {
|
||||
initRmaItemSelector();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(setupCustomerScripts);
|
||||
69
_dev/js/theme/components/dynamic-bootstrap-components.js
Normal file
69
_dev/js/theme/components/dynamic-bootstrap-components.js
Normal file
@ -0,0 +1,69 @@
|
||||
import $ from 'jquery';
|
||||
import DynamicImportHandler from '@js/theme/utils/DynamicImportHandler';
|
||||
|
||||
$(() => {
|
||||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "import" }] */
|
||||
|
||||
const importModal = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'modal',
|
||||
DOMEvents: 'click',
|
||||
DOMEventsSelector: '[data-toggle="modal"]',
|
||||
DOMEventsPreventDefault: true,
|
||||
files: () => [
|
||||
import('bootstrap/js/src/modal'),
|
||||
import('@css/dynamic/modal/_index.scss'),
|
||||
],
|
||||
});
|
||||
|
||||
const importDropdown = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'dropdown',
|
||||
DOMEvents: 'click',
|
||||
DOMEventsSelector: '[data-toggle="dropdown"]',
|
||||
DOMEventsPreventDefault: true,
|
||||
files: () => [
|
||||
import('bootstrap/js/src/dropdown'),
|
||||
import('@css/dynamic/dropdown/_index.scss'),
|
||||
],
|
||||
});
|
||||
|
||||
const importCollapse = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'collapse',
|
||||
DOMEvents: 'click',
|
||||
DOMEventsSelector: '[data-toggle="collapse"]',
|
||||
DOMEventsPreventDefault: true,
|
||||
files: () => [
|
||||
import('bootstrap/js/src/collapse'),
|
||||
],
|
||||
});
|
||||
|
||||
const importPopover = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'popover',
|
||||
files: () => [
|
||||
import('bootstrap/js/src/popover'),
|
||||
import('@css/dynamic/popover/_index.scss'),
|
||||
],
|
||||
});
|
||||
|
||||
const importScrollspy = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'scrollspy',
|
||||
files: () => [
|
||||
import('bootstrap/js/src/scrollspy'),
|
||||
],
|
||||
});
|
||||
|
||||
const importToast = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'toast',
|
||||
files: () => [
|
||||
import('bootstrap/js/src/toast'),
|
||||
import('@css/dynamic/toast/_index.scss'),
|
||||
],
|
||||
});
|
||||
|
||||
const importTooltip = new DynamicImportHandler({
|
||||
jqueryPluginCover: 'tooltip',
|
||||
files: () => [
|
||||
import('bootstrap/js/src/tooltip'),
|
||||
import('@css/dynamic/tooltip/_index.scss'),
|
||||
],
|
||||
});
|
||||
});
|
||||
109
_dev/js/theme/components/form.js
Normal file
109
_dev/js/theme/components/form.js
Normal file
@ -0,0 +1,109 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
const supportedValidity = () => {
|
||||
const input = document.createElement('input');
|
||||
|
||||
return (
|
||||
'validity' in input
|
||||
&& 'badInput' in input.validity
|
||||
&& 'patternMismatch' in input.validity
|
||||
&& 'rangeOverflow' in input.validity
|
||||
&& 'rangeUnderflow' in input.validity
|
||||
&& 'tooLong' in input.validity
|
||||
&& 'tooShort' in input.validity
|
||||
&& 'typeMismatch' in input.validity
|
||||
&& 'valid' in input.validity
|
||||
&& 'valueMissing' in input.validity
|
||||
);
|
||||
};
|
||||
|
||||
export default class Form {
|
||||
static init() {
|
||||
Form.parentFocus();
|
||||
Form.togglePasswordVisibility();
|
||||
Form.formValidation();
|
||||
}
|
||||
|
||||
static parentFocus() {
|
||||
$('.js-child-focus').on('focus', ({ target }) => {
|
||||
$(target).closest('.js-parent-focus').addClass('focus');
|
||||
});
|
||||
$('.js-child-focus').on('focusout', ({ target }) => {
|
||||
$(target).closest('.js-parent-focus').removeClass('focus');
|
||||
});
|
||||
}
|
||||
|
||||
static togglePasswordVisibility() {
|
||||
$('[data-action="show-password"]').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const $btn = $(e.currentTarget);
|
||||
const $input = $btn
|
||||
.closest('.input-group')
|
||||
.children('input.js-visible-password');
|
||||
|
||||
if ($input.attr('type') === 'password') {
|
||||
$input.attr('type', 'text');
|
||||
$btn.html($btn.data('text-hide'));
|
||||
} else {
|
||||
$input.attr('type', 'password');
|
||||
$btn.html($btn.data('textShow'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static formValidation() {
|
||||
// Fetch all the forms we want to apply custom Bootstrap validation styles to
|
||||
const forms = document.getElementsByClassName('needs-validation');
|
||||
|
||||
if (forms.length > 0) {
|
||||
if (!supportedValidity()) {
|
||||
return;
|
||||
}
|
||||
// Loop over them and prevent submission
|
||||
let divToScroll = false;
|
||||
|
||||
$('input, textarea', forms).on('blur', (e) => {
|
||||
const $field = $(e.currentTarget);
|
||||
$field.val($field.val().trim());
|
||||
});
|
||||
|
||||
Array.prototype.filter.call(forms, (form) => {
|
||||
form.addEventListener(
|
||||
'submit',
|
||||
(event) => {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$('input:invalid,select:invalid,textarea:invalid', form).each((index, field) => {
|
||||
const $field = $(field);
|
||||
const $parent = $field.closest('.form-group');
|
||||
|
||||
$('.js-invalid-feedback-browser', $parent).text(
|
||||
$field[0].validationMessage,
|
||||
);
|
||||
if (!divToScroll) {
|
||||
divToScroll = $parent;
|
||||
}
|
||||
});
|
||||
|
||||
const $form = $(form);
|
||||
$form.data('disabled', false);
|
||||
$form.find('[type="submit"]').removeClass('disabled');
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
if (divToScroll) {
|
||||
$('html, body').animate(
|
||||
{ scrollTop: divToScroll.offset().top },
|
||||
300,
|
||||
);
|
||||
divToScroll = false;
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
90
_dev/js/theme/components/product.js
Normal file
90
_dev/js/theme/components/product.js
Normal file
@ -0,0 +1,90 @@
|
||||
import $ from 'jquery';
|
||||
import prestashop from 'prestashop';
|
||||
|
||||
$(() => {
|
||||
const createInputFile = () => {
|
||||
$('.js-file-input').on('change', (event) => {
|
||||
const target = $(event.currentTarget)[0];
|
||||
const file = (target) ? target.files[0] : null;
|
||||
|
||||
if (target && file) {
|
||||
$(target).prev().text(file.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createProductSpin = () => {
|
||||
const $quantityInput = $('#quantity_wanted');
|
||||
|
||||
$quantityInput.TouchSpin({
|
||||
verticalupclass: 'material-icons touchspin-up',
|
||||
verticaldownclass: 'material-icons touchspin-down',
|
||||
buttondown_class: 'btn btn-touchspin js-touchspin',
|
||||
buttonup_class: 'btn btn-touchspin js-touchspin',
|
||||
min: parseInt($quantityInput.attr('min'), 10),
|
||||
max: 1000000,
|
||||
});
|
||||
|
||||
$quantityInput.on('focusout', () => {
|
||||
if ($quantityInput.val() === '' || $quantityInput.val() < $quantityInput.attr('min')) {
|
||||
$quantityInput.val($quantityInput.attr('min'));
|
||||
$quantityInput.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
$('body').on('change keyup', '#quantity_wanted', (event) => {
|
||||
$(event.currentTarget).trigger('touchspin.stopspin');
|
||||
prestashop.emit('updateProduct', {
|
||||
eventType: 'updatedProductQuantity',
|
||||
event,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
createProductSpin();
|
||||
createInputFile();
|
||||
let updateEvenType = false;
|
||||
|
||||
prestashop.on('updateProduct', ({ eventType }) => {
|
||||
updateEvenType = eventType;
|
||||
});
|
||||
|
||||
prestashop.on('updateCart', (event) => {
|
||||
if (
|
||||
prestashop.page.page_name === 'product'
|
||||
&& parseInt(event.reason.idProduct, 10) === parseInt($('#add-to-cart-or-refresh').find('[name="id_product"]').val(), 10)) {
|
||||
prestashop.emit('updateProduct', {
|
||||
event,
|
||||
resp: {},
|
||||
reason: {
|
||||
productUrl: prestashop.urls.pages.product || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
prestashop.on('updatedProduct', (event) => {
|
||||
createInputFile();
|
||||
|
||||
if (event && event.product_minimal_quantity) {
|
||||
const minimalProductQuantity = parseInt(event.product_minimal_quantity, 10);
|
||||
const quantityInputSelector = '#quantity_wanted';
|
||||
const quantityInput = $(quantityInputSelector);
|
||||
|
||||
// @see http://www.virtuosoft.eu/code/bootstrap-touchspin/ about Bootstrap TouchSpin
|
||||
quantityInput.trigger('touchspin.updatesettings', {
|
||||
min: minimalProductQuantity,
|
||||
});
|
||||
}
|
||||
|
||||
if (updateEvenType === 'updatedProductCombination') {
|
||||
$('.js-product-images').replaceWith(event.product_cover_thumbnails);
|
||||
$('.js-product-images-modal').replaceWith(event.product_images_modal);
|
||||
prestashop.emit('updatedProductCombination', event);
|
||||
}
|
||||
|
||||
updateEvenType = false;
|
||||
|
||||
prestashop.pageLazyLoad.update();
|
||||
});
|
||||
});
|
||||
49
_dev/js/theme/components/quickview.js
Normal file
49
_dev/js/theme/components/quickview.js
Normal file
@ -0,0 +1,49 @@
|
||||
import $ from 'jquery';
|
||||
import prestashop from 'prestashop';
|
||||
|
||||
$(() => {
|
||||
const productConfig = (qv) => {
|
||||
$('.js-thumb').on('click', (event) => {
|
||||
if ($('.js-thumb').hasClass('selected')) {
|
||||
$('.js-thumb').removeClass('selected');
|
||||
}
|
||||
$(event.currentTarget).addClass('selected');
|
||||
$('.js-qv-product-cover').attr('src', $(event.target).data('image-large-src'));
|
||||
});
|
||||
|
||||
qv.find('#quantity_wanted').TouchSpin({
|
||||
verticalupclass: 'material-icons touchspin-up',
|
||||
verticaldownclass: 'material-icons touchspin-down',
|
||||
buttondown_class: 'btn btn-touchspin js-touchspin',
|
||||
buttonup_class: 'btn btn-touchspin js-touchspin',
|
||||
min: 1,
|
||||
max: 1000000,
|
||||
});
|
||||
};
|
||||
|
||||
prestashop.on('clickQuickView', (elm) => {
|
||||
const data = {
|
||||
action: 'quickview',
|
||||
id_product: elm.dataset.idProduct,
|
||||
id_product_attribute: elm.dataset.idProductAttribute,
|
||||
};
|
||||
$.post(prestashop.urls.pages.product, data, null, 'json')
|
||||
.then((resp) => {
|
||||
$('body').append(resp.quickview_html);
|
||||
const productModal = $(
|
||||
`#quickview-modal-${resp.product.id}-${resp.product.id_product_attribute}`,
|
||||
);
|
||||
productModal.modal('show');
|
||||
productConfig(productModal);
|
||||
productModal.on('hidden.bs.modal', () => {
|
||||
productModal.remove();
|
||||
});
|
||||
})
|
||||
.fail((resp) => {
|
||||
prestashop.emit('handleError', {
|
||||
eventType: 'clickQuickView',
|
||||
resp,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
63
_dev/js/theme/components/responsive.js
Normal file
63
_dev/js/theme/components/responsive.js
Normal file
@ -0,0 +1,63 @@
|
||||
import $ from 'jquery';
|
||||
import prestashop from 'prestashop';
|
||||
|
||||
const isMobile = () => prestashop.responsive.current_width < prestashop.responsive.min_width;
|
||||
|
||||
prestashop.responsive = prestashop.responsive || {};
|
||||
|
||||
prestashop.responsive.current_width = window.innerWidth;
|
||||
prestashop.responsive.min_width = 768;
|
||||
prestashop.responsive.mobile = isMobile();
|
||||
|
||||
function swapChildren(obj1, obj2) {
|
||||
const temp = obj2.children().detach();
|
||||
obj2.empty().append(obj1.children().detach());
|
||||
obj1.append(temp);
|
||||
}
|
||||
|
||||
function toggleMobileStyles() {
|
||||
if (prestashop.responsive.mobile) {
|
||||
$("*[id^='_desktop_']").each((idx, el) => {
|
||||
const target = $(`#${el.id.replace('_desktop_', '_mobile_')}`);
|
||||
|
||||
if (target.length) {
|
||||
swapChildren($(el), target);
|
||||
}
|
||||
});
|
||||
|
||||
$('[data-collapse-hide-mobile]').collapse('hide');
|
||||
} else {
|
||||
$("*[id^='_mobile_']").each((idx, el) => {
|
||||
const target = $(`#${el.id.replace('_mobile_', '_desktop_')}`);
|
||||
|
||||
if (target.length) {
|
||||
swapChildren($(el), target);
|
||||
}
|
||||
});
|
||||
|
||||
$('[data-collapse-hide-mobile]').not('.show').collapse('show');
|
||||
$('[data-modal-hide-mobile].show').modal('hide');
|
||||
}
|
||||
prestashop.emit('responsive update', {
|
||||
mobile: prestashop.responsive.mobile,
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('resize', () => {
|
||||
const { responsive } = prestashop;
|
||||
const cw = responsive.current_width;
|
||||
const mw = responsive.min_width;
|
||||
const w = window.innerWidth;
|
||||
const toggle = (cw >= mw && w < mw) || (cw < mw && w >= mw);
|
||||
responsive.current_width = w;
|
||||
responsive.mobile = responsive.current_width < responsive.min_width;
|
||||
if (toggle) {
|
||||
toggleMobileStyles();
|
||||
}
|
||||
});
|
||||
|
||||
$(() => {
|
||||
if (prestashop.responsive.mobile) {
|
||||
toggleMobileStyles();
|
||||
}
|
||||
});
|
||||
82
_dev/js/theme/components/selectors.js
Normal file
82
_dev/js/theme/components/selectors.js
Normal file
@ -0,0 +1,82 @@
|
||||
import prestashop from 'prestashop';
|
||||
import $ from 'jquery';
|
||||
|
||||
prestashop.themeSelectors = {
|
||||
product: {
|
||||
tabs: '.tabs .nav-link',
|
||||
activeNavClass: 'js-product-nav-active',
|
||||
activeTabClass: 'js-product-tab-active',
|
||||
activeTabs: '.tabs .nav-link.active, .js-product-nav-active',
|
||||
imagesModal: '.js-product-images-modal',
|
||||
thumb: '.js-thumb',
|
||||
thumbContainer: '.thumb-container, .js-thumb-container',
|
||||
arrows: '.js-arrows',
|
||||
selected: '.selected, .js-thumb-selected',
|
||||
modalProductCover: '.js-modal-product-cover',
|
||||
cover: '.js-qv-product-cover',
|
||||
customizationModal: '.js-customization-modal',
|
||||
},
|
||||
listing: {
|
||||
searchFilterToggler: '#search_filter_toggler, .js-search-toggler',
|
||||
searchFiltersWrapper: '#search_filters_wrapper',
|
||||
searchFilterControls: '#search_filter_controls',
|
||||
searchFilters: '#search_filters',
|
||||
activeSearchFilters: '#js-active-search-filters',
|
||||
listTop: '#js-product-list-top',
|
||||
list: '#js-product-list',
|
||||
listBottom: '#js-product-list-bottom',
|
||||
listHeader: '#js-product-list-header',
|
||||
searchFiltersClearAll: '.js-search-filters-clear-all',
|
||||
searchLink: '.js-search-link',
|
||||
},
|
||||
order: {
|
||||
returnForm: '#order-return-form, .js-order-return-form',
|
||||
},
|
||||
arrowDown: '.arrow-down, .js-arrow-down',
|
||||
arrowUp: '.arrow-up, .js-arrow-up',
|
||||
clear: '.clear',
|
||||
fileInput: '.js-file-input',
|
||||
contentWrapper: '#content-wrapper, .js-content-wrapper',
|
||||
footer: '#footer, .js-footer',
|
||||
modalContent: '.js-modal-content',
|
||||
modal: '.js-checkout-modal',
|
||||
touchspin: '.js-touchspin',
|
||||
checkout: {
|
||||
termsLink: '.js-terms a',
|
||||
giftCheckbox: '.js-gift-checkbox',
|
||||
imagesLink: '.card-block .cart-summary-products p a, .js-show-details',
|
||||
carrierExtraContent: '.carrier-extra-content, .js-carrier-extra-content',
|
||||
btn: '.checkout a',
|
||||
},
|
||||
cart: {
|
||||
productLineQty: '.js-cart-line-product-quantity',
|
||||
quickview: '.quickview',
|
||||
touchspin: '.bootstrap-touchspin',
|
||||
promoCode: '#promo-code',
|
||||
displayPromo: '.display-promo',
|
||||
promoCodeButton: '.promo-code-button',
|
||||
discountCode: '.js-discount .code',
|
||||
discountName: '[name=discount_name]',
|
||||
actions: '[data-link-action="delete-from-cart"], [data-link-action="remove-voucher"]',
|
||||
},
|
||||
notifications: {
|
||||
dangerAlert: '#notifications article.alert-danger',
|
||||
container: '#notifications .container',
|
||||
},
|
||||
passwordPolicy: {
|
||||
template: '#password-feedback',
|
||||
hint: '.js-hint-password',
|
||||
container: '.js-password-strength-feedback',
|
||||
strengthText: '.js-password-strength-text',
|
||||
requirementScore: '.js-password-requirements-score',
|
||||
requirementLength: '.js-password-requirements-length',
|
||||
requirementScoreIcon: '.js-password-requirements-score i',
|
||||
requirementLengthIcon: '.js-password-requirements-length i',
|
||||
progressBar: '.js-password-policy-progress-bar',
|
||||
inputColumn: '.js-input-column',
|
||||
},
|
||||
};
|
||||
|
||||
$(() => {
|
||||
prestashop.emit('themeSelectorsInit');
|
||||
});
|
||||
11
_dev/js/theme/components/sliders.js
Normal file
11
_dev/js/theme/components/sliders.js
Normal file
@ -0,0 +1,11 @@
|
||||
import prestashop from 'prestashop';
|
||||
import $ from 'jquery';
|
||||
import PageSlider from '@js/theme/components/sliders/PageSlider';
|
||||
import SwiperSlider from '@js/theme/components/sliders/SwiperSlider';
|
||||
|
||||
prestashop.pageSlider = new PageSlider();
|
||||
prestashop.SwiperSlider = SwiperSlider;
|
||||
|
||||
$(() => {
|
||||
prestashop.pageSlider.init();
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
class DynamicImportSwiperModule {
|
||||
constructor(getFiles) {
|
||||
this.getFiles = getFiles;
|
||||
}
|
||||
|
||||
getModule() {
|
||||
return Promise.all(this.getFiles());
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicImportSwiperModule;
|
||||
83
_dev/js/theme/components/sliders/PageSlider.js
Normal file
83
_dev/js/theme/components/sliders/PageSlider.js
Normal file
@ -0,0 +1,83 @@
|
||||
import SwiperSlider from '@js/theme/components/sliders/SwiperSlider';
|
||||
|
||||
class PageSlider {
|
||||
constructor() {
|
||||
this.observeElementClass = 'js-slider-observed';
|
||||
this.selfInitializedSlidersSelector = '.swiper:not(.swiper-custom)';
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(({ intersectionRatio, target }) => {
|
||||
if (intersectionRatio > 0) {
|
||||
self.observer.unobserve(target);
|
||||
PageSlider.initSlider(target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.observerElements();
|
||||
}
|
||||
|
||||
static initSlider(target) {
|
||||
const swiper = new SwiperSlider(target, PageSlider.getConfigForSliderElement(target));
|
||||
swiper.initSlider();
|
||||
}
|
||||
|
||||
static getConfigForSliderElement(target) {
|
||||
let elConfig = target.dataset.swiper || {};
|
||||
|
||||
if (typeof elConfig === 'string') {
|
||||
elConfig = JSON.parse(elConfig);
|
||||
}
|
||||
|
||||
const parent = target.parentElement;
|
||||
const nextEl = parent.querySelector('.swiper-button-next');
|
||||
const prevEl = parent.querySelector('.swiper-button-prev');
|
||||
const pagination = parent.querySelector('.swiper-pagination');
|
||||
|
||||
if (nextEl && prevEl && typeof elConfig.navigation === 'undefined') {
|
||||
elConfig = {
|
||||
...elConfig,
|
||||
navigation: {
|
||||
nextEl,
|
||||
prevEl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (pagination && typeof elConfig.pagination === 'undefined') {
|
||||
elConfig = {
|
||||
...elConfig,
|
||||
pagination: {
|
||||
el: pagination,
|
||||
type: 'bullets',
|
||||
clickable: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return elConfig;
|
||||
}
|
||||
|
||||
observerElements() {
|
||||
const elms = document.querySelectorAll(this.selfInitializedSlidersSelector);
|
||||
|
||||
for (let i = 0; i < elms.length; i += 1) {
|
||||
const elem = elms[i];
|
||||
|
||||
if (!elem.classList.contains(this.observeElementClass)) {
|
||||
this.observer.observe(elem);
|
||||
elem.classList.add(this.observeElementClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.observerElements();
|
||||
}
|
||||
}
|
||||
|
||||
export default PageSlider;
|
||||
124
_dev/js/theme/components/sliders/SwiperSlider.js
Normal file
124
_dev/js/theme/components/sliders/SwiperSlider.js
Normal file
@ -0,0 +1,124 @@
|
||||
import Swiper, {
|
||||
Navigation, Pagination, Autoplay,
|
||||
} from 'swiper';
|
||||
|
||||
import DynamicImportSwiperModule from '@js/theme/components/sliders/DynamicImportSwiperModule';
|
||||
|
||||
/* eslint-disable */
|
||||
const dynamicModulesMap = {
|
||||
thumbs: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/thumbs/thumbs.js'),
|
||||
],
|
||||
),
|
||||
virtual: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/virtual/virtual.js'),
|
||||
import('@node_modules/swiper/modules/virtual/virtual.scss'),
|
||||
],
|
||||
),
|
||||
keyboard: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/keyboard/keyboard.js'),
|
||||
],
|
||||
),
|
||||
mousewheel: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/mousewheel/mousewheel.js'),
|
||||
],
|
||||
),
|
||||
scrollbar: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/scrollbar/scrollbar.js'),
|
||||
import('@node_modules/swiper/modules/scrollbar/scrollbar.scss'),
|
||||
],
|
||||
),
|
||||
parallax: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/parallax/parallax.js'),
|
||||
],
|
||||
),
|
||||
zoom: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/zoom/zoom.js'),
|
||||
import('@node_modules/swiper/modules/zoom/zoom.scss'),
|
||||
],
|
||||
),
|
||||
freeMode: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/free-mode/free-mode.js'),
|
||||
import('@node_modules/swiper/modules/free-mode/free-mode.scss'),
|
||||
],
|
||||
),
|
||||
controller: new DynamicImportSwiperModule(
|
||||
() => [
|
||||
import('@node_modules/swiper/modules/controller/controller.js'),
|
||||
import('@node_modules/swiper/modules/controller/controller.scss'),
|
||||
],
|
||||
),
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
const defaultModules = [Navigation, Pagination, Autoplay];
|
||||
|
||||
class SwiperSlider {
|
||||
constructor(target, options) {
|
||||
this.target = target;
|
||||
this.options = options;
|
||||
this.modules = defaultModules;
|
||||
this._modulesToFetch = [];
|
||||
this.SwiperInstance = null;
|
||||
}
|
||||
|
||||
async initSlider() {
|
||||
this.findNeededModulesToFetch();
|
||||
await this.fetchNeededModules();
|
||||
await this.initSwiper();
|
||||
|
||||
return this.SwiperInstance;
|
||||
}
|
||||
|
||||
initSwiper() {
|
||||
this.SwiperInstance = new Swiper(this.target, {
|
||||
...this.options,
|
||||
modules: this.modules,
|
||||
});
|
||||
}
|
||||
|
||||
async fetchNeededModules() {
|
||||
if (this._modulesToFetch.length > 0) {
|
||||
const modulesPromisesArray = [];
|
||||
|
||||
for (const module of this._modulesToFetch) {
|
||||
modulesPromisesArray.push(module.getFiles());
|
||||
}
|
||||
|
||||
const allPromises = Promise.all(
|
||||
modulesPromisesArray.map((innerModulesPromisesArray) => Promise.all(innerModulesPromisesArray)),
|
||||
);
|
||||
|
||||
return allPromises.then((arrayOfModules) => {
|
||||
for (const moduleImported of arrayOfModules) {
|
||||
for (const module of moduleImported) {
|
||||
if (typeof module.default !== 'undefined') {
|
||||
this.modules = [...this.modules, module.default];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
findNeededModulesToFetch() {
|
||||
for (const dynamicModuleProp in dynamicModulesMap) {
|
||||
if (Object.prototype.hasOwnProperty.call(dynamicModulesMap, dynamicModuleProp)
|
||||
&& typeof this.options[dynamicModuleProp] !== 'undefined') {
|
||||
this._modulesToFetch.push(dynamicModulesMap[dynamicModuleProp]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SwiperSlider;
|
||||
96
_dev/js/theme/components/useAlertToast.js
Normal file
96
_dev/js/theme/components/useAlertToast.js
Normal file
@ -0,0 +1,96 @@
|
||||
import parseToHtml from '@js/theme/utils/parseToHtml';
|
||||
|
||||
let id = 0;
|
||||
|
||||
const getId = (prefix = 'alert_toast_') => {
|
||||
id += 1;
|
||||
return prefix + id;
|
||||
};
|
||||
|
||||
const useAlertToast = (params) => {
|
||||
const {
|
||||
duration = 4000,
|
||||
} = params || {};
|
||||
const stackTemplateId = 'alert-toast-stack';
|
||||
const bodyElement = document.querySelector('body');
|
||||
|
||||
const buildToastTemplate = (text, type, toastId) => parseToHtml(`
|
||||
<div class="alert-toast alert-toast--${type} d-none" id=${toastId}>
|
||||
<div class="alert-toast__content">
|
||||
${text}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const buildToastStackTemplate = () => parseToHtml(`
|
||||
<div id="${stackTemplateId}" class="alert-toast-stack">
|
||||
</div>
|
||||
`);
|
||||
|
||||
const getToastStackTemplate = () => {
|
||||
const getElement = () => document.querySelector(`#${stackTemplateId}`);
|
||||
|
||||
if (!getElement()) {
|
||||
bodyElement.append(buildToastStackTemplate());
|
||||
}
|
||||
|
||||
return getElement();
|
||||
};
|
||||
|
||||
const hideToast = (toast) => {
|
||||
toast.classList.remove('show');
|
||||
|
||||
const hideDuration = (parseFloat(window.getComputedStyle(toast).transitionDuration)) * 1000;
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, hideDuration);
|
||||
};
|
||||
|
||||
const showToast = (text, type, timeOut = false) => {
|
||||
const toastId = getId();
|
||||
const toast = buildToastTemplate(text, type, toastId);
|
||||
const toastStack = getToastStackTemplate();
|
||||
timeOut = timeOut || duration;
|
||||
|
||||
toastStack.prepend(toast);
|
||||
|
||||
const toastInDOM = document.querySelector(`#${toastId}`);
|
||||
|
||||
toastInDOM.classList.remove('d-none');
|
||||
|
||||
setTimeout(() => {
|
||||
toastInDOM.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
toastInDOM.dataset.timeoutId = setTimeout(() => {
|
||||
hideToast(toastInDOM);
|
||||
}, timeOut);
|
||||
};
|
||||
|
||||
const info = (text, timeOut = false) => {
|
||||
showToast(text, 'info', timeOut);
|
||||
};
|
||||
|
||||
const success = (text, timeOut = false) => {
|
||||
showToast(text, 'success', timeOut);
|
||||
};
|
||||
|
||||
const danger = (text, timeOut = false) => {
|
||||
showToast(text, 'danger', timeOut);
|
||||
};
|
||||
|
||||
const warning = (text, timeOut = false) => {
|
||||
showToast(text, 'warning', timeOut);
|
||||
};
|
||||
|
||||
return {
|
||||
info,
|
||||
success,
|
||||
danger,
|
||||
warning,
|
||||
showToast,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAlertToast;
|
||||
173
_dev/js/theme/components/usePasswordPolicy.js
Normal file
173
_dev/js/theme/components/usePasswordPolicy.js
Normal file
@ -0,0 +1,173 @@
|
||||
import { sprintf } from 'sprintf-js';
|
||||
|
||||
const { passwordPolicy: PasswordPolicyMap } = prestashop.themeSelectors;
|
||||
|
||||
const PASSWORD_POLICY_ERROR = 'The password policy elements are undefined.';
|
||||
|
||||
const getPasswordStrengthFeedback = (
|
||||
strength,
|
||||
) => {
|
||||
switch (strength) {
|
||||
case 0:
|
||||
return {
|
||||
color: 'bg-danger',
|
||||
};
|
||||
|
||||
case 1:
|
||||
return {
|
||||
color: 'bg-danger',
|
||||
};
|
||||
|
||||
case 2:
|
||||
return {
|
||||
color: 'bg-warning',
|
||||
};
|
||||
|
||||
case 3:
|
||||
return {
|
||||
color: 'bg-success',
|
||||
};
|
||||
|
||||
case 4:
|
||||
return {
|
||||
color: 'bg-success',
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error('Invalid password strength indicator.');
|
||||
}
|
||||
};
|
||||
|
||||
const watchPassword = async (
|
||||
elementInput,
|
||||
feedbackContainer,
|
||||
hints,
|
||||
) => {
|
||||
const { prestashop } = window;
|
||||
const passwordValue = elementInput.value;
|
||||
const elementIcon = feedbackContainer.querySelector(PasswordPolicyMap.requirementScoreIcon);
|
||||
const result = await prestashop.checkPasswordScore(passwordValue);
|
||||
const feedback = getPasswordStrengthFeedback(result.score);
|
||||
const passwordLength = passwordValue.length;
|
||||
const popoverContent = [];
|
||||
|
||||
$(elementInput).popover('dispose');
|
||||
|
||||
feedbackContainer.style.display = passwordValue === '' ? 'none' : 'block';
|
||||
|
||||
if (result.feedback.warning !== '') {
|
||||
if (result.feedback.warning in hints) {
|
||||
popoverContent.push(hints[result.feedback.warning]);
|
||||
}
|
||||
}
|
||||
|
||||
result.feedback.suggestions.forEach((suggestion) => {
|
||||
if (suggestion in hints) {
|
||||
popoverContent.push(hints[suggestion]);
|
||||
}
|
||||
});
|
||||
|
||||
$(elementInput).popover({
|
||||
html: true,
|
||||
placement: 'top',
|
||||
content: popoverContent.join('<br/>'),
|
||||
}).popover('show');
|
||||
|
||||
const passwordLengthValid = passwordLength >= parseInt(elementInput.dataset.minlength, 10)
|
||||
&& passwordLength <= parseInt(elementInput.dataset.maxlength, 10);
|
||||
const passwordScoreValid = parseInt(elementInput.dataset.minscore, 10) <= result.score;
|
||||
|
||||
feedbackContainer.querySelector(PasswordPolicyMap.requirementLengthIcon).classList.toggle(
|
||||
'text-success',
|
||||
passwordLengthValid,
|
||||
);
|
||||
|
||||
elementIcon.classList.toggle(
|
||||
'text-success',
|
||||
passwordScoreValid,
|
||||
);
|
||||
|
||||
// Change input border color depending on the validity
|
||||
elementInput
|
||||
.classList.remove('border-success', 'border-danger');
|
||||
elementInput
|
||||
.classList.add(passwordScoreValid && passwordLengthValid ? 'border-success' : 'border-danger');
|
||||
elementInput
|
||||
.classList.add('form-control', 'border');
|
||||
|
||||
const percentage = (result.score * 20) + 20;
|
||||
const progressBar = feedbackContainer.querySelector(PasswordPolicyMap.progressBar);
|
||||
|
||||
// increase and decrease progress bar
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
progressBar.classList.remove('bg-success', 'bg-danger', 'bg-warning');
|
||||
progressBar.classList.add(feedback.color);
|
||||
}
|
||||
};
|
||||
|
||||
// Not testable because it manipulates SVG elements, unsupported by JSDom
|
||||
const usePasswordPolicy = (selector) => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element) => {
|
||||
const inputColumn = element?.querySelector(PasswordPolicyMap.inputColumn);
|
||||
const elementInput = element?.querySelector('input');
|
||||
const templateElement = document.createElement('div');
|
||||
const feedbackTemplate = document.querySelector(PasswordPolicyMap.template);
|
||||
let feedbackContainer;
|
||||
|
||||
if (feedbackTemplate && element && inputColumn && elementInput) {
|
||||
templateElement.innerHTML = feedbackTemplate.innerHTML;
|
||||
inputColumn.append(templateElement);
|
||||
feedbackContainer = element.querySelector(PasswordPolicyMap.container);
|
||||
|
||||
if (feedbackContainer) {
|
||||
const hintElement = document.querySelector(PasswordPolicyMap.hint);
|
||||
|
||||
if (hintElement) {
|
||||
const hints = JSON.parse(hintElement.innerHTML);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const passwordRequirementsLength = feedbackContainer.querySelector(PasswordPolicyMap.requirementLength);
|
||||
// eslint-disable-next-line max-len
|
||||
const passwordRequirementsScore = feedbackContainer.querySelector(PasswordPolicyMap.requirementScore);
|
||||
const passwordLengthText = passwordRequirementsLength?.querySelector('span');
|
||||
const passwordRequirementsText = passwordRequirementsScore?.querySelector('span');
|
||||
|
||||
if (passwordLengthText && passwordRequirementsLength && passwordRequirementsLength.dataset.translation) {
|
||||
passwordLengthText.innerText = sprintf(
|
||||
passwordRequirementsLength.dataset.translation,
|
||||
elementInput.dataset.minlength,
|
||||
elementInput.dataset.maxlength,
|
||||
);
|
||||
}
|
||||
|
||||
if (passwordRequirementsText && passwordRequirementsScore && passwordRequirementsScore.dataset.translation) {
|
||||
passwordRequirementsText.innerText = sprintf(
|
||||
passwordRequirementsScore.dataset.translation,
|
||||
hints[elementInput.dataset.minscore],
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
elementInput.addEventListener('keyup', () => watchPassword(elementInput, feedbackContainer, hints));
|
||||
elementInput.addEventListener('blur', () => {
|
||||
$(elementInput).popover('dispose');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
return {
|
||||
element,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: new Error(PASSWORD_POLICY_ERROR),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default usePasswordPolicy;
|
||||
75
_dev/js/theme/components/useStickyElement.js
Normal file
75
_dev/js/theme/components/useStickyElement.js
Normal file
@ -0,0 +1,75 @@
|
||||
import debounce from '@js/theme/utils/debounce';
|
||||
|
||||
/**
|
||||
* Returns sticky element data
|
||||
* @param element
|
||||
* @returns {{getTop: ((function(): number)), getTopOffset: ((function(): number)), getFullTopOffset: (function(): number)}}
|
||||
*/
|
||||
export default (element, stickyWrapper, options = {}) => {
|
||||
if (!element) {
|
||||
throw new Error('Sticky element: element not found');
|
||||
}
|
||||
|
||||
if (!stickyWrapper) {
|
||||
throw new Error('Sticky element: stickyWrapper not found');
|
||||
}
|
||||
|
||||
const {
|
||||
extraOffsetTop = 0,
|
||||
debounceTime = 5,
|
||||
zIndex = 100,
|
||||
} = options;
|
||||
let isSticky = false;
|
||||
const getWrapperRect = () => {
|
||||
const wrapperRect = stickyWrapper.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: wrapperRect.top,
|
||||
bottom: wrapperRect.bottom,
|
||||
height: wrapperRect.height,
|
||||
width: wrapperRect.width,
|
||||
};
|
||||
};
|
||||
const getExtraOffsetTop = typeof extraOffsetTop === 'function' ? extraOffsetTop : () => extraOffsetTop;
|
||||
const setElementSticky = () => {
|
||||
const { height } = getWrapperRect();
|
||||
stickyWrapper.style.height = `${height}px`;
|
||||
element.style.top = `${getExtraOffsetTop()}px`;
|
||||
element.style.left = 0;
|
||||
element.style.right = 0;
|
||||
element.style.bottom = 'auto';
|
||||
element.style.position = 'fixed';
|
||||
element.style.zIndex = zIndex;
|
||||
element.classList.add('is-sticky');
|
||||
isSticky = true;
|
||||
};
|
||||
const unsetElementSticky = () => {
|
||||
element.style.top = null;
|
||||
element.style.bottom = null;
|
||||
element.style.position = null;
|
||||
element.style.zIndex = null;
|
||||
element.classList.remove('is-sticky');
|
||||
stickyWrapper.style.height = null;
|
||||
isSticky = false;
|
||||
};
|
||||
const getIsSticky = () => isSticky;
|
||||
const handleSticky = () => {
|
||||
const { top } = getWrapperRect();
|
||||
|
||||
if (top <= getExtraOffsetTop()) {
|
||||
if (!isSticky) {
|
||||
setElementSticky();
|
||||
}
|
||||
} else if (isSticky) {
|
||||
unsetElementSticky();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', debounce(handleSticky, debounceTime));
|
||||
handleSticky();
|
||||
|
||||
return {
|
||||
getExtraOffsetTop,
|
||||
getIsSticky,
|
||||
};
|
||||
};
|
||||
73
_dev/js/theme/index.js
Normal file
73
_dev/js/theme/index.js
Normal file
@ -0,0 +1,73 @@
|
||||
import $ from 'jquery';
|
||||
import '@js/theme/vendors/bootstrap/bootstrap-imports';
|
||||
import 'bootstrap-touchspin';
|
||||
import 'jquery-hoverintent';
|
||||
import '@js/theme/components/dynamic-bootstrap-components';
|
||||
import bsCustomFileInput from 'bs-custom-file-input';
|
||||
|
||||
import '@js/theme/components/selectors';
|
||||
import '@js/theme/components/sliders';
|
||||
import '@js/theme/components/responsive';
|
||||
import '@js/theme/components/customer';
|
||||
import '@js/theme/components/quickview';
|
||||
import '@js/theme/components/product';
|
||||
import '@js/theme/components/cart/cart';
|
||||
import '@js/theme/components/cart/block-cart';
|
||||
|
||||
import usePasswordPolicy from '@js/theme/components/usePasswordPolicy';
|
||||
import prestashop from 'prestashop';
|
||||
import EventEmitter from 'events';
|
||||
import Form from '@js/theme/components/form';
|
||||
import TopMenu from '@js/theme/components/TopMenu';
|
||||
|
||||
import PageLazyLoad from '@js/theme/components/Lazyload';
|
||||
import PageLoader from '@js/theme/components/PageLoader';
|
||||
import useStickyElement from '@js/theme/components/useStickyElement';
|
||||
|
||||
/* eslint-disable */
|
||||
// "inherit" EventEmitter
|
||||
for (const i in EventEmitter.prototype) {
|
||||
prestashop[i] = EventEmitter.prototype[i];
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
prestashop.pageLazyLoad = new PageLazyLoad({
|
||||
selector: '.lazyload',
|
||||
});
|
||||
|
||||
prestashop.pageLoader = new PageLoader();
|
||||
|
||||
function accLinksTriggerActive() {
|
||||
const url = window.location.pathname;
|
||||
$('.js-customer-links a').each((i, el) => {
|
||||
const $el = $(el);
|
||||
|
||||
if ($el.attr('href').indexOf(url) !== -1) {
|
||||
$el.addClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initStickyHeader() {
|
||||
const header = document.querySelector('.js-header-top');
|
||||
const headerWrapper = document.querySelector('.js-header-top-wrapper');
|
||||
|
||||
if (header && headerWrapper) {
|
||||
useStickyElement(header, headerWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
$(() => {
|
||||
initStickyHeader();
|
||||
accLinksTriggerActive();
|
||||
Form.init();
|
||||
bsCustomFileInput.init();
|
||||
const topMenu = new TopMenu('#_desktop_top_menu .js-main-menu');
|
||||
usePasswordPolicy('.field-password-policy');
|
||||
|
||||
topMenu.init();
|
||||
|
||||
$('.js-select-link').on('change', ({ target }) => {
|
||||
window.location.href = $(target).val();
|
||||
});
|
||||
});
|
||||
42
_dev/js/theme/utils/DynamicImportDOMEvents.js
Normal file
42
_dev/js/theme/utils/DynamicImportDOMEvents.js
Normal file
@ -0,0 +1,42 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
class DynamicImportDOMEvents {
|
||||
constructor({
|
||||
importer,
|
||||
events,
|
||||
eventSelector,
|
||||
preventDefault,
|
||||
} = {}) {
|
||||
this.eventSelector = eventSelector;
|
||||
this.events = events;
|
||||
this.eventsArray = events.split(' ');
|
||||
this.preventDefault = preventDefault;
|
||||
this.importer = importer;
|
||||
this.fetchFiles = this.fetchFiles.bind(this);
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
fetchFiles(e = false) {
|
||||
if (e && this.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.importer.loadFiles(() => {
|
||||
if (e && this.eventsArray.includes(e.type)) {
|
||||
$(e.target).trigger(e.type);
|
||||
this.unbindEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
$(document).on(this.events, this.eventSelector, this.fetchFiles);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
$(document).off(this.events, this.eventSelector, this.fetchFiles);
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicImportDOMEvents;
|
||||
51
_dev/js/theme/utils/DynamicImportHandler.js
Normal file
51
_dev/js/theme/utils/DynamicImportHandler.js
Normal file
@ -0,0 +1,51 @@
|
||||
import DynamicImportJqueryPlugin from '@js/theme/utils/DynamicImportJqueryPlugin';
|
||||
import DynamicImportDOMEvents from '@js/theme/utils/DynamicImportDOMEvents';
|
||||
|
||||
export default class DynamicImportHandler {
|
||||
constructor({
|
||||
files,
|
||||
jqueryPluginCover = null,
|
||||
enableObserve = false,
|
||||
observeOptions = false,
|
||||
DOMEvents = false,
|
||||
DOMEventsSelector = false,
|
||||
DOMEventsPreventDefault = false,
|
||||
onLoadFiles = () => {},
|
||||
} = {}) {
|
||||
this.files = files;
|
||||
this.jqueryPluginCover = jqueryPluginCover;
|
||||
this.enableObserve = enableObserve;
|
||||
this.observeOptions = observeOptions;
|
||||
this.onLoadFiles = onLoadFiles;
|
||||
|
||||
this.jqueryDynamicImport = false;
|
||||
this.dynamicDOMEvents = false;
|
||||
this.filesLoaded = false;
|
||||
|
||||
if (jqueryPluginCover) {
|
||||
this.jqueryDynamicImport = new DynamicImportJqueryPlugin({
|
||||
jqueryPluginCover,
|
||||
importer: this,
|
||||
});
|
||||
}
|
||||
if (DOMEvents && DOMEventsSelector) {
|
||||
this.dynamicDOMEvents = new DynamicImportDOMEvents({
|
||||
events: DOMEvents,
|
||||
eventSelector: DOMEventsSelector,
|
||||
preventDefault: DOMEventsPreventDefault,
|
||||
importer: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadFiles(callback = () => {}) {
|
||||
if (!this.filesLoaded) {
|
||||
Promise.all(this.files()).then((res) => {
|
||||
callback();
|
||||
this.onLoadFiles(res);
|
||||
});
|
||||
|
||||
this.filesLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
_dev/js/theme/utils/DynamicImportJqueryPlugin.js
Normal file
41
_dev/js/theme/utils/DynamicImportJqueryPlugin.js
Normal file
@ -0,0 +1,41 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
class DynamicImportJqueryPlugin {
|
||||
constructor({
|
||||
jqueryPluginCover,
|
||||
importer,
|
||||
} = {}) {
|
||||
this.jqueryPluginCover = jqueryPluginCover;
|
||||
this.importer = importer;
|
||||
this.jqueryFuncCalled = [];
|
||||
|
||||
this.setJqueryPlugin();
|
||||
}
|
||||
|
||||
callJqueryAction() {
|
||||
for (const fncCall of this.jqueryFuncCalled) {
|
||||
fncCall.elem[this.jqueryPluginCover](fncCall.args);
|
||||
}
|
||||
}
|
||||
|
||||
fetchFiles() {
|
||||
this.importer.loadFiles(() => this.callJqueryAction());
|
||||
}
|
||||
|
||||
setJqueryPlugin() {
|
||||
const self = this;
|
||||
|
||||
/* eslint-disable func-names */
|
||||
$.fn[this.jqueryPluginCover] = function (args) {
|
||||
self.jqueryFuncCalled.push({
|
||||
elem: this,
|
||||
args,
|
||||
});
|
||||
self.fetchFiles();
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicImportJqueryPlugin;
|
||||
8
_dev/js/theme/utils/debounce.js
Normal file
8
_dev/js/theme/utils/debounce.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default function debounce(func, timeout = 300) {
|
||||
let timer;
|
||||
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||
};
|
||||
}
|
||||
13
_dev/js/theme/utils/parseToHtml.js
Normal file
13
_dev/js/theme/utils/parseToHtml.js
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Convert a template string into HTML DOM nodes
|
||||
* @param {String} str The template string
|
||||
* @return {Node} The template HTML
|
||||
*/
|
||||
const parseToHtml = (str) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(str, 'text/html');
|
||||
|
||||
return doc.body.children[0];
|
||||
};
|
||||
|
||||
export default parseToHtml;
|
||||
13
_dev/js/theme/vendors/bootstrap/bootstrap-imports.js
vendored
Normal file
13
_dev/js/theme/vendors/bootstrap/bootstrap-imports.js
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import 'bootstrap/js/dist/alert';
|
||||
import 'bootstrap/js/dist/button';
|
||||
import 'bootstrap/js/dist/tab';
|
||||
import 'bootstrap/js/dist/util';
|
||||
|
||||
// MOVED TO DYNAMIC IMPORTS
|
||||
// import 'bootstrap/js/dist/carousel';
|
||||
// import 'bootstrap/js/dist/collapse';
|
||||
// import 'bootstrap/js/dist/dropdown';
|
||||
// import 'bootstrap/js/dist/modal';
|
||||
// import 'bootstrap/js/dist/popover';
|
||||
// import 'bootstrap/js/dist/scrollspy';
|
||||
// import 'bootstrap/js/dist/toast';
|
||||
Reference in New Issue
Block a user