import jQ from 'jquery';
import Globals from '../helpers/globals';
import Settings from '../helpers/settings';
import Utils from '../helpers/utils';
import Api from '../helpers/api';
import Navigation from '../helpers/navigation';
import BoostPFS from '../boost-pfs';
import FilterOptionEnum from "../enum/filter-option-enum";
var filterCallback = null;
var apiEvent = {
eventType: '',
eventInfo: {}
}
/**
* The JSONP callback function for our API.
* @param result
*/
let BoostPFSFilterCallback = (result) => {
FilterApi.setDefaultValueForExcludedFields(result);
if (typeof FilterApi.afterCall == 'function') {
FilterApi.afterCall(result, apiEvent.eventType, apiEvent.eventInfo);
}
if (typeof FilterApi.afterCallAsync == 'function') {
FilterApi.afterCallAsync(result, callbackFilterApi, apiEvent.eventType, apiEvent.eventInfo);
return;
}
callbackFilterApi(result);
};
/**
* Calls API with JSONP.
* @param {string} eventType
* @param {function} successCallback
* @param {function} errorCallback
*/
const getFilterData = (eventType, successCallback, errorCallback) => {
// Prepare request param
prepareRequestParams(eventType);
// Execute all before get filter data functions (internal use)
if (beforeFilterApplyCallback) {
beforeFilterApplyCallback.forEach(func => {
if (typeof func == 'function') {
func(eventType);
}
});
}
// Execute before get filter data (customization use)
if (typeof FilterApi.beforeCall == 'function') {
FilterApi.beforeCall(eventType, apiEvent.eventInfo);
}
// Execute before get filter data (customization use - call async 3rd party)
if (typeof FilterApi.beforeCallAsync == 'function') {
var callFilterApiWrapper = () => {
callFilterApi(eventType, successCallback, errorCallback);
}
FilterApi.beforeCallAsync(callFilterApiWrapper, eventType, apiEvent.eventInfo);
return;
}
callFilterApi(eventType, successCallback, errorCallback);
};
/**
* Calls API with JSONP.
* @param {string} eventType
* @param {function} successCallback
* @param {function} errorCallback
* @param {number} errorCount
*/
const callFilterApi = (eventType, successCallback, errorCallback, errorCount) => {
// Get filter data
errorCount = typeof errorCount !== 'undefined' ? errorCount : 0;
filterCallback = successCallback;
Globals.queryParams.callback = 'BoostPFSFilterCallback';
Globals.queryParams.event_type = eventType;
// Prepare url
var url = Api.getApiUrl('filter');
if (Utils.isSearchPage()) {
url = Api.getApiUrl('search');
if (Globals.hasOwnProperty('searchDisplay') && Globals.searchDisplay && Globals.searchDisplay !== 'products') {
url += '/' + Globals.searchDisplay;
}
}
// Create request
var script = document.createElement("script");
script.type = 'text/javascript';
var timestamp = new Date().getTime();
// Remove functions from query params
var cleanQueryParams = JSON.parse(JSON.stringify(Globals.queryParams));
Globals.queryParams = cleanQueryParams;
script.src = url + '?t=' + timestamp + '&' + jQ.param(Globals.queryParams);
script.id = 'boost-pfs-filter-script';
script.async = true;
var resendAPITimer, resendAPIDuration = 2000;
script.addEventListener('error', function (e) {
if (typeof document.getElementById(script.id).remove == 'function') {
// If support is found
document.getElementById(script.id).remove();
} else {
// If not
document.getElementById(script.id).outerHTML = '';
}
// Resend API 2 times when got the error
if (errorCount < 2) {
errorCount++;
if (resendAPITimer) {
clearTimeout(resendAPITimer);
}
resendAPITimer = setTimeout(callFilterApi('resend', successCallback, errorCallback, errorCount), resendAPIDuration);
} else {
if (typeof errorCallback == 'function') {
errorCallback();
}
}
});
// Append the script element to the HEAD section.
document.getElementsByTagName('head')[0].appendChild(script);
script.addEventListener('load', function (e) {
if (typeof document.getElementById(script.id).remove == 'function') {
// If support is found
document.getElementById(script.id).remove();
} else {
// If not
document.getElementById(script.id).outerHTML = '';
}
});
};
const callbackFilterApi = (result) => {
if (typeof filterCallback == 'function') {
filterCallback(result, apiEvent.eventType, apiEvent.eventInfo);
}
}
// List function that will be called before get filter data
var beforeFilterApplyCallback = [];
const addBeforeApplyFilter = (func) => {
beforeFilterApplyCallback.push(func);
}
/**
* Prepare the Globals.queryParams before sending API request.
* It merges Globals.queryParams
* and extra params (page, limit, currency,...).
* @param {string} eventType
*/
const prepareRequestParams = (eventType) => {
var params = mergeObject({}, Globals.queryParams);
// Get Filter params
params = prepareFilterParams(params, eventType);
// Search Page: Get Search params
params = prepareSearchParams(params, eventType);
// Revert price in queryParams to default currency (Multi-currency)
params = revertPriceParams(params, eventType);
// Set locale param
params = Api.setApiLocaleParams(params);
// Remove currency param
if (params.hasOwnProperty('currency')) {
delete params.currency;
}
// Remove currency rate param
if (params.hasOwnProperty('currency_rate')) {
delete params.currency_rate;
}
// Set data back to queryParams
Globals.queryParams = params;
};
/**
* Revert price in queryParams to default currency (Multi-currency)
* @param {Object} params - The query params
*/
const revertPriceParams = (params, eventType) => {
// Detect price filter param by key prefix (pf_p_ or pf_vp_)
var paramKeys = Object.keys(params);
paramKeys = paramKeys.filter(function(key) {
return (key.indexOf('pf_p_') == 0 || key.indexOf('pf_vp_') == 0) && !key.includes('_exclude_from_value');
});
// Storaged the price of active currency
Globals.activeCurrencyPrices = [];
if (paramKeys.length) {
paramKeys.forEach((key) => {
var priceValueList = [];
if (Array.isArray(params[key])) {
params[key].forEach(paramValue => {
var priceVal = paramValue.split(':');
priceVal = priceVal.map((val, i) => {
var isMin = i == 0? true: false;
return val.length == 0? '': Utils.revertPriceToDefaultCurrency(val, isMin);
});
priceValueList.push(priceVal.join(':'));
})
}
Globals.activeCurrencyPrices[key] = params[key];
params[key] = priceValueList;
});
}
return params;
}
/**
* Build the params object for Filter API.
* @param {Object} params
* @param {string} eventType
* @return {Object}
*/
const prepareFilterParams = (params, eventType) => {
// History state
var historyState = Navigation.getHistoryState();
// Get collection id
// If clicking back button, read from history
var collectionId = 0;
if (eventType == 'history') {
// If history has collection_scope (back button to non-first load)
if (historyState && historyState.param && historyState.param.hasOwnProperty('collection_scope')) {
collectionId = parseInt(historyState.param.collection_scope);
// If history doesnt have collection_scope, read from liquid (back button to first load)
} else {
collectionId = parseInt(boostPFSConfig.general.collection_id);
}
// If not back button, read from setting
} else {
// Get collection id from filter tree (non-first load)
if (Globals.collectionId != null) {
collectionId = parseInt(Globals.collectionId);
// Get collection id from liquid (first load)
} else if (boostPFSConfig.general.collection_id) {
collectionId = parseInt(boostPFSConfig.general.collection_id);
}
}
// Assign collection id
Globals.collectionId = collectionId;
params.collection_scope = collectionId;
// Get tag value
// If clicking back button, read from history
var collectionTags = null;
if (eventType == 'history') {
// If history has 'tag', read from history (back button to non-first load)
if (historyState && historyState.param && historyState.param.hasOwnProperty('tag')) {
collectionTags = historyState.param.tag;
// If history doesnt have 'tag', read from liquid (back button to first load)
} else {
collectionTags = boostPFSConfig.general.collectionTags;
}
// If not back button, read from setting
} else {
// Get collection tags from filter tree (non-first load)
if (Globals.collectionTags) {
collectionTags = Globals.collectionTags;
// Get collection tags from liquid (first load)
} else if (boostPFSConfig.general.collectionTags) {
collectionTags = boostPFSConfig.general.collectionTags;
}
}
Globals.collectionTags = collectionTags;
params.tag = collectionTags;
// Get product/variant available or not
if (Settings.getSettingValue('general.availableAfterFiltering') == true) {
params.product_available = Utils.checkExistFilterOptionParam() === true ? true : Globals.productAvailable;
params.variant_available = Utils.checkExistFilterOptionParam() === true ? true : Globals.variantAvailable;
} else {
params.product_available = Globals.productAvailable;
params.variant_available = Globals.variantAvailable;
}
// Display filter option items even they return no product
if (Settings.getSettingValue('general.showOutOfStockOption')) params.zero_options = true;
// Build Filter tree or not
params.build_filter_tree = (typeof eventType !== 'undefined' && Globals.imutableFilterTree.indexOf(eventType) > -1) ? false : true; // Except events of elements in filter tree, all remaining events do not rebuild filter tree
// Enable cache when request params doest not have filter option values, "page" param equal to 1 and "sort" param equal to default sorting
params.check_cache = (Utils.checkExistFilterOptionParam() === false && params.page == 1 && params.sort == Globals.defaultSorting && params.limit == Settings.getSettingValue('general.limit') && !Utils.isSearchPage() && !Utils.isVendorPage() && !Utils.isTypePage()) ? true : false;
// Turn on price mode
if (Settings.getSettingValue('general.priceMode') != '') params.price_mode = Settings.getSettingValue('general.priceMode');
// Tag mode
if (Settings.getSettingValue('general.tagMode') != '') params['tag_mode'] = Settings.getSettingValue('general.tagMode');
// Availability Sorting
if (Settings.getSettingValue('general.sortingAvailableFirst')) params.sort_first = 'available';
// Vendor page: Get Vendor param
if (Utils.isVendorPage() && params.hasOwnProperty('q')) {
var vendorParam = Settings.getSettingValue('general.vendorParam');
// params[vendorParam] = [params['q'].replace(/\+/g, ' ')];
params[vendorParam] = [params['q']];
delete params['q'];
}
// Type page: Get Type param
if (Utils.isTypePage() && params.hasOwnProperty('q')) {
var typeParam = Settings.getSettingValue('general.typeParam');
params[typeParam] = [params['q']];
delete params['q'];
}
return params;
};
/**
* Build the params object for Search API.
* @param {Object} params
* @param {string} eventType
* @return {Object}
*/
const prepareSearchParams = (params, eventType) => {
if (Utils.isSearchPage()) {
params['q'] = Utils.getSearchTerm();
if (Globals.searchTermKey != 'q') delete params[Globals.searchTermKey];
var enableFuzzy = Settings.getSettingValue('search.enableFuzzy');
if (enableFuzzy !== true) params.fuzzy = enableFuzzy;
if (Settings.getSettingValue('search.reduceMinMatch') !== false) {
params.reduce_min_match = Settings.getSettingValue('search.reduceMinMatch');
}
if (Settings.getSettingValue('search.fullMinMatch')) params.full_min_match = true;
if (Settings.getSettingValue('general.sortingAvailableFirst')) params.sort_first = 'available';
if (Settings.getSettingValue('search.enablePlusCharacterSearch')) params.enable_plus_character_search = true;
}
return params;
};
/**
* Update the Globals.queryParam object
* from the current URL. This is called once on app load.
* @param url
*/
const updateParamsFromUrl = (url) => {
// Get Filter params
var params = getFilterParams(url);
// Set default params if missing from url params
params = setDefaultParams(params);
// Set data back to queryParams
Globals.queryParams = params;
};
/**
* Update the Globals.queryParam object
* from the current URL. This is called once on app load.
* @param url
*/
const getFilterParams = (url) => {
var urlQueryString = '';
if (!url) {
urlQueryString = Utils.getWindowLocation().search;
} else {
urlQueryString = typeof url == 'string' && url.split('?').length == 2 ? url.split('?')[1] : '';
}
var urlSearchParams = new URLSearchParams(urlQueryString);
var queryParams = {};
var urlScheme = Settings.getSettingValue('general.urlScheme');
urlSearchParams.forEach(function(value, key) {
var longKey = Navigation.longParamMap.get(key);
if (!longKey) longKey = key;
var isSpecialeKey = Globals.imutableFilterTree.includes(longKey)
|| longKey == Globals.searchTermKey
|| (longKey.startsWith(Globals.prefix) && longKey.includes('_and_condition'))
|| (longKey.startsWith(Globals.prefix) && longKey.includes('_show_exact_rating'))
|| (longKey.startsWith(Globals.prefix) && longKey.includes('_exclude_from_value'));
var isFilterOptionKey = longKey.startsWith(Globals.prefix);
if (isSpecialeKey) {
queryParams[longKey] = value;
Globals.hasFilterOptionParam = true;
} else if (isFilterOptionKey) {
var isFilterCollectionKey = longKey.startsWith(Globals.prefix + '_c_');
var isFilterCollectionTagKey = longKey.startsWith(Globals.prefix + '_ct_');
// If filter by collection, set 'collection_scope'
if (isFilterCollectionKey) {
if (Utils.isSearchPage()) {
queryParams.collection_scope = value;
} else {
return;
}
// If filter by collection tags, set 'tag'
} else if (isFilterCollectionTagKey) {
var tagMode = Settings.getSettingValue('general.multiLevelCollectionSelectType');
queryParams['tag_mode'] = tagMode == FilterOptionEnum.SelectType.MULTIPLE ? '2' : '1';
longKey = 'tag';
}
switch (urlScheme) {
case 2:
queryParams[longKey] = value.split(',');
break;
case 1:
default:
if (queryParams.hasOwnProperty(longKey)) {
queryParams[longKey].push(value);
} else {
queryParams[longKey] = [value];
}
break;
}
// Set the global variables by query params
if (isFilterCollectionKey) {
Globals.collectionId = queryParams.collection_scope;
} else if (isFilterCollectionTagKey) {
Globals.collectionTags = queryParams.tag;
}
Globals.hasFilterOptionParam = true;
}
});
return queryParams;
};
const setDefaultParams = (params) => {
params['_'] = Globals.prefix;
params.shop = params.hasOwnProperty('shop') ? params.shop : Globals.shopDomain;
params.page = params.hasOwnProperty('page') ? parseInt(params.page) : 1;
// Products per page
var settingLimit = Settings.getSettingValue('general.limit');
if (Settings.getSettingValue('general.paginationType') == 'default' || Settings.getSettingValue('general.paginationTypeAdvanced')) {
params.limit = params.hasOwnProperty('limit') ? params.limit : settingLimit;
} else {
params.limit = (params.hasOwnProperty('limit') ? params.limit : settingLimit) * params.page;
}
// Sorting
if (Utils.isSearchPage()) Globals.defaultSorting = 'relevance';
params.sort = params.hasOwnProperty('sort') ? params.sort : Globals.defaultSorting;
// Display type
params.display = params.hasOwnProperty('display') ? params.display : Settings.getSettingValue('general.defaultDisplay');
// Prepare the params for the third-party app
params = setThirdPartyAppParams(params);
return params;
};
const setThirdPartyAppParams = (params) => {
params = setShopifyMultiCurrencyParams(params);
return params;
};
const setShopifyMultiCurrencyParams = (params) => {
// If this store has the multi-currency feature
if (typeof boostPFSConfig !== 'undefined' &&
typeof boostPFSConfig.general.currencies != 'undefined' &&
boostPFSConfig.general.currencies.length > 1) {
// Get the current currency code
var currentCurrency = boostPFSConfig.general.current_currency.toLowerCase().trim();
// Pass the currency param to our api
params.currency = currentCurrency;
// Pass the currency_rate to our api
if (typeof Shopify !== 'undefined' &&
typeof Shopify.currency !== 'undefined' &&
typeof Shopify.currency.rate !== 'undefined') {
params.currency_rate = Shopify.currency.rate;
}
}
return params;
};
const setParam = (key, value) => {
// Set query params
if (value === null || typeof value == 'undefined' || (Array.isArray(value) && value.length == 0)) {
delete Globals.queryParams[key];
} else {
if (Array.isArray(value)) {
// Removes duplicate values
Globals.queryParams[key] = [...new Set(value)];
} else {
Globals.queryParams[key] = value;
}
}
// Set the global values by query params
var isFilterCollectionKey = key.startsWith(Globals.prefix + '_c_');
var isFilterCollectionTagKey = key.startsWith(Globals.prefix + '_ct_');
var paramValue = Globals.queryParams[key];
if (isFilterCollectionKey || key == 'collection_scope') {
Globals.queryParams.collection_scope = paramValue;
Globals.collectionId = paramValue;
// If clear the collection, clear all the tags in it
if (!paramValue) {
var tagKey = key.replace(Globals.prefix + '_c_', Globals.prefix + '_ct_');
delete Globals.queryParams[tagKey];
Globals.queryParams.tag = null;
Globals.collectionTags = null;
}
}
if (isFilterCollectionTagKey) {
Globals.queryParams.tag = paramValue;
Globals.collectionTags = paramValue;
}
}
const applyFilter = (eventType, eventInfo) => {
apiEvent.eventType = eventType;
apiEvent.eventInfo = eventInfo;
BoostPFS.instance.filter.filterLoadingIcon.setShow(true);
getFilterData(
eventType,
BoostPFS.instance.filter.setData.bind(BoostPFS.instance.filter),
BoostPFS.instance.filter.errorFilterCallback.bind(BoostPFS.instance.filter)
);
Navigation.updateAddressBar();
}
/**
* The API exclude some product fields in the response.
* We set the default values for these field.
* @param apiResult
*/
const setDefaultValueForExcludedFields = (apiResult) => {
if (Array.isArray(apiResult.products)){
var currentDate = new Date();
var currentDateString = currentDate.toISOString();
apiResult.products.forEach((product) => {
if (!product.hasOwnProperty('variants')) {
product.variants = [];
}
if (!product.hasOwnProperty('images_info')) {
product.images_info = [];
}
if (!product.hasOwnProperty('collections')) {
product.collections = [];
}
if (!product.hasOwnProperty('tags')) {
product.tags = [];
}
if (!product.hasOwnProperty('skus')) {
product.skus = [];
}
if (!product.hasOwnProperty('options_with_values')) {
product.options_with_values = [];
}
if (!product.hasOwnProperty('barcodes')) {
product.barcodes = [];
}
if (!product.hasOwnProperty('created_at')) {
product.created_at = currentDateString;
}
if (!product.hasOwnProperty('updated_at')) {
product.updated_at = currentDateString;
}
if (!product.hasOwnProperty('published_at')) {
product.published_at = currentDateString;
}
})
}
}
const FilterApi = {
BoostPFSFilterCallback: BoostPFSFilterCallback,
getFilterData: getFilterData,
updateParamsFromUrl: updateParamsFromUrl,
setParam: setParam,
setDefaultValueForExcludedFields: setDefaultValueForExcludedFields,
addBeforeApplyFilter: addBeforeApplyFilter,
applyFilter: applyFilter,
callFilterApi: callFilterApi,
callbackFilterApi: callbackFilterApi,
beforeCall: null,
afterCall: null,
beforeCallAsync: null,
afterCallAsync: null
}
export default FilterApi;