Source: components/filter/filter.js

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;