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:
2025-11-18 14:04:01 +01:00
parent 3a7f2db331
commit 6849b8eefd
605 changed files with 49820 additions and 0 deletions

BIN
falcon/_dev/js/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
import '@js/checkout/index';

View File

@ -0,0 +1,87 @@
/**
* 2007-2017 PrestaShop
*
* 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.txt.
* 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 http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2017 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
import $ from 'jquery';
import prestashop from 'prestashop';
function setUpCheckout() {
$(prestashop.themeSelectors.checkout.termsLink).on('click', (event) => {
event.preventDefault();
let url = $(event.target).attr('href');
if (url) {
// TODO: Handle request if no pretty URL
url += '?content_only=1';
$.get(url, (content) => {
$(prestashop.themeSelectors.modal)
.find(prestashop.themeSelectors.modalContent)
.html($(content).find('.page-cms').contents());
}).fail((resp) => {
prestashop.emit('handleError', { eventType: 'clickTerms', resp });
});
}
$(prestashop.themeSelectors.modal).modal('show');
});
$(prestashop.themeSelectors.checkout.giftCheckbox).on('click', () => {
$('#gift').slideToggle();
});
}
$(document).ready(() => {
if ($('body#checkout').length === 1) {
setUpCheckout();
}
prestashop.on('updatedDeliveryForm', (params) => {
if (typeof params.deliveryOption === 'undefined' || params.deliveryOption.length === 0) {
return;
}
// Hide all carrier extra content ...
$(prestashop.themeSelectors.checkout.carrierExtraContent).hide();
// and show the one related to the selected carrier
params.deliveryOption.next(prestashop.themeSelectors.checkout.carrierExtraContent).show();
});
prestashop.on('changedCheckoutStep', (params) => {
if (typeof params.event.currentTarget !== 'undefined') {
$('.collapse', params.event.currentTarget).not('.show').not('.collapse .collapse').collapse('show');
}
});
});
$(document).on('change', '.checkout-option input[type="radio"]', (event) => {
const $target = $(event.currentTarget);
const $block = $target.closest('.checkout-option');
const $relatedBlocks = $block.parent();
$relatedBlocks.find('.checkout-option').removeClass('selected');
$block.addClass('selected');
});
$(document).on('click', '.js-checkout-step-header', (event) => {
const stepIdentifier = $(event.currentTarget).data('identifier');
$(`#${stepIdentifier}`).addClass('-current');
$(`#content-${stepIdentifier}`).collapse('show').scrollTop();
});

View File

@ -0,0 +1 @@
import '@js/listing/index';

View File

@ -0,0 +1,54 @@
import prestashop from 'prestashop';
import $ from 'jquery';
import FiltersRangeSliders from '@js/listing/components/filters/FiltersRangeSliders';
class Filters {
constructor() {
this.$body = $('body');
this.setEvents();
this.rangeSliders = FiltersRangeSliders;
this.rangeSliders.init();
}
setEvents() {
prestashop.on('updatedProductList', () => {
prestashop.pageLoader.hideLoader();
this.rangeSliders.init();
});
prestashop.on('updateFacets', () => {
prestashop.pageLoader.showLoader();
});
this.$body.on('click', '.js-search-link', (event) => {
event.preventDefault();
prestashop.emit('updateFacets', $(event.target).closest('a').get(0).href);
});
this.$body.on('change', '[data-action="search-select"]', ({ target }) => {
prestashop.emit('updateFacets', $(target).find('option:selected').data('href'));
});
this.$body.on('click', '.js-search-filters-clear-all', (event) => {
prestashop.emit('updateFacets', this.constructor.parseSearchUrl(event));
});
this.$body.on('change', '#search_filters input[data-search-url]', (event) => {
prestashop.emit('updateFacets', this.constructor.parseSearchUrl(event));
});
}
static parseSearchUrl(event) {
if (event.target.dataset.searchUrl !== undefined) {
return event.target.dataset.searchUrl;
}
if ($(event.target).parent()[0].dataset.searchUrl === undefined) {
throw new Error('Can not parse search URL');
}
return $(event.target).parent()[0].dataset.searchUrl;
}
}
export default Filters;

View File

@ -0,0 +1,15 @@
import $ from 'jquery';
import RangeSlider from '@js/listing/components/filters/RangeSlider';
class FiltersRangeSliders {
static init() {
const $rangeSliders = $('.js-range-slider');
$rangeSliders.each((i, el) => {
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "slider" }] */
const slider = new RangeSlider(el);
});
}
}
export default FiltersRangeSliders;

View File

@ -0,0 +1,115 @@
class FiltersUrlHandler {
constructor() {
this.baseUrl = window.location.origin + window.location.pathname;
this.oldSearchUrl = null;
this.searchUrl = null;
}
setOldSearchUrl() {
this.oldSearchUrl = this.searchUrl;
}
getFiltersUrl() {
this.setOldSearchUrl();
return `${this.baseUrl}?q=${this.searchUrl}`;
}
setSearchUrl() {
const searchParams = new URLSearchParams(window.location.search);
this.searchUrl = searchParams.get('q');
this.oldSearchUrl = searchParams.get('q');
}
setRangeParams(group, { unit, from, to }) {
this.removeGroup(group);
this.appendParam(group, unit);
this.appendParam(group, from);
this.appendParam(group, to);
}
appendParam(group, prop) {
const oldSearchUrl = this.searchUrl || '';
let newSearchUrl = oldSearchUrl.length ? oldSearchUrl.split('/') : [];
let groupExist = false;
const newSearchUrlLength = newSearchUrl.length;
group = FiltersUrlHandler.specialEncode(group);
prop = FiltersUrlHandler.specialEncode(prop);
for (let i = 0; i < newSearchUrlLength; i += 1) {
const filterGroup = newSearchUrl[i];
const filterGroupArray = filterGroup.split('-');
if (filterGroupArray[0] === group) {
newSearchUrl[i] = `${newSearchUrl[i]}-${prop}`;
groupExist = true;
break;
}
}
if (!groupExist) {
newSearchUrl = [...newSearchUrl, `${group}-${prop}`];
}
this.searchUrl = FiltersUrlHandler.specialDecode(FiltersUrlHandler.formatSearchUrl(newSearchUrl));
}
removeGroup(group) {
const oldSearchUrl = this.searchUrl || '';
const newSearchUrl = oldSearchUrl.length ? oldSearchUrl.split('/') : [];
const newSearchUrlLength = newSearchUrl.length;
for (let i = 0; i < newSearchUrlLength; i += 1) {
const filterGroup = newSearchUrl[i];
const filterGroupArray = filterGroup.split('-');
if (filterGroupArray[0] === group) {
newSearchUrl.splice(i, 1);
}
}
this.searchUrl = FiltersUrlHandler.specialDecode(FiltersUrlHandler.formatSearchUrl(newSearchUrl));
}
static toString(value) {
return `${value}`;
}
static specialEncode(str) {
return FiltersUrlHandler.toString(str).replace('/', '[slash]');
}
static specialDecode(str) {
return FiltersUrlHandler.toString(str).replace('[slash]', '/');
}
removeParam(group, prop) {
const oldSearchUrl = this.searchUrl || '';
const newSearchUrl = oldSearchUrl.length ? oldSearchUrl.split('/') : [];
const newSearchUrlLength = newSearchUrl.length;
for (let i = 0; i < newSearchUrlLength; i += 1) {
const filterGroup = newSearchUrl[i];
const filterGroupArray = filterGroup.split('-');
if (filterGroupArray[0] === group) {
const filterResult = filterGroupArray.filter((el) => el !== prop);
if (filterResult.length === 1) {
newSearchUrl.splice(i, 1);
} else {
newSearchUrl[i] = filterResult.join('-');
}
break;
}
}
this.searchUrl = FiltersUrlHandler.specialDecode(FiltersUrlHandler.formatSearchUrl(newSearchUrl));
}
static formatSearchUrl(array) {
return array.join('/');
}
}
export default FiltersUrlHandler;

View File

@ -0,0 +1,182 @@
import $ from 'jquery';
import prestashop from 'prestashop';
import noUiSlider from 'nouislider';
import wNumb from 'wnumb';
import FiltersUrlHandler from '@js/listing/components/filters/FiltersUrlHandler';
class RangeSlider {
constructor(element) {
this.$slider = $(element);
this.setConfig();
this.setFormat();
this.initFilersSlider();
this.setEvents();
}
getSliderType() {
this.sliderType = this.$slider.data('slider-specifications') ? 'price' : 'weight';
}
setConfig() {
this.min = this.$slider.data('slider-min');
this.max = this.$slider.data('slider-max');
this.$parentContainer = this.$slider.closest('.js-input-range-slider-container');
this.$inputs = [this.$parentContainer.find('[data-action="range-from"]'), this.$parentContainer.find('[data-action="range-to"]')];
this.getSliderType();
if (this.sliderType === 'price') {
const {
currencySymbol,
positivePattern,
} = this.$slider.data('slider-specifications');
this.sign = currencySymbol;
this.positivePattern = positivePattern;
this.values = this.$slider.data('slider-values');
this.signPosition = this.positivePattern.indexOf('¤') === 0 ? 'prefix' : 'suffix';
} else if (this.sliderType === 'weight') {
const unit = this.$slider.data('slider-unit');
this.sign = unit;
this.values = this.$slider.data('slider-values');
this.signPosition = 'suffix';
}
if (!Array.isArray(this.values)) {
this.values = [this.min, this.max];
}
}
setFormat() {
this.format = wNumb({
mark: ',',
thousand: ' ',
decimals: 0,
[this.signPosition]:
this.signPosition === 'prefix' ? this.sign : ` ${this.sign}`,
});
}
initFilersSlider() {
this.sliderHandler = noUiSlider.create(this.$slider.get(0), {
start: this.values,
connect: [false, true, false],
range: {
min: this.min,
max: this.max,
},
format: this.format,
});
}
initFilersSliderInputs() {
this.setInputValues(this.values, true);
}
setInputValues(values, formatValue = false) {
this.$inputs.forEach((input, i) => {
const val = formatValue ? this.format.from(values[i]) : values[i];
$(input).val(val);
});
}
setEvents() {
this.sliderHandler.off('set', this.constructor.handlerSliderSet);
this.sliderHandler.on('set', this.constructor.handlerSliderSet);
this.sliderHandler.off('update', this.handlerSliderUpdate);
this.sliderHandler.on('update', this.handlerSliderUpdate);
this.$inputs.forEach(($input) => {
$input.off('focus', this.handleInputFocus);
$input.on('focus', this.handleInputFocus);
$input.off('blur', this.handleInputBlur);
$input.on('blur', this.handleInputBlur);
$input.on('keyup', this.handleInputKeyup);
});
}
static getInputAction($input) {
return $input.data('action');
}
getInputPositionInValue($input) {
const actionPosition = {
'range-from': 0,
'range-to': 1,
};
return actionPosition[this.constructor.getInputAction($input)];
}
handleInputFocus = ({ target }) => {
const $input = $(target);
$input.val(this.format.from($input.val()));
};
handleInputBlur = ({ target }) => {
const $input = $(target);
const value = $input.val();
const position = this.getInputPositionInValue($input);
const oldValues = this.values;
const newValues = [...oldValues];
newValues[position] = value;
if (value !== oldValues[position]) {
this.sliderHandler.set(newValues);
} else {
$input.val(this.format.to(parseFloat($input.val(), 10)));
}
};
handleInputKeyup = ({ target, keyCode }) => {
if (keyCode !== 13) {
return;
}
const $input = $(target);
const value = $input.val();
const position = this.getInputPositionInValue($input);
const oldValues = this.values;
const newValues = [...oldValues];
newValues[position] = value;
if (value !== oldValues[position]) {
this.sliderHandler.set(newValues);
} else {
$input.val(this.format.to(parseFloat($input.val(), 10)));
}
};
handlerSliderUpdate = (
values,
) => {
this.setInputValues(values);
};
static handlerSliderSet(
values,
handle,
unencoded,
tap,
positions,
noUiSliderInstance,
) {
const formatFunction = noUiSliderInstance.options.format;
const $target = $(noUiSliderInstance.target);
const group = $target.data('slider-label');
const unit = $target.data('slider-unit');
const [from, to] = values.map((val) => formatFunction.from(val));
const filtersHandler = new FiltersUrlHandler();
filtersHandler.setSearchUrl();
filtersHandler.setRangeParams(group, { unit, from, to });
const newUrl = filtersHandler.getFiltersUrl();
prestashop.emit('updateFacets', newUrl);
}
}
export default RangeSlider;

View File

@ -0,0 +1,47 @@
import $ from 'jquery';
import prestashop from 'prestashop';
import Filters from '@js/listing/components/filters/Filters';
function updateProductListDOM(data) {
$(prestashop.themeSelectors.listing.searchFilters).replaceWith(
data.rendered_facets,
);
$(prestashop.themeSelectors.listing.activeSearchFilters).replaceWith(
data.rendered_active_filters,
);
$(prestashop.themeSelectors.listing.listTop).replaceWith(
data.rendered_products_top,
);
const renderedProducts = $(data.rendered_products);
const productSelectors = $(prestashop.themeSelectors.listing.product);
if (productSelectors.length > 0) {
productSelectors.removeClass().addClass(productSelectors.first().attr('class'));
} else {
productSelectors.removeClass().addClass(renderedProducts.first().attr('class'));
}
$(prestashop.themeSelectors.listing.list).replaceWith(renderedProducts);
$(prestashop.themeSelectors.listing.listBottom).replaceWith(data.rendered_products_bottom);
if (data.rendered_products_header) {
$(prestashop.themeSelectors.listing.listHeader).replaceWith(data.rendered_products_header);
}
prestashop.emit('updatedProductList', data);
}
$(() => {
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "filters" }] */
const filters = new Filters();
prestashop.on('updateProductList', (data) => {
updateProductListDOM(data);
window.scrollTo(0, 0);
});
prestashop.on('updatedProductList', () => {
prestashop.pageLazyLoad.update();
});
});

View File

@ -0,0 +1 @@
import '@js/product/index';

View File

@ -0,0 +1,101 @@
class ProductGallery {
constructor({
thumbsSliderSelector = '.js-product-thumbs',
mainSliderSelector = '.js-product-main-images',
modalSliderSelector = '.js-modal-gallery',
galleryModalSelector = '.js-product-images-modal',
} = {}) {
this.thumbsSliderSelector = thumbsSliderSelector;
this.mainSliderSelector = mainSliderSelector;
this.modalSliderSelector = modalSliderSelector;
this.galleryModalSelector = galleryModalSelector;
this.mainSliderSwiperInstance = null;
this.modalSliderSwiperInstance = null;
}
init() {
this.mainSliderSwiperInstance = null;
this.modalSliderSwiperInstance = null;
this.initProductImageSlider();
this.initModalGallerySlider();
}
async initProductImageSlider() {
const thumbsElem = document.querySelector(this.thumbsSliderSelector);
const galleryTopElem = document.querySelector(this.mainSliderSelector);
if (!thumbsElem && !galleryTopElem) {
return;
}
const galleryThumbs = new prestashop.SwiperSlider(thumbsElem, {
breakpoints: {
320: {
slidesPerView: 3,
},
576: {
slidesPerView: 4,
},
},
watchSlidesVisibility: true,
watchSlidesProgress: true,
});
const galleryThumbsInstance = await galleryThumbs.initSlider();
const mainSlider = new prestashop.SwiperSlider(galleryTopElem, {
spaceBetween: 10,
navigation: {
nextEl: galleryTopElem.querySelector('.swiper-button-next'),
prevEl: galleryTopElem.querySelector('.swiper-button-prev'),
},
thumbs: {
swiper: galleryThumbsInstance,
},
});
const mainSliderInstance = await mainSlider.initSlider();
this.mainSliderSwiperInstance = mainSliderInstance;
}
initModalGallerySlider() {
const gallerySliderElem = document.querySelector(this.modalSliderSelector);
if (!gallerySliderElem) {
return;
}
const handleModalOpen = async () => {
if (this.modalSliderSwiperInstance) {
gallerySliderElem.style.opacity = 0;
// DIRTY HACK
setTimeout(() => {
this.modalSliderSwiperInstance.update();
this.modalSliderSwiperInstance.slideTo(this.mainSliderSwiperInstance ? this.mainSliderSwiperInstance.activeIndex : 0, 0);
gallerySliderElem.style.opacity = 1;
}, 200);
} else {
const modalSlider = new prestashop.SwiperSlider(gallerySliderElem, {
slidesPerView: 1,
spaceBetween: 10,
initialSlide: this.mainSliderSwiperInstance ? this.mainSliderSwiperInstance.activeIndex : 0,
navigation: {
nextEl: gallerySliderElem.querySelector('.swiper-button-next'),
prevEl: gallerySliderElem.querySelector('.swiper-button-prev'),
},
});
const modalSliderInstance = await modalSlider.initSlider();
this.modalSliderSwiperInstance = modalSliderInstance;
}
};
// TO REFACTO LATER WITH BS5 REMOVE JQUERY!
$(this.galleryModalSelector).on('show.bs.modal', handleModalOpen);
}
}
export default ProductGallery;

View File

@ -0,0 +1,45 @@
import $ from 'jquery';
import ProductGallery from '@js/product/components/ProductGallery';
function activateFirstProductTab() {
$('.product-tabs .nav .nav-item:first-child a').tab('show');
}
function handleProductDetailsToggle() {
const $link = $('[href="#product-details"]');
const $tab = $($link.attr('href'));
if ($tab.length && $link.length && $link.hasClass('active')) {
$tab.addClass('show active');
}
}
$(() => {
activateFirstProductTab();
const gallery = new ProductGallery();
gallery.init();
prestashop.on('updatedProductCombination', (event) => {
gallery.init();
const { product_add_to_cart: productAddToCart } = event;
if (productAddToCart) {
const node = document.createElement('div');
node.innerHTML = productAddToCart;
const html = node.querySelector('.js-product-actions-buttons');
if (html) {
const productActionsElement = document.querySelector('.js-product-actions-buttons');
productActionsElement.replaceWith(html);
}
}
});
prestashop.on('updatedProduct', () => {
handleProductDetailsToggle();
});
});

1
falcon/_dev/js/theme.js Normal file
View File

@ -0,0 +1 @@
import '@js/theme/index';

BIN
falcon/_dev/js/theme/.DS_Store vendored Normal file

Binary file not shown.

View 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;

View 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;

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

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

View 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;
},
);
});

View 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);

View 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'),
],
});
});

View 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,
);
});
}
}
}

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

View 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,
});
});
});
});

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

View 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');
});

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

View File

@ -0,0 +1,11 @@
class DynamicImportSwiperModule {
constructor(getFiles) {
this.getFiles = getFiles;
}
getModule() {
return Promise.all(this.getFiles());
}
}
export default DynamicImportSwiperModule;

View 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;

View 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;

View 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;

View 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;

View 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,
};
};

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

View 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;

View 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;
}
}
}

View 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;

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

View 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;

View 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';