@salla.sa/twilight-components
Version:
Salla Web Component
1,023 lines (1,022 loc) • 39.2 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { Host, h } from "@stencil/core";
import anime from "animejs";
import ShoppingBag from "../../assets/svg/shopping-bag.svg";
import Helper from "../../Helpers/Helper";
export class SallaProductsList {
constructor() {
this.lastViewedProductKey = "lastViewedProductId";
this.filtersKey = "filters";
this.infiniteScrollStateKey = "infiniteScrollState";
this.prevCategoryIdKey = "prevCategoryId";
this.isProcessing = false; // Tracks if we are processing data
this.scrollTimeout = null;
this.source = undefined;
this.sourceValue = undefined;
this.limit = undefined;
this.sortBy = undefined;
this.filtersResults = undefined;
this.horizontalCards = undefined;
this.rowCards = undefined;
this.autoload = false;
this.loadMoreText = undefined;
this.productCardComponent = 'custom-salla-product-card';
this.includes = undefined;
this.page = 1;
this.nextPage = undefined;
this.hasInfiniteScroll = undefined;
this.hasCustomComponent = undefined;
this.sourceValueIsValid = undefined;
this.placeholderText = salla.lang.get('pages.categories.no_products');
this.endOfText = salla.lang.get('common.elements.end_of_content');
this.failedLoadMore = salla.lang.get('common.elements.failed_to_load_more');
this.currentPage = salla.config.get('page');
this.currentCategoryIdFilter = undefined;
this.isReady = undefined;
this.showPlaceholder = undefined;
this.switchToNormalBehavior = undefined;
this.parsedFilters = undefined;
this.filtersSnapshot = [];
//TODO:: check why `this.includes` not working!!
this.includes = Helper.parseJson(this.includes || this.host.getAttribute('includes'));
if (!Array.isArray(this.includes)) {
this.includes = null;
}
Helper.setIncludes(this.includes);
salla.lang.onLoaded(() => {
this.placeholderText = salla.lang.get('pages.categories.no_products');
this.endOfText = salla.lang.get('common.elements.end_of_content');
this.failedLoadMore = salla.lang.get('common.elements.failed_to_load_more');
this.currentPage = salla.config.get('page');
});
}
connectedCallback() {
//Override browser scroll restoration default behaviour
if ("scrollRestoration" in history)
history.scrollRestoration = "manual";
// required for scroll restoration case when the component loads before DOM content is completed (slow internet bandwidth /low device specs)
window.addEventListener('DOMContentLoaded', this.scrollToLastViewedProduct.bind(this));
salla.event.on('salla-filters::changed', filters => this.setFilters(filters));
}
disconnectedCallback() {
window.removeEventListener('DOMContentLoaded', this.scrollToLastViewedProduct);
}
/**
* Set parsed filters data from URI
* @param filters
*/
async setFilters(filters) {
var _a;
if (!filters || JSON.stringify(filters) === JSON.stringify(this.parsedFilters)) {
return;
}
window.scrollTo({ top: 0, behavior: 'smooth' });
// Create a deep copy of the filters object to avoid mutating the original object
this.parsedFilters = JSON.parse(JSON.stringify(filters));
this.filtersSnapshot = this.parsedFilters;
if (((_a = this.currentPage) === null || _a === void 0 ? void 0 : _a.slug) == "product.index" && this.parsedFilters && this.parsedFilters.category_id) {
this.currentCategoryIdFilter = [this.parsedFilters.category_id];
}
return this.reload();
}
/**
* Reload the list of products (entire content of the component).
*/
async reload() {
!this.autoload && this.loadMoreWrapper && (this.loadMoreWrapper.style.display = 'none');
this.hasInfiniteScroll && salla.infiniteScroll.destroy(this.infiniteScroll);
this.buildNextPageUrl();
// TODO: this is problematic in testing, for the time being it's been resolved like this
this.wrapper.innerHTML = '';
if (this.hasInfiniteScroll) {
this.init();
}
else {
this.getInitialData();
}
// Special case for the placeholder loader
if (this.showPlaceholder) {
this.showPlaceholder = false;
this.placeholderLoader = document.createElement('div');
this.placeholderLoader.classList.add('s-products-list-loading-wrapper');
this.placeholderLoader.style.display = 'inherit';
this.placeholderLoader.innerHTML = `<span class="s-button-loader s-button-loader-center s-infinite-scroll-btn-loader"></span>`;
this.host.insertAdjacentElement('afterend', this.placeholderLoader);
}
}
isFilterable() {
return salla.config.get('store.settings.product.filters') && this.filtersResults;
}
isSourceWithoutValue() {
return ['offers', 'latest', 'sales', 'wishlist', 'top-rated'].includes(this.getSource());
}
animateItems() {
anime({
targets: 'salla-products-list salla-product-card',
opacity: [0, 1],
duration: 1200,
translateY: [20, 0],
delay: function (_el, i) {
return i * 100;
},
});
}
initBaseNextPageUrl(source) {
var _a;
this.nextPage = salla.url.api(`products?source=${source}`);
if ((_a = this.includes) === null || _a === void 0 ? void 0 : _a.length) {
this.nextPage += `&includes[]=${this.includes.join('&includes[]=')}`;
}
if (this.limit) {
this.nextPage += `&per_page=${this.limit > 32 ? 32 : this.limit}`;
}
if (this.sortBy) {
this.nextPage += `&sort=${this.sortBy}`;
}
if (salla.config.get('theme.mode') === 'preview') {
this.nextPage += `&use_username_url=1`;
}
this.nextPage += '&filterable=1';
for (const [key, value] of Object.entries(this.parsedFilters || {})) {
if (["string", "number"].includes(typeof value)) {
// @ts-ignore
this.nextPage += `&filters[${encodeURIComponent(key)}]=${encodeURIComponent(value)}`;
}
else if (Array.isArray(value)) {
value.forEach(item => this.nextPage += `&filters[${encodeURIComponent(key)}][]=${encodeURIComponent(item)}`);
}
else if (typeof value === 'object') {
for (const [k, v] of Object.entries(value)) {
this.nextPage += `&filters[${encodeURIComponent(key)}][${encodeURIComponent(k)}]=${encodeURIComponent(v)}`;
}
}
}
}
buildNextPageUrl() {
let source = this.getSource();
if (source === 'json') {
return;
}
const snapshot = sessionStorage.getItem(this.infiniteScrollStateKey);
if (snapshot) {
const { nextPage } = JSON.parse(snapshot);
if (!nextPage) {
return;
}
}
this.initBaseNextPageUrl(source);
if (this.isSourceWithoutValue()) {
return;
}
if (['search', 'related', 'landing-page'].includes(source)) {
this.nextPage += `&source_value=${this.getSourceValue()}`;
return;
}
try {
this.nextPage += `&source_value[]=${this.getSourceValue().join('&source_value[]=')}`;
}
catch (e) {
salla.logger.warn(`source-value prop should be array of ids ex source-value="[1,2,3]" for the source [${source}]`);
this.sourceValueIsValid = false;
}
}
loading(isLoading = true, isBtn = false) {
if (!isLoading) {
if (!this.autoload) {
this.btnLoader && (this.btnLoader.style.display = 'none');
}
this.loader && (this.loader.style.display = 'none');
}
else {
let currentLoader = isBtn && !this.autoload ? this.btnLoader : this.loader;
currentLoader && (currentLoader.style.display = 'inherit');
}
}
getItemHTML(product) {
//as a request they don't want to let the user to open the product details
//todo:: find a better way to handle this request
this.getSource() === 'landing-page' && (product.url = '');
const customComponentTag = this.hasCustomComponent ? this.productCardComponent : 'salla-product-card';
const productCard = document.createElement(customComponentTag);
productCard.product = product;
this.applyLandingPageStyles(productCard);
this.applyHorizontalCardStyles(productCard);
// Attach click event listener to save the current products snapshot
productCard.addEventListener('click', (event) => {
if (!this.hasInfiniteScroll) {
return;
}
const target = event.target;
// Check if the clicked element is an anchor or inside an anchor
const anchor = target.closest('a');
if (anchor) {
this.takeStateSnapshot();
sessionStorage.setItem(this.lastViewedProductKey, product.id);
}
});
return productCard;
}
applyLandingPageStyles(productCard) {
if (this.getSource() === 'landing-page' && !this.hasCustomComponent) {
productCard.toggleAttribute('hide-add-btn', true);
productCard.classList.add('s-product-card-fit-height');
}
}
applyHorizontalCardStyles(productCard) {
if (!this.horizontalCards) {
return;
}
productCard.setAttribute('horizontal', true);
if (!this.hasCustomComponent) {
productCard.setAttribute('shadow-on-hover', true);
}
}
waitForResizing(element) {
let timeout = null;
return new Promise((resolve) => {
const resizeObserver = new ResizeObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
resizeObserver.disconnect();
resolve(null);
}, 160); // Adjust delay as needed for your layout
});
resizeObserver.observe(element); // Watch the body or a specific container
});
}
waitForLayoutStable(element) {
let timeout = null;
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
observer.disconnect();
resolve(null);
}, 160); // Adjust delay as needed for your layout
});
// Observe changes to the entire body, including child nodes and attributes.
observer.observe(element, {
childList: true,
subtree: true,
attributes: true,
});
});
}
async waitForStableLayout(element) {
// Wait for DOM mutations and layout shifts to stabilize
return await Promise.allSettled([
this.waitForResizing(element),
this.waitForLayoutStable(element)
]);
}
scrollToLastViewedProduct() {
const lastViewedProductId = sessionStorage.getItem(this.lastViewedProductKey);
if (!lastViewedProductId)
return;
this.isElementLoaded(`[id*="${lastViewedProductId}"]`)
.then(() => {
var _a;
const productCard = (_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.querySelector(`[id*="${lastViewedProductId}"]`);
if (!productCard)
return;
const scrollToPosition = () => {
const scrollToProductCard = () => {
var _a, _b, _c, _d;
// calculations is located here for last second changes in the ui (ie. sticky header height)
productCard.scrollIntoView({ block: 'start', behavior: 'instant' });
const headerSelector = matchMedia('(max-width: 1024px)').matches ? "header .inner" : "ul.main-menu";
const headerHeight = (window === null || window === void 0 ? void 0 : window.header_is_sticky) ? ((_c = (_b = (_a = document.querySelector(headerSelector)) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.height) !== null && _c !== void 0 ? _c : 56) : 0;
const cardsListRowGap = (_d = parseInt(getComputedStyle(this.wrapper).rowGap)) !== null && _d !== void 0 ? _d : 16;
const productCardOffset = headerHeight + cardsListRowGap;
scrollBy({ top: productCardOffset * -1, behavior: 'instant' });
};
//start scrolling to the product card position
requestAnimationFrame(scrollToProductCard);
const productImages = productCard.querySelectorAll('img.lazy');
productImages.forEach((image) => {
const productImage = image;
const handleImageEvent = () => {
requestAnimationFrame(scrollToProductCard);
productImage.onload = productImage.onerror = null;
};
productImage.onload = handleImageEvent;
productImage.onerror = handleImageEvent;
});
// remove snapshot of product cards items in session storage after scroll restoration completiion
this.removeScrollRestorationSession();
return void 0;
};
this.waitForStableLayout(this.host).then(() => requestAnimationFrame(scrollToPosition));
});
}
isElementLoaded(selector) {
return new Promise((resolve => {
const interval = setInterval(() => {
if (document.querySelector(selector)) {
clearInterval(interval);
return resolve(document.querySelector(selector));
}
}, 50);
}));
}
;
takeStateSnapshot() {
var _a, _b, _c;
const currentPageData = [];
// for performance concerns we used the classic for loop
for (let i = 0; i < this.wrapper.children.length; i++) {
const child = (_a = this.wrapper.children) === null || _a === void 0 ? void 0 : _a[i];
currentPageData.push(child.product);
}
const scrollState = {
pageIndex: Math.max(((_c = (_b = this.infiniteScroll) === null || _b === void 0 ? void 0 : _b.pageIndex) !== null && _c !== void 0 ? _c : 1) - 1, 1),
nextPage: this.nextPage,
currentPageData
};
if (this.isFilterable()) {
const existingFilters = sessionStorage.getItem(this.filtersKey);
sessionStorage.setItem(this.filtersKey, existingFilters || JSON.stringify(this.filtersSnapshot));
}
sessionStorage.setItem(this.infiniteScrollStateKey, JSON.stringify(scrollState));
sessionStorage.setItem(this.prevCategoryIdKey, salla.config.get('page.id'));
}
removeScrollRestorationSession() {
if (!this.isFilterable()) {
sessionStorage.removeItem(this.filtersKey);
}
sessionStorage.removeItem(this.infiniteScrollStateKey);
sessionStorage.removeItem(this.lastViewedProductKey);
sessionStorage.removeItem(this.prevCategoryIdKey);
}
loadStoredScrollState() {
const storedState = sessionStorage.getItem(this.infiniteScrollStateKey);
const filetrsState = sessionStorage.getItem(this.filtersKey);
const prevCategoryId = sessionStorage.getItem(this.prevCategoryIdKey);
/*
* Remove the scroll restoration session under the following conditions:
* 1. The user has navigated to a different category:
*
* 2. The user is on the homepage, but infinite scroll is disabled:
*/
if ((prevCategoryId && Number(prevCategoryId) !== salla.config.get('page.id') && !salla.url.is_page('index'))
||
(salla.url.is_page('index') && !this.hasInfiniteScroll)) {
this.removeScrollRestorationSession();
this.switchToNormalBehavior = true;
return false;
}
if (storedState) {
try {
const { pageIndex, nextPage, currentPageData } = JSON.parse(storedState);
if (filetrsState !== 'undefined') {
const filters = JSON.parse(filetrsState);
salla.event.emit('filters::fetched', { filters });
}
this.page = pageIndex;
this.nextPage = nextPage;
// more performant on larger set of data
for (let i = 0; i < currentPageData.length; i++) {
this.wrapper.append(this.getItemHTML(currentPageData === null || currentPageData === void 0 ? void 0 : currentPageData[i]));
}
return true;
}
catch (error) {
console.error('Failed to load stored scroll state:', error);
this.removeScrollRestorationSession();
}
}
return false;
}
getSource() {
return Helper.getProductsSource(this.source);
}
getSourceValue() {
return this.currentCategoryIdFilter ? this.currentCategoryIdFilter : Helper.getProductsSourceValue(this.source, this.sourceValue);
}
appendDataLayer(data) {
if (typeof dataLayer !== 'object' || !Array.isArray(dataLayer)) {
//todo:: check if we should define it here
return;
}
dataLayer.push({
"event": "impressions",
"ecommerce": {
"currencyCode": salla.config.currency().code,
// "event_id":"", // todo
"impressions": data.map((product, index) => {
var _a, _b, _c;
return {
"id": product.id,
"name": product.name,
"price": product.price,
"brand": ((_a = product.brand) === null || _a === void 0 ? void 0 : _a.name) || '',
"quantity": product.quantity,
// "variant": "",
"categories": [
{
"name": ((_b = product.category) === null || _b === void 0 ? void 0 : _b.name) || salla.config.get('page.title'),
"id": salla.config.get('page.id')
}
],
"category": ((_c = product.category) === null || _c === void 0 ? void 0 : _c.name) || salla.config.get('page.title'),
'position': index + 1
};
})
}
});
}
initiateInfiniteScroll() {
var _a, _b, _c;
if (!this.hasInfiniteScroll) {
return;
}
const shouldApplyManualLoad = this.autoload && this.includes && this.includes.length > 0;
this.infiniteScroll = salla.infiniteScroll.initiate(this.wrapper, this.wrapper, {
path: () => this.nextPage,
history: false,
nextPage: this.nextPage,
scrollThreshold: shouldApplyManualLoad ? false : this.autoload ? 100 : false,
loadOnScroll: shouldApplyManualLoad ? false : this.autoload
}, true);
this.infiniteScroll.pageIndex = this.page;
(_a = this.infiniteScroll) === null || _a === void 0 ? void 0 : _a.on('request', () => {
this.loading(true, this.autoload ? false : true);
});
// Manual scroll listener
shouldApplyManualLoad && window.addEventListener('scroll', this.handleScroll.bind(this));
(_b = this.infiniteScroll) === null || _b === void 0 ? void 0 : _b.on('load', async (response) => {
var _a;
if (this.isProcessing)
return;
this.isProcessing = true;
try {
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.length) && this.infiniteScroll.pageIndex === 2) {
this.showPlaceholder = true;
salla.infiniteScroll.destroy(this.infiniteScroll);
this.loading(false);
this.placeholderLoader && this.placeholderLoader.remove();
return;
}
if (this.includes) {
await this.injectAndProcessData(response);
}
const items = this.handleResponse(response);
this.infiniteScroll.appendItems(items);
if (this.infiniteScroll.pageIndex === 2) {
if (!this.autoload && this.nextPage) {
this.loadMoreWrapper.style.display = 'block';
}
this.animateItems();
}
}
catch (error) {
console.error('Error during load:', error);
}
finally {
this.isProcessing = false;
}
});
(_c = this.infiniteScroll) === null || _c === void 0 ? void 0 : _c.on('error', () => {
console.log('Error occurred during request');
this.status.querySelector('.s-infinite-scroll-error').classList.remove('s-hidden');
this.placeholderLoader && this.placeholderLoader.remove();
this.loading(false);
this.isProcessing = false;
});
salla.onReady(() => {
const snapshot = sessionStorage.getItem(this.infiniteScrollStateKey);
if (!snapshot) {
this.manualLoadNextPage();
}
});
}
handleScroll() {
if (this.isProcessing)
return;
if (this.scrollTimeout)
return;
this.scrollTimeout = setTimeout(() => {
this.scrollTimeout = null;
const wrapperRect = this.wrapper.getBoundingClientRect();
const windowHeight = window.innerHeight;
const distanceToBottom = wrapperRect.bottom - windowHeight;
// Trigger only when the wrapper bottom is within 200px of the viewport bottom
if (distanceToBottom <= 200 && !this.isProcessing && this.nextPage) {
// Reached near the bottom of the wrapper, loading next page...
this.manualLoadNextPage();
}
}, 200);
}
manualLoadNextPage() {
if (this.nextPage) {
this.infiniteScroll.loadNextPage();
}
}
async injectAndProcessData(response) {
try {
await Helper.injectExtraFieldsToResponse(response);
}
catch (error) {
console.error('Error injecting data:', error);
}
}
getInitialData() {
this.loading();
return salla.product.api.fetch({ source: this.getSource(), source_value: this.getSourceValue(), limit: this.limit }).then(async (response) => {
if (!response.data.length) {
this.showPlaceholder = true;
this.placeholderLoader && this.placeholderLoader.remove();
this.loading(false);
return;
}
await Helper.injectExtraFieldsToResponse(response);
//this.firstPageResponse will be null only in the first page, after that it will be assinged,
//for the first page we need to inject the dom after the load, see @method componentDidLoad
if (!this.firstPageResponse) {
this.firstPageResponse = response;
this.nextPage = response.cursor ? response.cursor.next : this.nextPage;
return;
}
this.handleResponse(response).forEach(card => this.wrapper.append(card));
});
}
async loadMore() {
var _a;
(_a = this.infiniteScroll) === null || _a === void 0 ? void 0 : _a.loadNextPage();
}
componentWillLoad() {
return salla.onReady()
.then(() => {
this.hasCustomComponent = !!customElements.get(this.productCardComponent);
this.sourceValueIsValid = !!(this.getSourceValue() || this.isSourceWithoutValue());
this.hasInfiniteScroll = !['json', 'selected', 'related', 'landing-page'].includes(this.getSource()) && !this.limit;
let searchParams = new URLSearchParams(window.location.search);
try {
this.sortBy = this.sortBy || searchParams.get('sort') || searchParams.get('by');
this.parsedFilters = Helper.extractFiltersFromUrl(searchParams);
if (this.parsedFilters && this.parsedFilters.category_id) {
this.currentCategoryIdFilter = [this.parsedFilters.category_id];
}
}
catch (e) {
salla.logger.warn('failed to get filters from url', e.message);
}
this.buildNextPageUrl();
this.isReady = true;
const snapshot = sessionStorage.getItem(this.infiniteScrollStateKey);
if (!!snapshot) {
return;
}
if (!this.sourceValueIsValid) {
salla.logger.warn(`source-value prop is required for source [${this.getSource()}]`);
return;
}
if (this.hasInfiniteScroll) {
return;
}
// Handle json source
if (this.getSource() === 'json') {
if (!this.getSourceValue().length) {
this.showPlaceholder = true;
return;
}
//todo:: avoid using timeout, just assigne the data to the this.firstPageResponse and it should work fine, because it will be rendered in componentDidLoad
setTimeout(() => {
let productsList = this.getSourceValue();
productsList.map(product => this.wrapper.append(this.getItemHTML(product)));
});
return;
}
// Handle selected source
if (this.getSource() === 'selected' && !this.getSourceValue().length) {
this.showPlaceholder = true;
return;
}
return this.getInitialData();
});
}
componentDidLoad() {
this.hasInfiniteScroll && this.init();
if (this.loadStoredScrollState()) {
if (this.autoload) {
!this.nextPage && this.loading(false);
}
else if (!this.nextPage) {
this.loadMoreWrapper && (this.loadMoreWrapper.style.display = 'none');
this.status.querySelector('.s-infinite-scroll-last').classList.remove('s-hidden');
}
else {
this.loadMoreWrapper && (this.loadMoreWrapper.style.display = 'block');
}
this.scrollToLastViewedProduct();
}
else if (!this.firstPageResponse && this.switchToNormalBehavior) {
this.getInitialData()
.then(() => {
if (this.firstPageResponse) {
this.handleResponse(this.firstPageResponse, false).forEach(card => {
this.wrapper.append(card);
});
setTimeout(() => {
if (!this.autoload && this.nextPage && this.infiniteScroll.pageIndex == 1) {
const loadMoreWrapper = this.host.querySelector('.s-infinite-scroll-wrapper');
loadMoreWrapper && (loadMoreWrapper.style.display = 'block');
}
});
}
else {
console.error("No response received after getInitialData.");
}
})
.catch(error => {
console.error("Error during initial data fetch:", error);
});
}
else {
this.firstPageResponse && this.handleResponse(this.firstPageResponse, false).forEach(card => this.wrapper.append(card));
}
}
canRender() {
return this.sourceValueIsValid && this.isReady;
}
render() {
if (!this.canRender()) {
return '';
}
if (this.showPlaceholder) {
return h("div", { class: "s-products-list-placeholder" }, h("span", { innerHTML: ShoppingBag }), h("p", null, this.placeholderText));
}
return (h(Host, { class: "s-products-list" }, h("div", { class: {
"s-products-list-wrapper": true,
's-products-list-horizontal-cards': this.horizontalCards && !this.filtersResults,
's-products-list-vertical-cards': !this.horizontalCards && !this.rowCards && !this.filtersResults,
's-products-list-row-cards': this.rowCards,
's-products-list-filters-results': this.filtersResults,
}, ref: wrapper => this.wrapper = wrapper }), h("div", { class: "s-infinite-scroll-status", ref: status => this.status = status }, h("p", { class: "s-infinite-scroll-last infinite-scroll-last s-hidden" }, this.endOfText), h("p", { class: "s-infinite-scroll-error infinite-scroll-error s-hidden" }, this.failedLoadMore)), this.autoload && h("div", { class: "s-products-list-loading-wrapper", style: { "display": "none" }, ref: loader => this.loader = loader }, h("span", { class: "s-button-loader s-button-loader-center s-infinite-scroll-btn-loader" })), this.hasInfiniteScroll && this.nextPage && !this.autoload ? (h("div", { class: "s-infinite-scroll-wrapper", style: { "display": "none" }, ref: loadMoreWrapper => this.loadMoreWrapper = loadMoreWrapper }, h("button", { onClick: () => this.loadMore(), class: "s-infinite-scroll-btn s-button-btn s-button-primary" }, h("span", { class: "s-button-text s-infinite-scroll-btn-text" }, this.loadMoreText ? this.loadMoreText : salla.lang.get('common.elements.load_more')), h("span", { class: "s-button-loader s-button-loader-center s-infinite-scroll-btn-loader", ref: btnLoader => this.btnLoader = btnLoader, style: { "display": "none" } })))) : ""));
}
init() {
this.initiateInfiniteScroll();
this.loading();
}
handleResponse(response, shouldBuildNextPage = true) {
var _a, _b, _c, _d, _e, _f, _g, _h;
//todo:: check why it reach here undfined one time🤔
if (!response) {
return [];
}
let source = this.getSource();
let title = '';
//help the developer to know the current page title
if (((_a = response.cursor) === null || _a === void 0 ? void 0 : _a.current) === 1) {
title = Helper.getPageTitleForSource(source);
try {
if (this.getSource() === 'search') {
title = salla.lang.get('common.elements.search_about', { 'word': this.getSourceValue() });
}
else if (!title) {
let catId = this.parsedFilters.category_id || this.getSourceValue()[0];
// get the first filter that its key is category_id, then get the value when filter.value.*.key==catId
title = (_f = (_e = (_d = (_c = (_b = response.filters) === null || _b === void 0 ? void 0 : _b.find(filter => filter.key === 'category_id')) === null || _c === void 0 ? void 0 : _c.values) === null || _d === void 0 ? void 0 : _d.find(cat => cat.key === catId)) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : '';
this.filtersSnapshot = response.filters;
}
title += (title ? ' - ' : '') + salla.lang.choice('blocks.header.products_count', (_g = response.data) === null || _g === void 0 ? void 0 : _g.length);
if (response.data.length === 15) {
title = title.replace(response.data.length, salla.lang.get('common.elements.more_than') + ' ' + response.data.length);
}
response.title = title;
}
catch (e) {
salla.logger.error('Error::falid to handle response', e);
}
//inject the SEO schema
//handleResponse works only on the infinite-scroll and infinite scroll works only on the categories pages and main latest-page ..etc, so no need to add condition
Helper.generateProductSchema(response.data);
}
this.appendDataLayer(response.data);
salla.event.emit('salla-products-list::products.fetched', response);
this.productsFetched.emit(response);
//💡 when source is related, cursor will not be existed
if (response.filters && this.isFilterable()) {
this.filtersResults = true;
this.filtersSnapshot = JSON.parse(JSON.stringify(response.filters));
salla.event.emit('filters::fetched', { filters: response.filters });
}
else if (this.isFilterable()) {
salla.event.emit('filters::hidden');
}
//because: this.nextPage is state we don't need to touch it after the build, to avoid the re-rendering
if (shouldBuildNextPage) {
this.nextPage = response.cursor ? response.cursor.next : this.nextPage;
}
this.loading(false);
this.placeholderLoader && this.placeholderLoader.remove();
if (this.hasInfiniteScroll && !this.nextPage) {
this.infiniteScroll.option({ scrollThreshold: false, loadOnScroll: false });
this.status.querySelector('.s-infinite-scroll-last').classList.remove('s-hidden');
}
const productCardsView = [];
for (let i = 0; i < response.data.length; i++) {
productCardsView.push(this.getItemHTML((_h = response.data) === null || _h === void 0 ? void 0 : _h[i]));
}
return productCardsView;
}
static get is() { return "salla-products-list"; }
static get originalStyleUrls() {
return {
"$": ["salla-products-list.scss"]
};
}
static get styleUrls() {
return {
"$": ["salla-products-list.css"]
};
}
static get properties() {
return {
"source": {
"type": "string",
"mutable": true,
"complexType": {
"original": "'categories' | 'latest' | 'related' | 'brands' | 'json' | 'search' | 'tags' | 'selected' | 'offers' | 'landing-page' | 'sales' | 'wishlist' | 'top-rated'",
"resolved": "\"brands\" | \"categories\" | \"json\" | \"landing-page\" | \"latest\" | \"offers\" | \"related\" | \"sales\" | \"search\" | \"selected\" | \"tags\" | \"top-rated\" | \"wishlist\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "type",
"text": "{string}"
}],
"text": "The source of the products list"
},
"attribute": "source",
"reflect": true
},
"sourceValue": {
"type": "any",
"mutable": true,
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "type",
"text": "{string}"
}],
"text": "The source value, cloud be different values as following:\n- array of ids when `source` in ['categories', 'brands', 'tags', 'selected']\n- keyword when `source` = 'search'\n- products payload when `source` = 'json'\n- product_id when `source` = 'related'"
},
"attribute": "source-value",
"reflect": false
},
"limit": {
"type": "number",
"mutable": true,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Limit for number of products in the list."
},
"attribute": "limit",
"reflect": false
},
"sortBy": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string | 'ourSuggest' | 'bestSell' | 'topRated' | 'priceFromTopToLow' | 'priceFromLowToTop'",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Sorting the list of products"
},
"attribute": "sort-by",
"reflect": false
},
"filtersResults": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "should listen to filters events `salla-filters::changed` and re-render"
},
"attribute": "filters-results",
"reflect": true
},
"horizontalCards": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Horizontal cards"
},
"attribute": "horizontal-cards",
"reflect": true
},
"rowCards": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Row cards"
},
"attribute": "row-cards",
"reflect": true
},
"autoload": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Autoload next page when scroll"
},
"attribute": "autoload",
"reflect": false,
"defaultValue": "false"
},
"loadMoreText": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Load more text"
},
"attribute": "load-more-text",
"reflect": false
},
"productCardComponent": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "example",
"text": "<salla-products-list product-card-component=\"my-custom-card-style1\" ...\n<salla-products-list product-card-component=\"my-custom-card-style2\" ..."
}],
"text": "Custom Card Component for the Salla Products List.\n\nThis component allows you to customize the appearance of individual product cards within a Salla Products List."
},
"attribute": "product-card-component",
"reflect": false,
"defaultValue": "'custom-salla-product-card'"
},
"includes": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "string[]",
"resolved": "string[]",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies additional data to be included in the product fetch response.\nThe value can be an array of strings or a JSON string.\n\nExample:\n- As an array: includes=[\"options\"]\n- As a JSON string: includes='[\"options\", \"metadata\", 'images']'\n\nThe array or JSON string can include:\n- \"options\": Include product options in the response.\n- \"images\": Include product images in the response."
}
}
};
}
static get states() {
return {
"page": {},
"nextPage": {},
"hasInfiniteScroll": {},
"hasCustomComponent": {},
"sourceValueIsValid": {},
"placeholderText": {},
"endOfText": {},
"failedLoadMore": {},
"currentPage": {},
"currentCategoryIdFilter": {},
"isReady": {},
"showPlaceholder": {},
"switchToNormalBehavior": {},
"parsedFilters": {},
"filtersSnapshot": {}
};
}
static get events() {
return [{
"method": "productsFetched",
"name": "productsFetched",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Custom event fired when the the products fetched."
},
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
}
}];
}
static get methods() {
return {
"setFilters": {
"complexType": {
"signature": "(filters: any) => Promise<void>",
"parameters": [{
"tags": [{
"name": "param",
"text": "filters"
}],
"text": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Set parsed filters data from URI",
"tags": [{
"name": "param",
"text": "filters"
}]
}
},
"reload": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Reload the list of products (entire content of the component).",
"tags": []
}
}
};
}
static get elementRef() { return "host"; }
}
//# sourceMappingURL=salla-products-list.js.map