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;