Source: components/filter/filter-tree/filter-option-item/filter-option-item.js

import jQ from 'jquery';

import BaseComponent from "../../../base-component";
import Settings from "../../../../helpers/settings";
import Utils from "../../../../helpers/utils";
import Globals from "../../../../helpers/globals";
import FilterOptionEnum from '../../../../enum/filter-option-enum';
import FilterTreeEnum from '../../../../enum/filter-tree-enum';
import FilterApi from "../../../../api/filter-api";
import Navigation from "../../../../helpers/navigation";
import Labels from "../../../../helpers/labels";

/**
 * Filter option item.
 * A filter option item is a value to filter by. Like blue, red (in color), S (in size)
 * @extends BaseComponent
 */
class FilterOptionItem extends BaseComponent {
	/**
	 * Creates a new FilterOptionItem
	 * @param {FilterTreeEnum} filterTreeType - 'vertical' or 'horizontal'
	 */
	constructor(filterTreeType) {
		super();
		if (!filterTreeType) {
			throw Error('Pass filterTreeType into FilterOptionItem constructor.');
		}

		/**
		 * If set to 'true', when clicked, it will call the API right away.
		 * If set to 'false', when clicked, it will set 'this.selected = true', and not call API.
		 * Use this together with the apply button to check for selected items and then call API.
		 * @type {boolean}
		 */
		this.requestInstantly = true;
		this.filterTreeType = filterTreeType;
		this.$element = null;

		this.settings = {
			enable3rdCurrencySupport: Settings.getSettingValue('general.enable3rdCurrencySupport')
		}
	}

	init() {
		this.requestInstantly = this.filterTreeType == FilterTreeEnum.FilterTreeType.VERTICAL
			|| Settings.getSettingValue('general.requestInstantly');
	}

	getTemplate() {
		throw Error('Override this method')
	}

	compileTemplate() {
		throw Error('Override this method')
	}

	isRender() {
		var filterOption = this.filterOption ? this.filterOption : this.parent;
		var validDocCount = this.hasOwnProperty("docCount") && (this.docCount > 0 || this.docCount === null);
		var isExactStarRating = filterOption.filterType == FilterOptionEnum.FilterType.REVIEW_RATINGS && filterOption.showExactRating;
		var isStaticCollection = filterOption.filterType == FilterOptionEnum.FilterType.COLLECTION && (filterOption.keepValuesStatic || this.handle == 'all');
		var isMultiLevelCollectionTag = (filterOption.displayType == FilterOptionEnum.DisplayType.MULTI_LEVEL_COLLECTIONS && this.level != 1);
		var isShowOutOfStock =  Settings.getSettingValue('general.showOutOfStockOption');
		return isExactStarRating || isStaticCollection || isMultiLevelCollectionTag || validDocCount || isShowOutOfStock;
	}

	render() {
		if (!this.$element) {
			this.$element = jQ(this.compileTemplate());
		}

		this.isSelected = this.isAppliedFilter();
		if (this.isSelected) {
			this.$element.addClass('selected');
			this.$element.find('button').attr('aria-checked', true);
		} else {
			this.$element.removeClass('selected');
			this.$element.find('button').removeAttr('aria-checked');
		}
	}

	/**
	 * Build the product count text of the filter value
	 * @returns {string} - The count text
	 */
	buildCount() {
		var countLabel = '';
		if (Settings.getSettingValue('general.showFilterOptionCount') && this.parent.displayType != 'box') {
			var isShowCount = false;

			if (this.docCount > 0) {
				isShowCount = true;
			} else if (Settings.getSettingValue('general.showOutOfStockOption')) {
				isShowCount = true;
			} else if (this.parent.filterType == FilterOptionEnum.FilterType.REVIEW_RATINGS && this.parent.showExactRating) {
				isShowCount = true;
			}

			var isEmptyAllCollection = this.handle == 'all' && this.docCount == 0;

			if (isShowCount && !this.parent.keepValuesStatic && !isEmptyAllCollection) {
				countLabel = '(' + this.docCount + ')';
                
			}
		}
		return countLabel;
	}

	/**
	 * Format the filter item label: remove prefix, captialize,...
	 * @returns {string}
	 */
	buildLabel() {
		var filterOption = this.filterOption ? this.filterOption : this.parent;

		var label = this.label;
		var prefix = filterOption.prefix;

		if (typeof label != 'string') return '';

		// Remove Prefix
		if (typeof prefix == 'string') {
			prefix = prefix.replace(/\\/g, '');
			label = label.replace(prefix, '').trim();
		}

		// Escape special character
		label = Utils.stripScriptTag(label);

		// FIXME: ignore trip HTML to add spen.money to support Currency (3rd party app)
		if (!this.settings.enable3rdCurrencySupport) {
			label = Utils.stripHtml(label);
		}

		// No capital label of Rating filter option
		if (label.indexOf('boost-pfs-filter-icon-star') > -1) return label;

		// Make the text to uppercase
		filterOption.displayAllValuesInUppercaseForm = filterOption.displayAllValuesInUppercaseForm || false;
		if (filterOption.displayAllValuesInUppercaseForm) return label.toUpperCase();

		// Make all letters lowercase first, then capitalize all first letters of each string in a filter option value
		// For example: HELLO World => Hello World
		if (Settings.getSettingValue('general.forceCapitalizeFilterOptionValues')) return Utils.capitalize(label, true);

		// Make all letters lowercase first, then capitalize first letter of a filter option value
		// For example: product fILTER => Product filter
		if (Settings.getSettingValue('general.capitalizeFirstLetterFilterOptionValues')) return Utils.capitalize(label, true, true);

		// Just capitalize first letter and don't change the format of any other letters 
		// For example: hello wORLD => Hello WORLD
		if (Settings.getSettingValue('general.capitalizeFilterOptionValues')) return Utils.capitalize(label);

		// return label
		return label;
	};

	/**
	 * Build special label for percent sale:
	 * 'Under X%', 'Above Y%',...
	 * @returns {string}
	 */
	buildPercentSaleLabel() {
		var label = '';
		if (!this.from) {
			label = Labels.under + ' ' + this.to + '%';
		} else if (!this.to) {
			label = Labels.above  + ' ' + this.from + '%';
		} else {
			label = this.from + '% - ' + this.to + '%';
		}
		return label;
	}

	buildPriceListLabel() {
		var label = '';
		if (!this.from) {
			label = Labels.under + ' ' + Utils.formatMoney(this.to, Globals.moneyFormat, true);
		} else if (!this.to) {
			label = Labels.above  + ' ' + Utils.formatMoney(this.from, Globals.moneyFormat, true);
		} else {
			label = Utils.formatMoney(this.from, Globals.moneyFormat, true) + ' - ' + Utils.formatMoney(this.to, Globals.moneyFormat, true);
		}
		return label;
	}

	isBindEvents() { return !this.isBoundEvent; }

	bindEvents() {
		// This bind events function is overridden in sub-category, multi-level collections/tags
		if (this.$element) {
			this.$element.on('click', this.onClick.bind(this));
		}
	}

	/**
	 * On click the filter option item.
	 * This checks for 'this.requestInstantly' field to see if we call API right away or just set 'this.selected=true'
	 */
	onClick(event) {
		if (event) {
			event.preventDefault();
		}
		if (!this.isDisabled()) {
			if (this.requestInstantly || this.parent.filterType == FilterOptionEnum.FilterType.COLLECTION) {
				this.onApplyFilter();
			} else {
				this.onSelectFilter();
			}
		}
	}

	/**
	 * Check if the filter item is show but greyed out (disabled)
	 * Item is disabled when they have 0 products
	 * @returns {boolean}
	 */
	isDisabled() {
		if (this.parent.filterType == FilterOptionEnum.FilterType.COLLECTION) {
			if (this.parent.keepValuesStatic) {
				return false;
			} else if (this.handle == 'all') {
				return false;
			} else {
				return this.docCount == 0;
			}
		} else {
			return this.docCount == 0;
		}
	}

	/**
	 * Check if the filter item is applied (already called API)
	 * A filter item might be selected (visually) but we haven't applied it (call API) yet.
	 * @returns {boolean}
	 */
	isAppliedFilter() {
		var filterOptionId = this.parent.filterOptionId;
		var filterType = this.parent.filterType;
		
		if (filterType == FilterOptionEnum.FilterType.COLLECTION) {
			if (Globals.queryParams['collection_scope'] == this.collectionId) {
				return true;
			}
		} else {
			var values = Globals.queryParams[filterOptionId];
			if (Array.isArray(values) && values.includes(this.value)){
				return true;
			}
		}
		return false;
	};

	/**
	 * Select the filter item but don't perform API request yet.
	 */
	onSelectFilter() {
		// Toggle selected
		this.isSelected = !this.isSelected;
		this.$element.toggleClass('selected');
		this.isSelected ? this.$element.find('button').attr('aria-checked', true) : this.$element.find('button').removeAttr('aria-checked');

		// If select type is single, deselects other options
		if (this.isSelected && this.parent.selectType == FilterOptionEnum.SelectType.SINGLE) {
			this.parent.filterItems.forEach(filterItem => {
				if (filterItem != this) {
					if (filterItem.$element) {
						filterItem.$element.removeClass('selected');
						this.$element.find('button').removeAttr('aria-checked');
					}
					filterItem.isSelected = false;
				}

			})
		}
	}

	/**
	 * Perform API request
	 */
	onApplyFilter() {
		var filterType = this.parent.filterType;
		var displayType = this.parent.displayType;
		var selectType = this.parent.selectType;
		var filterOptionId = this.parent.filterOptionId;
		var isEmptyAllCollection = this.handle == 'all' && this.docCount == 0 && filterType == FilterOptionEnum.FilterType.COLLECTION;

		if (this.docCount > 0 || this.parent.keepValuesStatic || displayType == FilterOptionEnum.DisplayType.RANGE || isEmptyAllCollection) {
			// This is action of clicking of user, not browser event
			Globals.internalClick = true;
			var eventType = '';

			// If filter by collection, set collectionId param, change address bar and window title
			if (filterType == FilterOptionEnum.FilterType.COLLECTION) {
				this.isSelected = true;
				Globals.collectionId = this.collectionId;
				FilterApi.setParam('collection_scope', this.collectionId);

				// If search page, adds pf_c_collection param
				if (Utils.isSearchPage()) {
					FilterApi.setParam(filterOptionId, this.collectionId);
				// If collection page, change collection URL
				} else {
					Navigation.setAddressBarPathAfterFilter('/collections/' + this.handle);
					Navigation.setWindowTitleAfterFilter(this.label + ' - ' + Globals.shopName);

					// Set new sort order on collection pages
					FilterApi.setParam('sort', this.sortOrder);
				}

				// Clear all filter option params except collection
				var currentFilterOptionIds = [];
				Object.keys(Globals.queryParams).forEach(queryParam => {
					if (queryParam.startsWith(Globals.prefix) && !queryParam.startsWith(Globals.prefix  + '_c')) {
						currentFilterOptionIds.push(queryParam);
					}
				});

				currentFilterOptionIds.forEach(filterOptionId => {
					FilterApi.setParam(filterOptionId, null);
				});

				eventType = 'collection';
			
			// Else, set the filter option value(s)
			} else {

				// Toggle the selected state
				this.isSelected = !this.isSelected;

				// The values array to send to API
				var values = null;

				// If single option, set or clear the value based on selected state
				if (selectType == FilterOptionEnum.SelectType.SINGLE) {
					values = this.isSelected ? [this.value] : [];

				// If multiple option, remove or add the new value based on selected state
				} else {

					// Get existing value(s)
					values = Globals.queryParams[filterOptionId];
					if (!Array.isArray(values)){
						values = [];
					}

					if (this.isSelected) {
						// Add new value to existing values array
						if (!values.includes(this.value)) {
							values.push(this.value);
						}
					} else {
						// Remove value from existing values array
						values = values.filter(x => x !== this.value);
					}
				}
				FilterApi.setParam(filterOptionId, values);
				FilterApi.setParam(filterOptionId + '_and_condition', this.parent.useAndCondition && values.length > 0 ? true : null);
				FilterApi.setParam(filterOptionId + '_show_exact_rating', this.parent.showExactRating && values.length > 0 ? true : null);
				FilterApi.setParam(filterOptionId + '_exclude_from_value', this.parent.excludePriceFromValue && values.length > 0 ? true : null);
				eventType = 'filter';
			}

			// Reset the page param
			FilterApi.setParam('page', 1);

			var eventInfo = {
				filterOptionId: filterOptionId,
				filterValue: this.value
			}
			FilterApi.applyFilter(eventType, eventInfo);
		}
	}

	/**
	 * Set data for filter option items
	 * This also calls buildLabel(), buildCount(), and set isAppliedFilter()
	 * @param {Object} data - One element of the array: data.filter.options[index].values from API.
	 */
	setData(data) {
		// Note:
		// These displayType extends the setData function: range, sub-cateogory, multi-Level collection/tags.
		// Check the code in those components instead.

		this.value = data.key;
		this.label = data.key;
		this.docCount = data.doc_count ? data.doc_count : 0;
		this.isRenderOnScroll = data.isRenderOnScroll;

		// Set extra data for special filter types.
		// We need to set this data here, and not override in components,
		// because the components are split by display type, not filter type.
		switch (this.parent.filterType) {
			case FilterOptionEnum.FilterType.COLLECTION:
				this.collectionId = data.key;
				this.label = data.displayName ? data.displayName : data.label;
				this.handle = data.handle;
				this.href = Utils.isSearchPage() ? 'javascript:void(0);' : '/collections/' + this.handle;
				this.sortOrder = data.sort_order ? data.sort_order : Globals.defaultSorting;
				break;
			case FilterOptionEnum.FilterType.REVIEW_RATINGS:
				this.from = parseFloat(data.from).toFixed();
				this.value = this.from;
				break;
			case FilterOptionEnum.FilterType.STOCK:
				this.value = data.key == 'in-stock' ? 'true' : 'false';
				this.label = data.label;
				break;
			case FilterOptionEnum.FilterType.PERCENT_SALE:
				this.from = data.from;
				this.to = data.to;
				this.label = this.buildPercentSaleLabel();
				this.value = (this.from ? this.from : '') + ':' + (this.to ? this.to : '');
				break;
			case FilterOptionEnum.FilterType.PRICE:
			case FilterOptionEnum.FilterType.VARIANTS_PRICE:
				this.from = data.from;
				this.to = data.to;
				this.label = this.buildPriceListLabel();
				this.value = (this.from ? this.from : '') + ':' + (this.to ? this.to : '');
				break;
			default:
				break;
		}

		this.label = this.buildLabel();
		this.countLabel = this.buildCount();
		this.isSelected = this.isAppliedFilter();
	}
}

export default FilterOptionItem;