import jQ from 'jquery';
import BaseComponent from '../base-component';
import FilterApi from '../../api/filter-api';
import Selector from '../../helpers/selector';
import FilterResult from './filter-result/filter-result';
import FilterMobileButton from './filter-tree/filter-mobile-button';
import FilterLoadingIcon from './filter-tree/filter-loading-icon';
import FilterScrollToTop from './filter-tree/filter-scroll-to-top';
import FilterTreeEnum from '../../enum/filter-tree-enum';
import Class from '../../helpers/class';
import FilterStyle from "./filter-tree/filter-style/filter-style";
import Globals from '../../helpers/globals';
import Utils from '../../helpers/utils';
/**
* Manages all filter trees, product list and
* other related elements (refine by, filter mobile button,..)
* @extends BaseComponent
*/
class Filter extends BaseComponent {
/**
* Creates a new filter manager.
* Don't manually initialize this class, it is initialized only once in BoostPFS class.
*/
constructor() {
super();
/**
* Arrays of FilterTree objects
* @type {array}
*/
this.filterTrees = [];
this.filterMobileButton = null;
this.filterResult = null;
this.filterLoadingIcon = null;
this.filterScrollToTop = null;
/**
* Unmodified data returned from the filter API
* @type {Object}
*/
this.data = null;
this.fromCache = false;
this.eventType = '';
this.error = '';
this.isFetchedFilterData = false;
};
beforeInit() {
var isXSS = this.isBadUrl();
if (isXSS) {
// If is XSS in URL, redirect to page without any params
this.isInit = true;
Utils.setWindowLocation(window.location.pathname);
}
}
/**
* Test for XSS in URL before init filter
* @return {boolean}
*/
isBadUrl () {
try {
var urlParams = decodeURIComponent(Utils.getWindowLocation().search).split('&');
var isXSSUrl = false;
if (urlParams.length > 0) {
for (var i = 0; i < urlParams.length; i++) {
var param = urlParams[i];
var countOpenTag = (param.match(/</g) || []).length;
var countCloseTag = (param.match(/>/g) || []).length;
var isAlert = (param.match(/alert\(/g) || []).length;
var isExecCommand = (param.match(/execCommand/g) || []).length;
if ((countOpenTag > 0 && countCloseTag > 0) || countOpenTag > 1 || countCloseTag > 1 || isAlert || isExecCommand) {
isXSSUrl = true;
break;
}
}
}
return isXSSUrl;
} catch {
return true;
}
}
init() {
this.initFilterTrees();
this.initFilterMobileButton();
this.filterResult = new FilterResult();
this.addComponent(this.filterResult);
this.filterLoadingIcon = new FilterLoadingIcon();
this.addComponent(this.filterLoadingIcon);
this.filterScrollToTop = new FilterScrollToTop();
this.addComponent(this.filterScrollToTop);
this.filterLoadingIcon.setShow(true);
};
/**
* Send init filter request after initialize the filter component
*/
afterInit() {
FilterApi.updateParamsFromUrl();
FilterApi.getFilterData('init', this.setData.bind(this), this.errorFilterCallback.bind(this));
}
/**
* Get DOM elements with class 'boost-pfs-filter-tree'
* and create new FilterTree instances.
* Assigns unique ID 'boost-pfs-filter-tree1', 'boost-pfs-filter-tree2',... to the DOM elements
*/
initFilterTrees() {
// Selects .boost-pfs-filter-tree on DOM
var $filterTrees = jQ(Selector.filterTree);
$filterTrees.each((index, element) => {
var $element = jQ(element);
// Get filter tree type (vertical, horizontal)
var filterTreeType = '';
if ($element.hasClass(Class.filterTreeVertical)) {
filterTreeType = FilterTreeEnum.FilterTreeType.VERTICAL;
} else if ($element.hasClass(Class.filterTreeHorizontal)) {
filterTreeType = FilterTreeEnum.FilterTreeType.HORIZONTAL;
}
if (filterTreeType) {
// Generate id (#boost-pfs-filter-tree, #boost-pfs-filter-tree2,...)
var filterTreeId = Class.filterTree + (index == 0 ? '' : (index + 1 ).toString());
$element.attr('id', filterTreeId);
// Create filter tree with style
var filterTree = FilterStyle.newFilterTree(filterTreeId, filterTreeType);
this.addComponent(filterTree);
this.filterTrees.push(filterTree);
}
})
}
/**
* Get DOM elements with class 'boost-pfs-filter-tree-mobile-button'
* and create new FilterMobileButton instances.
* 'data-filter-tree-id' on the element: pick which filter tree to toggle
*/
initFilterMobileButton() {
// Selects .boost-pfs-filter-tree-mobile-button on DOM
if (jQ(Selector.filterTreeMobileButton).length > 0) {
var $filterTreeMobileButton = jQ(Selector.filterTreeMobileButton).first();
// data-filter-tree-id: pick which filter tree to toggle, by default it toggles the first Vertical filter tree
var toggleFilterTreeId = $filterTreeMobileButton.attr("data-filter-tree-id");
this.filterMobileButton = null;
var filterTreeSize = this.filterTrees.length;
for (var i = 0; i < filterTreeSize; i++) {
if (!this.filterMobileButton) {
if (jQ('#' + this.filterTrees[i].id).not('[data-is-desktop]').length != 0 &&
((toggleFilterTreeId && this.filterTrees[i].id == toggleFilterTreeId) || (!toggleFilterTreeId && this.filterTrees[i].filterTreeType == FilterTreeEnum.FilterTreeType.VERTICAL))) {
this.filterMobileButton = new FilterMobileButton(this.filterTrees[i]);
this.addComponent(this.filterMobileButton);
}
}
}
}
}
isRender() {
return this.isFetchedFilterData || this.isFetchedProductData;
}
render() {
if (this.isFetchedFilterData) {
// Insert the whole filter tree into the DOM
this.filterTrees.forEach(filterTree => {
if (jQ(filterTree.idSelector).length > 0 && !filterTree.isRenderPartially) {
jQ(filterTree.idSelector).first().html('').append(filterTree.$element);
}
});
// Insert mobile button into the DOM
if (jQ(Selector.filterTreeMobileButton).length > 0 && this.eventType == 'init' && this.filterMobileButton) {
jQ(Selector.filterTreeMobileButton).first().html('').append(this.filterMobileButton.$element);
}
}
};
/**
* Set the filter data returned from API,
* and then call this.refresh() to rebuild everything.
* data including:
* - data.filter: filter tree data (depending on the event, this might not be returned by API)
* - data.products: product list result data (depending on the event, this might not be returned by API)
* @param {Object} data - The data returned from API
* @param {string} eventType - Event type that we called the API with
* @param {Object} eventInfo - Extra info about the event
*/
setData(data, eventType, eventInfo) {
this.isFetchedFilterData = false;
this.isFetchedProductData = false;
this.data = JSON.parse(JSON.stringify(data));
this.fromCache = data.from_cache;
this.error = data.error;
// Remove functions from query params
var cleanQueryParams = JSON.parse(JSON.stringify(Globals.queryParams));
Globals.queryParams = cleanQueryParams;
// Restore the active currency prices
if (typeof Globals.activeCurrencyPrices !== 'undefined') {
jQ.extend(Globals.queryParams, Globals.activeCurrencyPrices);
}
this.eventType = eventType ? eventType : data.event_type;
this.clickedFilterOptionId = (eventInfo && eventInfo.filterOptionId && eventType != 'clear') ? eventInfo.filterOptionId : '';
data = this.modifyData(data);
if (data.filter && data.filter.options && Globals.imutableFilterTree.indexOf(this.eventType) == -1) {
this.isFetchedFilterData = true;
this.filterTrees.forEach((filterTree) => {
filterTree.setData(data.filter);
});
}
if (data.products || data.collections || data.pages) {
this.isFetchedProductData = true;
this.filterResult.setData(data);
}
this.refresh();
this.filterLoadingIcon.setShow(false);
};
/**
* Function to modify data, use for customization
* @param data
*/
modifyData(data) {
return data;
}
errorFilterCallback() {
if(!Utils.isiOS() && !Utils.isSafari() && !Utils.isBackButton()) {
var url = Utils.getWindowLocation().href.split("?")[0];
var searchQuery = Utils.isSearchPage() && Globals.queryParams.hasOwnProperty('q')? '&q=' + Globals.queryParams.q : '';
window.location.replace(url + '?view=boost-pfs-original' + searchQuery);
}
}
}
export default Filter;