UNPKG

react-instantsearch-core

Version:
1,570 lines (1,354 loc) 752 kB
/*! React InstantSearchCore UNRELEASED | © Algolia, inc. | https://github.com/algolia/instantsearch */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = global || self, factory(global.ReactInstantSearchCore = {}, global.React)); }(this, (function (exports, React) { 'use strict'; var React__default = 'default' in React ? React['default'] : React; var version = '7.16.0'; function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. function EventEmitter() { this._events = this._events || {}; this._maxListeners = this._maxListeners || undefined; } var events = EventEmitter; // Backwards-compat with node 0.10.x // EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function(n) { if (!isNumber(n) || n < 0 || isNaN(n)) throw TypeError('n must be a positive number'); this._maxListeners = n; return this; }; EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; if (!this._events) this._events = {}; // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events.error || (isObject(this._events.error) && !this._events.error.length)) { er = arguments[1]; if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } } } handler = this._events[type]; if (isUndefined(handler)) return false; if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: args = Array.prototype.slice.call(arguments, 1); handler.apply(this, args); } } else if (isObject(handler)) { args = Array.prototype.slice.call(arguments, 1); listeners = handler.slice(); len = listeners.length; for (i = 0; i < len; i++) listeners[i].apply(this, args); } return true; }; EventEmitter.prototype.addListener = function(type, listener) { var m; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events) this._events = {}; // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (this._events.newListener) this.emit('newListener', type, isFunction(listener.listener) ? listener.listener : listener); if (!this._events[type]) // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; else if (isObject(this._events[type])) // If we've already got an array, just append. this._events[type].push(listener); else // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; // Check for listener leak if (isObject(this._events[type]) && !this._events[type].warned) { if (!isUndefined(this._maxListeners)) { m = this._maxListeners; } else { m = EventEmitter.defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else if (listeners) { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.prototype.listenerCount = function(type) { if (this._events) { var evlistener = this._events[type]; if (isFunction(evlistener)) return 1; else if (evlistener) return evlistener.length; } return 0; }; EventEmitter.listenerCount = function(emitter, type) { return emitter.listenerCount(type); }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } function inherits(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true, }, }); } var inherits_1 = inherits; /** * A DerivedHelper is a way to create sub requests to * Algolia from a main helper. * @class * @classdesc The DerivedHelper provides an event based interface for search callbacks: * - search: when a search is triggered using the `search()` method. * - result: when the response is retrieved from Algolia and is processed. * This event contains a {@link SearchResults} object and the * {@link SearchParameters} corresponding to this answer. * @param {AlgoliaSearchHelper} mainHelper the main helper * @param {function} fn the function to create the derived state for search * @param {function} recommendFn the function to create the derived state for recommendations */ function DerivedHelper(mainHelper, fn, recommendFn) { this.main = mainHelper; this.fn = fn; this.recommendFn = recommendFn; this.lastResults = null; this.lastRecommendResults = null; } inherits_1(DerivedHelper, events); /** * Detach this helper from the main helper * @return {undefined} * @throws Error if the derived helper is already detached */ DerivedHelper.prototype.detach = function () { this.removeAllListeners(); this.main.detachDerivedHelper(this); }; DerivedHelper.prototype.getModifiedState = function (parameters) { return this.fn(parameters); }; DerivedHelper.prototype.getModifiedRecommendState = function (parameters) { return this.recommendFn(parameters); }; var DerivedHelper_1 = DerivedHelper; /** * Replaces a leading - with \- * @private * @param {any} value the facet value to replace * @returns {any} the escaped facet value or the value if it was not a string */ function escapeFacetValue(value) { if (typeof value !== 'string') return value; return String(value).replace(/^-/, '\\-'); } /** * Replaces a leading \- with - * @private * @param {any} value the escaped facet value * @returns {any} the unescaped facet value or the value if it was not a string */ function unescapeFacetValue(value) { if (typeof value !== 'string') return value; return value.replace(/^\\-/, '-'); } var escapeFacetValue_1 = { escapeFacetValue: escapeFacetValue, unescapeFacetValue: unescapeFacetValue, }; function clone(value) { if (typeof value === 'object' && value !== null) { return _merge(Array.isArray(value) ? [] : {}, value); } return value; } function isObjectOrArrayOrFunction(value) { return ( typeof value === 'function' || Array.isArray(value) || Object.prototype.toString.call(value) === '[object Object]' ); } function _merge(target, source) { if (target === source) { return target; } // eslint-disable-next-line no-restricted-syntax for (var key in source) { if ( !Object.prototype.hasOwnProperty.call(source, key) || key === '__proto__' || key === 'constructor' ) { // eslint-disable-next-line no-continue continue; } var sourceVal = source[key]; var targetVal = target[key]; if (typeof targetVal !== 'undefined' && typeof sourceVal === 'undefined') { // eslint-disable-next-line no-continue continue; } if ( isObjectOrArrayOrFunction(targetVal) && isObjectOrArrayOrFunction(sourceVal) ) { target[key] = _merge(targetVal, sourceVal); } else { target[key] = clone(sourceVal); } } return target; } /** * This method is like Object.assign, but recursively merges own and inherited * enumerable keyed properties of source objects into the destination object. * * NOTE: this behaves like lodash/merge, but: * - does mutate functions if they are a source * - treats non-plain objects as plain * - does not work for circular objects * - treats sparse arrays as sparse * - does not convert Array-like objects (Arguments, NodeLists, etc.) to arrays * * @param {Object} target The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. */ function merge(target) { if (!isObjectOrArrayOrFunction(target)) { target = {}; } for (var i = 1, l = arguments.length; i < l; i++) { var source = arguments[i]; if (isObjectOrArrayOrFunction(source)) { _merge(target, source); } } return target; } var merge_1 = merge; function objectHasKeys(obj) { return obj && Object.keys(obj).length > 0; } var objectHasKeys_1 = objectHasKeys; // https://github.com/babel/babel/blob/3aaafae053fa75febb3aa45d45b6f00646e30ba4/packages/babel-helpers/src/helpers.js#L604-L620 function _objectWithoutPropertiesLoose$1(source, excluded) { if (source === null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key; var i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; // eslint-disable-next-line no-continue if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var omit = _objectWithoutPropertiesLoose$1; /** * RecommendParameters is the data structure that contains all the information * usable for getting recommendations from the Algolia API. It doesn't do the * search itself, nor does it contains logic about the parameters. * It is an immutable object, therefore it has been created in a way that each * changes does not change the object itself but returns a copy with the * modification. * This object should probably not be instantiated outside of the helper. It * will be provided when needed. * @constructor * @classdesc contains all the parameters for recommendations * @param {RecommendParametersOptions} opts the options to create the object */ function RecommendParameters(opts) { opts = opts || {}; this.params = opts.params || []; } RecommendParameters.prototype = { constructor: RecommendParameters, addParams: function (params) { var newParams = this.params.slice(); newParams.push(params); return new RecommendParameters({ params: newParams }); }, removeParams: function (id) { return new RecommendParameters({ params: this.params.filter(function (param) { return param.$$id !== id; }), }); }, addFrequentlyBoughtTogether: function (params) { return this.addParams( Object.assign({}, params, { model: 'bought-together' }) ); }, addRelatedProducts: function (params) { return this.addParams( Object.assign({}, params, { model: 'related-products' }) ); }, addTrendingItems: function (params) { return this.addParams( Object.assign({}, params, { model: 'trending-items' }) ); }, addTrendingFacets: function (params) { return this.addParams( Object.assign({}, params, { model: 'trending-facets' }) ); }, addLookingSimilar: function (params) { return this.addParams( Object.assign({}, params, { model: 'looking-similar' }) ); }, _buildQueries: function (indexName, cache) { return this.params .filter(function (params) { return cache[params.$$id] === undefined; }) .map(function (params) { var query = Object.assign({}, params, { indexName: indexName, // @TODO: remove this if it ever gets handled by the API threshold: params.threshold || 0, }); delete query.$$id; return query; }); }, }; var RecommendParameters_1 = RecommendParameters; /** * Constructor for SearchResults * @class * @classdesc SearchResults contains the results of a query to Algolia using the * {@link AlgoliaSearchHelper}. * @param {RecommendParameters} state state that led to the response * @param {Record<string,RecommendResultItem>} results the results from algolia client **/ function RecommendResults(state, results) { this._state = state; this._rawResults = {}; // eslint-disable-next-line consistent-this var self = this; state.params.forEach(function (param) { var id = param.$$id; self[id] = results[id]; self._rawResults[id] = results[id]; }); } RecommendResults.prototype = { constructor: RecommendResults, }; var RecommendResults_1 = RecommendResults; function sortObject(obj) { return Object.keys(obj) .sort() .reduce(function (acc, curr) { acc[curr] = obj[curr]; return acc; }, {}); } var requestBuilder = { /** * Get all the queries to send to the client, those queries can used directly * with the Algolia client. * @private * @param {string} index The name of the index * @param {SearchParameters} state The state from which to get the queries * @return {object[]} The queries */ _getQueries: function getQueries(index, state) { var queries = []; // One query for the hits queries.push({ indexName: index, params: requestBuilder._getHitsSearchParams(state), }); // One for each disjunctive facets state.getRefinedDisjunctiveFacets().forEach(function (refinedFacet) { queries.push({ indexName: index, params: requestBuilder._getDisjunctiveFacetSearchParams( state, refinedFacet ), }); }); // More to get the parent levels of the hierarchical facets when refined state.getRefinedHierarchicalFacets().forEach(function (refinedFacet) { var hierarchicalFacet = state.getHierarchicalFacetByName(refinedFacet); var currentRefinement = state.getHierarchicalRefinement(refinedFacet); var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet); // If we are deeper than level 0 (starting from `beer > IPA`) // we want to get all parent values if ( currentRefinement.length > 0 && currentRefinement[0].split(separator).length > 1 ) { // We generate a map of the filters we will use for our facet values queries var filtersMap = currentRefinement[0] .split(separator) .slice(0, -1) .reduce(function createFiltersMap(map, segment, level) { return map.concat({ attribute: hierarchicalFacet.attributes[level], value: level === 0 ? segment : [map[map.length - 1].value, segment].join(separator), }); }, []); filtersMap.forEach(function (filter, level) { var params = requestBuilder._getDisjunctiveFacetSearchParams( state, filter.attribute, level === 0 ); // Keep facet filters unrelated to current hierarchical attributes function hasHierarchicalFacetFilter(value) { return hierarchicalFacet.attributes.some(function (attribute) { return attribute === value.split(':')[0]; }); } var filteredFacetFilters = (params.facetFilters || []).reduce( function (acc, facetFilter) { if (Array.isArray(facetFilter)) { var filtered = facetFilter.filter(function (filterValue) { return !hasHierarchicalFacetFilter(filterValue); }); if (filtered.length > 0) { acc.push(filtered); } } if ( typeof facetFilter === 'string' && !hasHierarchicalFacetFilter(facetFilter) ) { acc.push(facetFilter); } return acc; }, [] ); var parent = filtersMap[level - 1]; if (level > 0) { params.facetFilters = filteredFacetFilters.concat( parent.attribute + ':' + parent.value ); } else if (filteredFacetFilters.length > 0) { params.facetFilters = filteredFacetFilters; } else { delete params.facetFilters; } queries.push({ indexName: index, params: params }); }); } }); return queries; }, /** * Get all the queries to send to the client, those queries can used directly * with the Algolia client. * @private * @param {SearchParameters} state The state from which to get the queries * @return {object[]} The queries */ _getCompositionQueries: function getQueries(state) { return [ { compositionID: state.index, requestBody: { params: requestBuilder._getCompositionHitsSearchParams(state), }, }, ]; }, /** * Build search parameters used to fetch hits * @private * @param {SearchParameters} state The state from which to get the queries * @return {object.<string, any>} The search parameters for hits */ _getHitsSearchParams: function (state) { var facets = state.facets .concat(state.disjunctiveFacets) .concat(requestBuilder._getHitsHierarchicalFacetsAttributes(state)) .sort(); var facetFilters = requestBuilder._getFacetFilters(state); var numericFilters = requestBuilder._getNumericFilters(state); var tagFilters = requestBuilder._getTagFilters(state); var additionalParams = {}; if (facets.length > 0) { additionalParams.facets = facets.indexOf('*') > -1 ? ['*'] : facets; } if (tagFilters.length > 0) { additionalParams.tagFilters = tagFilters; } if (facetFilters.length > 0) { additionalParams.facetFilters = facetFilters; } if (numericFilters.length > 0) { additionalParams.numericFilters = numericFilters; } return sortObject(merge_1({}, state.getQueryParams(), additionalParams)); }, /** * Build search parameters used to fetch hits * @private * @param {SearchParameters} state The state from which to get the queries * @return {object.<string, any>} The search parameters for hits */ _getCompositionHitsSearchParams: function (state) { var facets = state.facets .concat( state.disjunctiveFacets.map(function (value) { if ( state.disjunctiveFacetsRefinements && state.disjunctiveFacetsRefinements[value] && state.disjunctiveFacetsRefinements[value].length > 0 ) { // only tag a disjunctiveFacet as disjunctive if it has a value selected // this helps avoid hitting the limit of 20 disjunctive facets in the Composition API return 'disjunctive(' + value + ')'; } return value; }) ) .concat(requestBuilder._getHitsHierarchicalFacetsAttributes(state)) .sort(); var facetFilters = requestBuilder._getFacetFilters(state); var numericFilters = requestBuilder._getNumericFilters(state); var tagFilters = requestBuilder._getTagFilters(state); var additionalParams = {}; if (facets.length > 0) { additionalParams.facets = facets.indexOf('*') > -1 ? ['*'] : facets; } if (tagFilters.length > 0) { additionalParams.tagFilters = tagFilters; } if (facetFilters.length > 0) { additionalParams.facetFilters = facetFilters; } if (numericFilters.length > 0) { additionalParams.numericFilters = numericFilters; } var params = state.getQueryParams(); delete params.highlightPreTag; delete params.highlightPostTag; // not a valid search parameter, it is handled in _getCompositionQueries delete params.index; return sortObject(merge_1({}, params, additionalParams)); }, /** * Build search parameters used to fetch a disjunctive facet * @private * @param {SearchParameters} state The state from which to get the queries * @param {string} facet the associated facet name * @param {boolean} hierarchicalRootLevel ?? FIXME * @return {object} The search parameters for a disjunctive facet */ _getDisjunctiveFacetSearchParams: function ( state, facet, hierarchicalRootLevel ) { var facetFilters = requestBuilder._getFacetFilters( state, facet, hierarchicalRootLevel ); var numericFilters = requestBuilder._getNumericFilters(state, facet); var tagFilters = requestBuilder._getTagFilters(state); var additionalParams = { hitsPerPage: 0, page: 0, analytics: false, clickAnalytics: false, }; if (tagFilters.length > 0) { additionalParams.tagFilters = tagFilters; } var hierarchicalFacet = state.getHierarchicalFacetByName(facet); if (hierarchicalFacet) { additionalParams.facets = requestBuilder._getDisjunctiveHierarchicalFacetAttribute( state, hierarchicalFacet, hierarchicalRootLevel ); } else { additionalParams.facets = facet; } if (numericFilters.length > 0) { additionalParams.numericFilters = numericFilters; } if (facetFilters.length > 0) { additionalParams.facetFilters = facetFilters; } return sortObject(merge_1({}, state.getQueryParams(), additionalParams)); }, /** * Return the numeric filters in an algolia request fashion * @private * @param {SearchParameters} state the state from which to get the filters * @param {string} [facetName] the name of the attribute for which the filters should be excluded * @return {string[]} the numeric filters in the algolia format */ _getNumericFilters: function (state, facetName) { if (state.numericFilters) { return state.numericFilters; } var numericFilters = []; Object.keys(state.numericRefinements).forEach(function (attribute) { var operators = state.numericRefinements[attribute] || {}; Object.keys(operators).forEach(function (operator) { var values = operators[operator] || []; if (facetName !== attribute) { values.forEach(function (value) { if (Array.isArray(value)) { var vs = value.map(function (v) { return attribute + operator + v; }); numericFilters.push(vs); } else { numericFilters.push(attribute + operator + value); } }); } }); }); return numericFilters; }, /** * Return the tags filters depending on which format is used, either tagFilters or tagRefinements * @private * @param {SearchParameters} state the state from which to get the filters * @return {string} Tag filters in a single string */ _getTagFilters: function (state) { if (state.tagFilters) { return state.tagFilters; } return state.tagRefinements.join(','); }, /** * Build facetFilters parameter based on current refinements. The array returned * contains strings representing the facet filters in the algolia format. * @private * @param {SearchParameters} state The state from which to get the queries * @param {string} [facet] if set, the current disjunctive facet * @param {boolean} [hierarchicalRootLevel] ?? FIXME * @return {array.<string>} The facet filters in the algolia format */ _getFacetFilters: function (state, facet, hierarchicalRootLevel) { var facetFilters = []; var facetsRefinements = state.facetsRefinements || {}; Object.keys(facetsRefinements) .sort() .forEach(function (facetName) { var facetValues = facetsRefinements[facetName] || []; facetValues .slice() .sort() .forEach(function (facetValue) { facetFilters.push(facetName + ':' + facetValue); }); }); var facetsExcludes = state.facetsExcludes || {}; Object.keys(facetsExcludes) .sort() .forEach(function (facetName) { var facetValues = facetsExcludes[facetName] || []; facetValues.sort().forEach(function (facetValue) { facetFilters.push(facetName + ':-' + facetValue); }); }); var disjunctiveFacetsRefinements = state.disjunctiveFacetsRefinements || {}; Object.keys(disjunctiveFacetsRefinements) .sort() .forEach(function (facetName) { var facetValues = disjunctiveFacetsRefinements[facetName] || []; if (facetName === facet || !facetValues || facetValues.length === 0) { return; } var orFilters = []; facetValues .slice() .sort() .forEach(function (facetValue) { orFilters.push(facetName + ':' + facetValue); }); facetFilters.push(orFilters); }); var hierarchicalFacetsRefinements = state.hierarchicalFacetsRefinements || {}; Object.keys(hierarchicalFacetsRefinements) .sort() .forEach(function (facetName) { var facetValues = hierarchicalFacetsRefinements[facetName] || []; var facetValue = facetValues[0]; if (facetValue === undefined) { return; } var hierarchicalFacet = state.getHierarchicalFacetByName(facetName); var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet); var rootPath = state._getHierarchicalRootPath(hierarchicalFacet); var attributeToRefine; var attributesIndex; // we ask for parent facet values only when the `facet` is the current hierarchical facet if (facet === facetName) { // if we are at the root level already, no need to ask for facet values, we get them from // the hits query if ( facetValue.indexOf(separator) === -1 || (!rootPath && hierarchicalRootLevel === true) || (rootPath && rootPath.split(separator).length === facetValue.split(separator).length) ) { return; } if (!rootPath) { attributesIndex = facetValue.split(separator).length - 2; facetValue = facetValue.slice(0, facetValue.lastIndexOf(separator)); } else { attributesIndex = rootPath.split(separator).length - 1; facetValue = rootPath; } attributeToRefine = hierarchicalFacet.attributes[attributesIndex]; } else { attributesIndex = facetValue.split(separator).length - 1; attributeToRefine = hierarchicalFacet.attributes[attributesIndex]; } if (attributeToRefine) { facetFilters.push([attributeToRefine + ':' + facetValue]); } }); return facetFilters; }, _getHitsHierarchicalFacetsAttributes: function (state) { var out = []; return state.hierarchicalFacets.reduce( // ask for as much levels as there's hierarchical refinements function getHitsAttributesForHierarchicalFacet( allAttributes, hierarchicalFacet ) { var hierarchicalRefinement = state.getHierarchicalRefinement( hierarchicalFacet.name )[0]; // if no refinement, ask for root level if (!hierarchicalRefinement) { allAttributes.push(hierarchicalFacet.attributes[0]); return allAttributes; } var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet); var level = hierarchicalRefinement.split(separator).length; var newAttributes = hierarchicalFacet.attributes.slice(0, level + 1); return allAttributes.concat(newAttributes); }, out ); }, _getDisjunctiveHierarchicalFacetAttribute: function ( state, hierarchicalFacet, rootLevel ) { var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet); if (rootLevel === true) { var rootPath = state._getHierarchicalRootPath(hierarchicalFacet); var attributeIndex = 0; if (rootPath) { attributeIndex = rootPath.split(separator).length; } return [hierarchicalFacet.attributes[attributeIndex]]; } var hierarchicalRefinement = state.getHierarchicalRefinement(hierarchicalFacet.name)[0] || ''; // if refinement is 'beers > IPA > Flying dog', // then we want `facets: ['beers > IPA']` as disjunctive facet (parent level values) var parentLevel = hierarchicalRefinement.split(separator).length - 1; return hierarchicalFacet.attributes.slice(0, parentLevel + 1); }, getSearchForFacetQuery: function (facetName, query, maxFacetHits, state) { var stateForSearchForFacetValues = state.isDisjunctiveFacet(facetName) ? state.clearRefinements(facetName) : state; var searchForFacetSearchParameters = { facetQuery: query, facetName: facetName, }; if (typeof maxFacetHits === 'number') { searchForFacetSearchParameters.maxFacetHits = maxFacetHits; } return sortObject( merge_1( {}, requestBuilder._getHitsSearchParams(stateForSearchForFacetValues), searchForFacetSearchParameters ) ); }, }; var requestBuilder_1 = requestBuilder; // NOTE: this behaves like lodash/defaults, but doesn't mutate the target // it also preserve keys order var defaultsPure = function defaultsPure() { var sources = Array.prototype.slice.call(arguments); return sources.reduceRight(function (acc, source) { Object.keys(Object(source)).forEach(function (key) { if (source[key] === undefined) { return; } if (acc[key] !== undefined) { // remove if already added, so that we can add it in correct order delete acc[key]; } acc[key] = source[key]; }); return acc; }, {}); }; // @MAJOR can be replaced by native Array#find when we change support var find = function find(array, comparator) { if (!Array.isArray(array)) { return undefined; } for (var i = 0; i < array.length; i++) { if (comparator(array[i])) { return array[i]; } } return undefined; }; function intersection(arr1, arr2) { return arr1.filter(function (value, index) { return ( arr2.indexOf(value) > -1 && arr1.indexOf(value) === index /* skips duplicates */ ); }); } var intersection_1 = intersection; function valToNumber(v) { if (typeof v === 'number') { return v; } else if (typeof v === 'string') { return parseFloat(v); } else if (Array.isArray(v)) { return v.map(valToNumber); } throw new Error( 'The value should be a number, a parsable string or an array of those.' ); } var valToNumber_1 = valToNumber; var isValidUserToken = function isValidUserToken(userToken) { if (userToken === null) { return false; } return /^[a-zA-Z0-9_-]{1,64}$/.test(userToken); }; /** * Functions to manipulate refinement lists * * The RefinementList is not formally defined through a prototype but is based * on a specific structure. * * @module SearchParameters.refinementList * * @typedef {string[]} SearchParameters.refinementList.Refinements * @typedef {Object.<string, SearchParameters.refinementList.Refinements>} SearchParameters.refinementList.RefinementList */ var lib = { /** * Adds a refinement to a RefinementList * @param {RefinementList} refinementList the initial list * @param {string} attribute the attribute to refine * @param {string} value the value of the refinement, if the value is not a string it will be converted * @return {RefinementList} a new and updated refinement list */ addRefinement: function addRefinement(refinementList, attribute, value) { if (lib.isRefined(refinementList, attribute, value)) { return refinementList; } var valueAsString = '' + value; var facetRefinement = !refinementList[attribute] ? [valueAsString] : refinementList[attribute].concat(valueAsString); var mod = {}; mod[attribute] = facetRefinement; return defaultsPure(mod, refinementList); }, /** * Removes refinement(s) for an attribute: * - if the value is specified removes the refinement for the value on the attribute * - if no value is specified removes all the refinements for this attribute * @param {RefinementList} refinementList the initial list * @param {string} attribute the attribute to refine * @param {string} [value] the value of the refinement * @return {RefinementList} a new and updated refinement lst */ removeRefinement: function removeRefinement( refinementList, attribute, value ) { if (value === undefined) { // we use the "filter" form of clearRefinement, since it leaves empty values as-is // the form with a string will remove the attribute completely return lib.clearRefinement(refinementList, function (v, f) { return attribute === f; }); } var valueAsString = '' + value; return lib.clearRefinement(refinementList, function (v, f) { return attribute === f && valueAsString === v; }); }, /** * Toggles the refinement value for an attribute. * @param {RefinementList} refinementList the initial list * @param {string} attribute the attribute to refine * @param {string} value the value of the refinement * @return {RefinementList} a new and updated list */ toggleRefinement: function toggleRefinement( refinementList, attribute, value ) { if (value === undefined) throw new Error('toggleRefinement should be used with a value'); if (lib.isRefined(refinementList, attribute, value)) { return lib.removeRefinement(refinementList, attribute, value); } return lib.addRefinement(refinementList, attribute, value); }, /** * Clear all or parts of a RefinementList. Depending on the arguments, three * kinds of behavior can happen: * - if no attribute is provided: clears the whole list * - if an attribute is provided as a string: clears the list for the specific attribute * - if an attribute is provided as a function: discards the elements for which the function returns true * @param {RefinementList} refinementList the initial list * @param {string} [attribute] the attribute or function to discard * @param {string} [refinementType] optional parameter to give more context to the attribute function * @return {RefinementList} a new and updated refinement list */ clearRefinement: function clearRefinement( refinementList, attribute, refinementType ) { if (attribute === undefined) { // return the same object if the list is already empty // this is mainly for tests, as it doesn't have much impact on performance if (!objectHasKeys_1(refinementList)) { return refinementList; } return {}; } else if (typeof attribute === 'string') { return omit(refinementList, [attribute]); } else if (typeof attribute === 'function') { var hasChanged = false; var newRefinementList = Object.keys(refinementList).reduce(function ( memo, key ) { var values = refinementList[key] || []; var facetList = values.filter(function (value) { return !attribute(value, key, refinementType); }); if (facetList.length !== values.length) { hasChanged = true; } memo[key] = facetList; return memo; }, {}); if (hasChanged) return newRefinementList; return refinementList; } // We return nothing if the attribute is not undefined, a string or a function, // as it is not a valid value for a refinement return undefined; }, /** * Test if the refinement value is used for the attribute. If no refinement value * is provided, test if the refinementList contains any refinement for the * given attribute. * @param {RefinementList} refinementList the list of refinement * @param {string} attribute name of the attribute * @param {string} [refinementValue] value of the filter/refinement * @return {boolean} true if the attribute is refined, false otherwise */ isRefined: function isRefined(refinementList, attribute, refinementValue) { var containsRefinements = Boolean(refinementList[attribute]) && refinementList[attribute].length > 0; if (refinementValue === undefined || !containsRefinements) { return containsRefinements; } var refinementValueAsString = '' + refinementValue; return refinementList[attribute].indexOf(refinementValueAsString) !== -1; }, }; var RefinementList = lib; /** * isEqual, but only for numeric refinement values, possible values: * - 5 * - [5] * - [[5]] * - [[5,5],[4]] * @param {any} a numeric refinement value * @param {any} b numeric refinement value * @return {boolean} true if the values are equal */ function isEqualNumericRefinement(a, b) { if (Array.isArray(a) && Array.isArray(b)) { return ( a.length === b.length && a.every(function (el, i) { return isEqualNumericRefinement(b[i], el); }) ); } return a === b; } /** * like _.find but using deep equality to be able to use it * to find arrays. * @private * @param {any[]} array array to search into (elements are base or array of base) * @param {any} searchedValue the value we're looking for (base or array of base) * @return {any} the searched value or undefined */ function findArray(array, searchedValue) { return find(array, function (currentValue) { return isEqualNumericRefinement(currentValue, searchedValue); }); } /** * The facet list is the structure used to store the list of values used to * filter a single attribute. * @typedef {string[]} SearchParameters.FacetList */ /** * Structure to store numeric filters with the operator as the key. The supported operators * are `=`, `>`, `<`, `>=`, `<=` and `!=`. * @typedef {Object.<string, Array.<number|number[]>>} SearchParameters.OperatorList */ /** * SearchParameters is the data structure that contains all the information * usable for making a search t