(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.algoliasearchHelper = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 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; } },{}],2:[function(require,module,exports){ 'use strict'; var AlgoliaSearchHelper = require('./src/algoliasearch.helper'); var SearchParameters = require('./src/SearchParameters'); var SearchResults = require('./src/SearchResults'); /** * The algoliasearchHelper module is the function that will let its * contains everything needed to use the Algoliasearch * Helper. It is a also a function that instanciate the helper. * To use the helper, you also need the Algolia JS client v3. * @example * //using the UMD build * var client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'); * var helper = algoliasearchHelper(client, 'bestbuy', { * facets: ['shipping'], * disjunctiveFacets: ['category'] * }); * helper.on('result', function(event) { * console.log(event.results); * }); * helper * .toggleFacetRefinement('category', 'Movies & TV Shows') * .toggleFacetRefinement('shipping', 'Free shipping') * .search(); * @example * // The helper is an event emitter using the node API * helper.on('result', updateTheResults); * helper.once('result', updateTheResults); * helper.removeListener('result', updateTheResults); * helper.removeAllListeners('result'); * @module algoliasearchHelper * @param {AlgoliaSearch} client an AlgoliaSearch client * @param {string} index the name of the index to query * @param {SearchParameters|object} opts an object defining the initial config of the search. It doesn't have to be a {SearchParameters}, just an object containing the properties you need from it. * @param {SearchResultsOptions|object} searchResultsOptions an object defining the options to use when creating the search results. * @return {AlgoliaSearchHelper} The helper instance */ function algoliasearchHelper(client, index, opts, searchResultsOptions) { return new AlgoliaSearchHelper(client, index, opts, searchResultsOptions); } /** * The version currently used * @member module:algoliasearchHelper.version * @type {number} */ algoliasearchHelper.version = require('./src/version'); /** * Constructor for the Helper. * @member module:algoliasearchHelper.AlgoliaSearchHelper * @type {AlgoliaSearchHelper} */ algoliasearchHelper.AlgoliaSearchHelper = AlgoliaSearchHelper; /** * Constructor for the object containing all the parameters of the search. * @member module:algoliasearchHelper.SearchParameters * @type {SearchParameters} */ algoliasearchHelper.SearchParameters = SearchParameters; /** * Constructor for the object containing the results of the search. * @member module:algoliasearchHelper.SearchResults * @type {SearchResults} */ algoliasearchHelper.SearchResults = SearchResults; module.exports = algoliasearchHelper; },{"./src/SearchParameters":5,"./src/SearchResults":7,"./src/algoliasearch.helper":8,"./src/version":24}],3:[function(require,module,exports){ 'use strict'; var EventEmitter = require('@algolia/events'); var inherits = require('../functions/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 */ function DerivedHelper(mainHelper, fn) { this.main = mainHelper; this.fn = fn; this.lastResults = null; } inherits(DerivedHelper, EventEmitter); /** * 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); }; module.exports = DerivedHelper; },{"../functions/inherits":15,"@algolia/events":1}],4:[function(require,module,exports){ 'use strict'; /** * 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.} SearchParameters.refinementList.RefinementList */ var defaultsPure = require('../functions/defaultsPure'); var objectHasKeys = require('../functions/objectHasKeys'); var omit = require('../functions/omit'); 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(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; }, }; module.exports = lib; },{"../functions/defaultsPure":10,"../functions/objectHasKeys":18,"../functions/omit":19}],5:[function(require,module,exports){ 'use strict'; var defaultsPure = require('../functions/defaultsPure'); var find = require('../functions/find'); var intersection = require('../functions/intersection'); var merge = require('../functions/merge'); var objectHasKeys = require('../functions/objectHasKeys'); var omit = require('../functions/omit'); var valToNumber = require('../functions/valToNumber'); var isValidUserToken = require('../utils/isValidUserToken'); var RefinementList = require('./RefinementList'); /** * 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.>} SearchParameters.OperatorList */ /** * SearchParameters is the data structure that contains all the information * usable for making a search to 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. This object is documented for reference as you'll * get it from events generated by the {@link AlgoliaSearchHelper}. * If need be, instantiate the Helper from the factory function {@link SearchParameters.make} * @constructor * @classdesc contains all the parameters of a search * @param {object|SearchParameters} newParameters existing parameters or partial object * for the properties of a new SearchParameters * @see SearchParameters.make * @example SearchParameters of the first query in * the instant search demo { "query": "", "disjunctiveFacets": [ "customerReviewCount", "category", "salePrice_range", "manufacturer" ], "maxValuesPerFacet": 30, "page": 0, "hitsPerPage": 10, "facets": [ "type", "shipping" ] } */ function SearchParameters(newParameters) { var params = newParameters ? SearchParameters._parseNumbers(newParameters) : {}; if (params.userToken !== undefined && !isValidUserToken(params.userToken)) { // eslint-disable-next-line no-console console.warn( '[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}' ); } /** * This attribute contains the list of all the conjunctive facets * used. This list will be added to requested facets in the * [facets attribute](https://www.algolia.com/doc/rest-api/search#param-facets) sent to algolia. * @member {string[]} */ this.facets = params.facets || []; /** * This attribute contains the list of all the disjunctive facets * used. This list will be added to requested facets in the * [facets attribute](https://www.algolia.com/doc/rest-api/search#param-facets) sent to algolia. * @member {string[]} */ this.disjunctiveFacets = params.disjunctiveFacets || []; /** * This attribute contains the list of all the hierarchical facets * used. This list will be added to requested facets in the * [facets attribute](https://www.algolia.com/doc/rest-api/search#param-facets) sent to algolia. * Hierarchical facets are a sub type of disjunctive facets that * let you filter faceted attributes hierarchically. * @member {string[]|object[]} */ this.hierarchicalFacets = params.hierarchicalFacets || []; // Refinements /** * This attribute contains all the filters that need to be * applied on the conjunctive facets. Each facet must be properly * defined in the `facets` attribute. * * The key is the name of the facet, and the `FacetList` contains all * filters selected for the associated facet name. * * When querying algolia, the values stored in this attribute will * be translated into the `facetFilters` attribute. * @member {Object.} */ this.facetsRefinements = params.facetsRefinements || {}; /** * This attribute contains all the filters that need to be * excluded from the conjunctive facets. Each facet must be properly * defined in the `facets` attribute. * * The key is the name of the facet, and the `FacetList` contains all * filters excluded for the associated facet name. * * When querying algolia, the values stored in this attribute will * be translated into the `facetFilters` attribute. * @member {Object.} */ this.facetsExcludes = params.facetsExcludes || {}; /** * This attribute contains all the filters that need to be * applied on the disjunctive facets. Each facet must be properly * defined in the `disjunctiveFacets` attribute. * * The key is the name of the facet, and the `FacetList` contains all * filters selected for the associated facet name. * * When querying algolia, the values stored in this attribute will * be translated into the `facetFilters` attribute. * @member {Object.} */ this.disjunctiveFacetsRefinements = params.disjunctiveFacetsRefinements || {}; /** * This attribute contains all the filters that need to be * applied on the numeric attributes. * * The key is the name of the attribute, and the value is the * filters to apply to this attribute. * * When querying algolia, the values stored in this attribute will * be translated into the `numericFilters` attribute. * @member {Object.} */ this.numericRefinements = params.numericRefinements || {}; /** * This attribute contains all the tags used to refine the query. * * When querying algolia, the values stored in this attribute will * be translated into the `tagFilters` attribute. * @member {string[]} */ this.tagRefinements = params.tagRefinements || []; /** * This attribute contains all the filters that need to be * applied on the hierarchical facets. Each facet must be properly * defined in the `hierarchicalFacets` attribute. * * The key is the name of the facet, and the `FacetList` contains all * filters selected for the associated facet name. The FacetList values * are structured as a string that contain the values for each level * separated by the configured separator. * * When querying algolia, the values stored in this attribute will * be translated into the `facetFilters` attribute. * @member {Object.} */ this.hierarchicalFacetsRefinements = params.hierarchicalFacetsRefinements || {}; // eslint-disable-next-line consistent-this var self = this; Object.keys(params).forEach(function (paramName) { var isKeyKnown = SearchParameters.PARAMETERS.indexOf(paramName) !== -1; var isValueDefined = params[paramName] !== undefined; if (!isKeyKnown && isValueDefined) { self[paramName] = params[paramName]; } }); } /** * List all the properties in SearchParameters and therefore all the known Algolia properties * This doesn't contain any beta/hidden features. * @private */ SearchParameters.PARAMETERS = Object.keys(new SearchParameters()); /** * @private * @param {object} partialState full or part of a state * @return {object} a new object with the number keys as number */ SearchParameters._parseNumbers = function (partialState) { // Do not parse numbers again in SearchParameters, they ought to be parsed already if (partialState instanceof SearchParameters) return partialState; var numbers = {}; var numberKeys = [ 'aroundPrecision', 'aroundRadius', 'getRankingInfo', 'minWordSizefor2Typos', 'minWordSizefor1Typo', 'page', 'maxValuesPerFacet', 'distinct', 'minimumAroundRadius', 'hitsPerPage', 'minProximity', ]; numberKeys.forEach(function (k) { var value = partialState[k]; if (typeof value === 'string') { var parsedValue = parseFloat(value); // global isNaN is ok to use here, value is only number or NaN numbers[k] = isNaN(parsedValue) ? value : parsedValue; } }); // there's two formats of insideBoundingBox, we need to parse // the one which is an array of float geo rectangles if (Array.isArray(partialState.insideBoundingBox)) { numbers.insideBoundingBox = partialState.insideBoundingBox.map(function ( geoRect ) { if (Array.isArray(geoRect)) { return geoRect.map(function (value) { return parseFloat(value); }); } return geoRect; }); } if (partialState.numericRefinements) { var numericRefinements = {}; Object.keys(partialState.numericRefinements).forEach(function (attribute) { var operators = partialState.numericRefinements[attribute] || {}; numericRefinements[attribute] = {}; Object.keys(operators).forEach(function (operator) { var values = operators[operator]; var parsedValues = values.map(function (v) { if (Array.isArray(v)) { return v.map(function (vPrime) { if (typeof vPrime === 'string') { return parseFloat(vPrime); } return vPrime; }); } else if (typeof v === 'string') { return parseFloat(v); } return v; }); numericRefinements[attribute][operator] = parsedValues; }); }); numbers.numericRefinements = numericRefinements; } return merge(partialState, numbers); }; /** * Factory for SearchParameters * @param {object|SearchParameters} newParameters existing parameters or partial * object for the properties of a new SearchParameters * @return {SearchParameters} frozen instance of SearchParameters */ SearchParameters.make = function makeSearchParameters(newParameters) { var instance = new SearchParameters(newParameters); var hierarchicalFacets = newParameters.hierarchicalFacets || []; hierarchicalFacets.forEach(function (facet) { if (facet.rootPath) { var currentRefinement = instance.getHierarchicalRefinement(facet.name); if ( currentRefinement.length > 0 && currentRefinement[0].indexOf(facet.rootPath) !== 0 ) { instance = instance.clearRefinements(facet.name); } // get it again in case it has been cleared currentRefinement = instance.getHierarchicalRefinement(facet.name); if (currentRefinement.length === 0) { instance = instance.toggleHierarchicalFacetRefinement( facet.name, facet.rootPath ); } } }); return instance; }; /** * Validates the new parameters based on the previous state * @param {SearchParameters} currentState the current state * @param {object|SearchParameters} parameters the new parameters to set * @return {Error|null} Error if the modification is invalid, null otherwise */ SearchParameters.validate = function (currentState, parameters) { var params = parameters || {}; if ( currentState.tagFilters && params.tagRefinements && params.tagRefinements.length > 0 ) { return new Error( '[Tags] Cannot switch from the managed tag API to the advanced API. It is probably ' + 'an error, if it is really what you want, you should first clear the tags with clearTags method.' ); } if (currentState.tagRefinements.length > 0 && params.tagFilters) { return new Error( '[Tags] Cannot switch from the advanced tag API to the managed API. It is probably ' + 'an error, if it is not, you should first clear the tags with clearTags method.' ); } if ( currentState.numericFilters && params.numericRefinements && objectHasKeys(params.numericRefinements) ) { return new Error( "[Numeric filters] Can't switch from the advanced to the managed API. It" + ' is probably an error, if this is really what you want, you have to first' + ' clear the numeric filters.' ); } if (objectHasKeys(currentState.numericRefinements) && params.numericFilters) { return new Error( "[Numeric filters] Can't switch from the managed API to the advanced. It" + ' is probably an error, if this is really what you want, you have to first' + ' clear the numeric filters.' ); } return null; }; SearchParameters.prototype = { constructor: SearchParameters, /** * Remove all refinements (disjunctive + conjunctive + excludes + numeric filters) * @method * @param {undefined|string|SearchParameters.clearCallback} [attribute] optional string or function * - If not given, means to clear all the filters. * - If `string`, means to clear all refinements for the `attribute` named filter. * - If `function`, means to clear all the refinements that return truthy values. * @return {SearchParameters} new instance with filters cleared */ clearRefinements: function clearRefinements(attribute) { var patch = { numericRefinements: this._clearNumericRefinements(attribute), facetsRefinements: RefinementList.clearRefinement( this.facetsRefinements, attribute, 'conjunctiveFacet' ), facetsExcludes: RefinementList.clearRefinement( this.facetsExcludes, attribute, 'exclude' ), disjunctiveFacetsRefinements: RefinementList.clearRefinement( this.disjunctiveFacetsRefinements, attribute, 'disjunctiveFacet' ), hierarchicalFacetsRefinements: RefinementList.clearRefinement( this.hierarchicalFacetsRefinements, attribute, 'hierarchicalFacet' ), }; if ( patch.numericRefinements === this.numericRefinements && patch.facetsRefinements === this.facetsRefinements && patch.facetsExcludes === this.facetsExcludes && patch.disjunctiveFacetsRefinements === this.disjunctiveFacetsRefinements && patch.hierarchicalFacetsRefinements === this.hierarchicalFacetsRefinements ) { return this; } return this.setQueryParameters(patch); }, /** * Remove all the refined tags from the SearchParameters * @method * @return {SearchParameters} new instance with tags cleared */ clearTags: function clearTags() { if (this.tagFilters === undefined && this.tagRefinements.length === 0) return this; return this.setQueryParameters({ tagFilters: undefined, tagRefinements: [], }); }, /** * Set the index. * @method * @param {string} index the index name * @return {SearchParameters} new instance */ setIndex: function setIndex(index) { if (index === this.index) return this; return this.setQueryParameters({ index: index, }); }, /** * Query setter * @method * @param {string} newQuery value for the new query * @return {SearchParameters} new instance */ setQuery: function setQuery(newQuery) { if (newQuery === this.query) return this; return this.setQueryParameters({ query: newQuery, }); }, /** * Page setter * @method * @param {number} newPage new page number * @return {SearchParameters} new instance */ setPage: function setPage(newPage) { if (newPage === this.page) return this; return this.setQueryParameters({ page: newPage, }); }, /** * Facets setter * The facets are the simple facets, used for conjunctive (and) faceting. * @method * @param {string[]} facets all the attributes of the algolia records used for conjunctive faceting * @return {SearchParameters} new instance */ setFacets: function setFacets(facets) { return this.setQueryParameters({ facets: facets, }); }, /** * Disjunctive facets setter * Change the list of disjunctive (or) facets the helper chan handle. * @method * @param {string[]} facets all the attributes of the algolia records used for disjunctive faceting * @return {SearchParameters} new instance */ setDisjunctiveFacets: function setDisjunctiveFacets(facets) { return this.setQueryParameters({ disjunctiveFacets: facets, }); }, /** * HitsPerPage setter * Hits per page represents the number of hits retrieved for this query * @method * @param {number} n number of hits retrieved per page of results * @return {SearchParameters} new instance */ setHitsPerPage: function setHitsPerPage(n) { if (this.hitsPerPage === n) return this; return this.setQueryParameters({ hitsPerPage: n, }); }, /** * typoTolerance setter * Set the value of typoTolerance * @method * @param {string} typoTolerance new value of typoTolerance ("true", "false", "min" or "strict") * @return {SearchParameters} new instance */ setTypoTolerance: function setTypoTolerance(typoTolerance) { if (this.typoTolerance === typoTolerance) return this; return this.setQueryParameters({ typoTolerance: typoTolerance, }); }, /** * Add a numeric filter for a given attribute * When value is an array, they are combined with OR * When value is a single value, it will combined with AND * @method * @param {string} attribute attribute to set the filter on * @param {string} operator operator of the filter (possible values: =, >, >=, <, <=, !=) * @param {number | number[]} value value of the filter * @return {SearchParameters} new instance * @example * // for price = 50 or 40 * state.addNumericRefinement('price', '=', [50, 40]); * @example * // for size = 38 and 40 * state.addNumericRefinement('size', '=', 38); * state.addNumericRefinement('size', '=', 40); */ addNumericRefinement: function (attribute, operator, value) { var val = valToNumber(value); if (this.isNumericRefined(attribute, operator, val)) return this; var mod = merge({}, this.numericRefinements); mod[attribute] = merge({}, mod[attribute]); if (mod[attribute][operator]) { // Array copy mod[attribute][operator] = mod[attribute][operator].slice(); // Add the element. Concat can't be used here because value can be an array. mod[attribute][operator].push(val); } else { mod[attribute][operator] = [val]; } return this.setQueryParameters({ numericRefinements: mod, }); }, /** * Get the list of conjunctive refinements for a single facet * @param {string} facetName name of the attribute used for faceting * @return {string[]} list of refinements */ getConjunctiveRefinements: function (facetName) { if (!this.isConjunctiveFacet(facetName)) { return []; } return this.facetsRefinements[facetName] || []; }, /** * Get the list of disjunctive refinements for a single facet * @param {string} facetName name of the attribute used for faceting * @return {string[]} list of refinements */ getDisjunctiveRefinements: function (facetName) { if (!this.isDisjunctiveFacet(facetName)) { return []; } return this.disjunctiveFacetsRefinements[facetName] || []; }, /** * Get the list of hierarchical refinements for a single facet * @param {string} facetName name of the attribute used for faceting * @return {string[]} list of refinements */ getHierarchicalRefinement: function (facetName) { // we send an array but we currently do not support multiple // hierarchicalRefinements for a hierarchicalFacet return this.hierarchicalFacetsRefinements[facetName] || []; }, /** * Get the list of exclude refinements for a single facet * @param {string} facetName name of the attribute used for faceting * @return {string[]} list of refinements */ getExcludeRefinements: function (facetName) { if (!this.isConjunctiveFacet(facetName)) { return []; } return this.facetsExcludes[facetName] || []; }, /** * Remove all the numeric filter for a given (attribute, operator) * @method * @param {string} attribute attribute to set the filter on * @param {string} [operator] operator of the filter (possible values: =, >, >=, <, <=, !=) * @param {number} [number] the value to be removed * @return {SearchParameters} new instance */ removeNumericRefinement: function (attribute, operator, number) { var paramValue = number; if (paramValue !== undefined) { if (!this.isNumericRefined(attribute, operator, paramValue)) { return this; } return this.setQueryParameters({ numericRefinements: this._clearNumericRefinements(function ( value, key ) { return ( key === attribute && value.op === operator && isEqualNumericRefinement(value.val, valToNumber(paramValue)) ); }), }); } else if (operator !== undefined) { if (!this.isNumericRefined(attribute, operator)) return this; return this.setQueryParameters({ numericRefinements: this._clearNumericRefinements(function ( value, key ) { return key === attribute && value.op === operator; }), }); } if (!this.isNumericRefined(attribute)) return this; return this.setQueryParameters({ numericRefinements: this._clearNumericRefinements(function (value, key) { return key === attribute; }), }); }, /** * Get the list of numeric refinements for a single facet * @param {string} facetName name of the attribute used for faceting * @return {SearchParameters.OperatorList} list of refinements */ getNumericRefinements: function (facetName) { return this.numericRefinements[facetName] || {}; }, /** * Return the current refinement for the (attribute, operator) * @param {string} attribute attribute in the record * @param {string} operator operator applied on the refined values * @return {Array.} refined values */ getNumericRefinement: function (attribute, operator) { return ( this.numericRefinements[attribute] && this.numericRefinements[attribute][operator] ); }, /** * Clear numeric filters. * @method * @private * @param {string|SearchParameters.clearCallback} [attribute] optional string or function * - If not given, means to clear all the filters. * - If `string`, means to clear all refinements for the `attribute` named filter. * - If `function`, means to clear all the refinements that return truthy values. * @return {Object.} new numeric refinements */ _clearNumericRefinements: function _clearNumericRefinements(attribute) { if (attribute === undefined) { if (!objectHasKeys(this.numericRefinements)) { return this.numericRefinements; } return {}; } else if (typeof attribute === 'string') { return omit(this.numericRefinements, [attribute]); } else if (typeof attribute === 'function') { var hasChanged = false; var numericRefinements = this.numericRefinements; var newNumericRefinements = Object.keys(numericRefinements).reduce( function (memo, key) { var operators = numericRefinements[key]; var operatorList = {}; operators = operators || {}; Object.keys(operators).forEach(function (operator) { var values = operators[operator] || []; var outValues = []; values.forEach(function (value) { var predicateResult = attribute( { val: value, op: operator }, key, 'numeric' ); if (!predicateResult) outValues.push(value); }); if (outValues.length !== values.length) { hasChanged = true; } operatorList[operator] = outValues; }); memo[key] = operatorList; return memo; }, {} ); if (hasChanged) return newNumericRefinements; return this.numericRefinements; } // 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; }, /** * Add a facet to the facets attribute of the helper configuration, if it * isn't already present. * @method * @param {string} facet facet name to add * @return {SearchParameters} new instance */ addFacet: function addFacet(facet) { if (this.isConjunctiveFacet(facet)) { return this; } return this.setQueryParameters({ facets: this.facets.concat([facet]), }); }, /** * Add a disjunctive facet to the disjunctiveFacets attribute of the helper * configuration, if it isn't already present. * @method * @param {string} facet disjunctive facet name to add * @return {SearchParameters} new instance */ addDisjunctiveFacet: function addDisjunctiveFacet(facet) { if (this.isDisjunctiveFacet(facet)) { return this; } return this.setQueryParameters({ disjunctiveFacets: this.disjunctiveFacets.concat([facet]), }); }, /** * Add a hierarchical facet to the hierarchicalFacets attribute of the helper * configuration. * @method * @param {object} hierarchicalFacet hierarchical facet to add * @return {SearchParameters} new instance * @throws will throw an error if a hierarchical facet with the same name was already declared */ addHierarchicalFacet: function addHierarchicalFacet(hierarchicalFacet) { if (this.isHierarchicalFacet(hierarchicalFacet.name)) { throw new Error( 'Cannot declare two hierarchical facets with the same name: `' + hierarchicalFacet.name + '`' ); } return this.setQueryParameters({ hierarchicalFacets: this.hierarchicalFacets.concat([hierarchicalFacet]), }); }, /** * Add a refinement on a "normal" facet * @method * @param {string} facet attribute to apply the faceting on * @param {string} value value of the attribute (will be converted to string) * @return {SearchParameters} new instance */ addFacetRefinement: function addFacetRefinement(facet, value) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } if (RefinementList.isRefined(this.facetsRefinements, facet, value)) return this; return this.setQueryParameters({ facetsRefinements: RefinementList.addRefinement( this.facetsRefinements, facet, value ), }); }, /** * Exclude a value from a "normal" facet * @method * @param {string} facet attribute to apply the exclusion on * @param {string} value value of the attribute (will be converted to string) * @return {SearchParameters} new instance */ addExcludeRefinement: function addExcludeRefinement(facet, value) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } if (RefinementList.isRefined(this.facetsExcludes, facet, value)) return this; return this.setQueryParameters({ facetsExcludes: RefinementList.addRefinement( this.facetsExcludes, facet, value ), }); }, /** * Adds a refinement on a disjunctive facet. * @method * @param {string} facet attribute to apply the faceting on * @param {string} value value of the attribute (will be converted to string) * @return {SearchParameters} new instance */ addDisjunctiveFacetRefinement: function addDisjunctiveFacetRefinement( facet, value ) { if (!this.isDisjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the disjunctiveFacets attribute of the helper configuration' ); } if ( RefinementList.isRefined(this.disjunctiveFacetsRefinements, facet, value) ) return this; return this.setQueryParameters({ disjunctiveFacetsRefinements: RefinementList.addRefinement( this.disjunctiveFacetsRefinements, facet, value ), }); }, /** * addTagRefinement adds a tag to the list used to filter the results * @param {string} tag tag to be added * @return {SearchParameters} new instance */ addTagRefinement: function addTagRefinement(tag) { if (this.isTagRefined(tag)) return this; var modification = { tagRefinements: this.tagRefinements.concat(tag), }; return this.setQueryParameters(modification); }, /** * Remove a facet from the facets attribute of the helper configuration, if it * is present. * @method * @param {string} facet facet name to remove * @return {SearchParameters} new instance */ removeFacet: function removeFacet(facet) { if (!this.isConjunctiveFacet(facet)) { return this; } return this.clearRefinements(facet).setQueryParameters({ facets: this.facets.filter(function (f) { return f !== facet; }), }); }, /** * Remove a disjunctive facet from the disjunctiveFacets attribute of the * helper configuration, if it is present. * @method * @param {string} facet disjunctive facet name to remove * @return {SearchParameters} new instance */ removeDisjunctiveFacet: function removeDisjunctiveFacet(facet) { if (!this.isDisjunctiveFacet(facet)) { return this; } return this.clearRefinements(facet).setQueryParameters({ disjunctiveFacets: this.disjunctiveFacets.filter(function (f) { return f !== facet; }), }); }, /** * Remove a hierarchical facet from the hierarchicalFacets attribute of the * helper configuration, if it is present. * @method * @param {string} facet hierarchical facet name to remove * @return {SearchParameters} new instance */ removeHierarchicalFacet: function removeHierarchicalFacet(facet) { if (!this.isHierarchicalFacet(facet)) { return this; } return this.clearRefinements(facet).setQueryParameters({ hierarchicalFacets: this.hierarchicalFacets.filter(function (f) { return f.name !== facet; }), }); }, /** * Remove a refinement set on facet. If a value is provided, it will clear the * refinement for the given value, otherwise it will clear all the refinement * values for the faceted attribute. * @method * @param {string} facet name of the attribute used for faceting * @param {string} [value] value used to filter * @return {SearchParameters} new instance */ removeFacetRefinement: function removeFacetRefinement(facet, value) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } if (!RefinementList.isRefined(this.facetsRefinements, facet, value)) return this; return this.setQueryParameters({ facetsRefinements: RefinementList.removeRefinement( this.facetsRefinements, facet, value ), }); }, /** * Remove a negative refinement on a facet * @method * @param {string} facet name of the attribute used for faceting * @param {string} value value used to filter * @return {SearchParameters} new instance */ removeExcludeRefinement: function removeExcludeRefinement(facet, value) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } if (!RefinementList.isRefined(this.facetsExcludes, facet, value)) return this; return this.setQueryParameters({ facetsExcludes: RefinementList.removeRefinement( this.facetsExcludes, facet, value ), }); }, /** * Remove a refinement on a disjunctive facet * @method * @param {string} facet name of the attribute used for faceting * @param {string} value value used to filter * @return {SearchParameters} new instance */ removeDisjunctiveFacetRefinement: function removeDisjunctiveFacetRefinement( facet, value ) { if (!this.isDisjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the disjunctiveFacets attribute of the helper configuration' ); } if ( !RefinementList.isRefined(this.disjunctiveFacetsRefinements, facet, value) ) return this; return this.setQueryParameters({ disjunctiveFacetsRefinements: RefinementList.removeRefinement( this.disjunctiveFacetsRefinements, facet, value ), }); }, /** * Remove a tag from the list of tag refinements * @method * @param {string} tag the tag to remove * @return {SearchParameters} new instance */ removeTagRefinement: function removeTagRefinement(tag) { if (!this.isTagRefined(tag)) return this; var modification = { tagRefinements: this.tagRefinements.filter(function (t) { return t !== tag; }), }; return this.setQueryParameters(modification); }, /** * Generic toggle refinement method to use with facet, disjunctive facets * and hierarchical facets * @param {string} facet the facet to refine * @param {string} value the associated value * @return {SearchParameters} new instance * @throws will throw an error if the facet is not declared in the settings of the helper * @deprecated since version 2.19.0, see {@link SearchParameters#toggleFacetRefinement} */ toggleRefinement: function toggleRefinement(facet, value) { return this.toggleFacetRefinement(facet, value); }, /** * Generic toggle refinement method to use with facet, disjunctive facets * and hierarchical facets * @param {string} facet the facet to refine * @param {string} value the associated value * @return {SearchParameters} new instance * @throws will throw an error if the facet is not declared in the settings of the helper */ toggleFacetRefinement: function toggleFacetRefinement(facet, value) { if (this.isHierarchicalFacet(facet)) { return this.toggleHierarchicalFacetRefinement(facet, value); } else if (this.isConjunctiveFacet(facet)) { return this.toggleConjunctiveFacetRefinement(facet, value); } else if (this.isDisjunctiveFacet(facet)) { return this.toggleDisjunctiveFacetRefinement(facet, value); } throw new Error( 'Cannot refine the undeclared facet ' + facet + '; it should be added to the helper options facets, disjunctiveFacets or hierarchicalFacets' ); }, /** * Switch the refinement applied over a facet/value * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {SearchParameters} new instance */ toggleConjunctiveFacetRefinement: function toggleConjunctiveFacetRefinement( facet, value ) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } return this.setQueryParameters({ facetsRefinements: RefinementList.toggleRefinement( this.facetsRefinements, facet, value ), }); }, /** * Switch the refinement applied over a facet/value * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {SearchParameters} new instance */ toggleExcludeFacetRefinement: function toggleExcludeFacetRefinement( facet, value ) { if (!this.isConjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the facets attribute of the helper configuration' ); } return this.setQueryParameters({ facetsExcludes: RefinementList.toggleRefinement( this.facetsExcludes, facet, value ), }); }, /** * Switch the refinement applied over a facet/value * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {SearchParameters} new instance */ toggleDisjunctiveFacetRefinement: function toggleDisjunctiveFacetRefinement( facet, value ) { if (!this.isDisjunctiveFacet(facet)) { throw new Error( facet + ' is not defined in the disjunctiveFacets attribute of the helper configuration' ); } return this.setQueryParameters({ disjunctiveFacetsRefinements: RefinementList.toggleRefinement( this.disjunctiveFacetsRefinements, facet, value ), }); }, /** * Switch the refinement applied over a facet/value * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {SearchParameters} new instance */ toggleHierarchicalFacetRefinement: function toggleHierarchicalFacetRefinement( facet, value ) { if (!this.isHierarchicalFacet(facet)) { throw new Error( facet + ' is not defined in the hierarchicalFacets attribute of the helper configuration' ); } var separator = this._getHierarchicalFacetSeparator( this.getHierarchicalFacetByName(facet) ); var mod = {}; var upOneOrMultipleLevel = this.hierarchicalFacetsRefinements[facet] !== undefined && this.hierarchicalFacetsRefinements[facet].length > 0 && // remove current refinement: // refinement was 'beer > IPA', call is toggleRefine('beer > IPA'), refinement should be `beer` (this.hierarchicalFacetsRefinements[facet][0] === value || // remove a parent refinement of the current refinement: // - refinement was 'beer > IPA > Flying dog' // - call is toggleRefine('beer > IPA') // - refinement should be `beer` this.hierarchicalFacetsRefinements[facet][0].indexOf( value + separator ) === 0); if (upOneOrMultipleLevel) { if (value.indexOf(separator) === -1) { // go back to root level mod[facet] = []; } else { mod[facet] = [value.slice(0, value.lastIndexOf(separator))]; } } else { mod[facet] = [value]; } return this.setQueryParameters({ hierarchicalFacetsRefinements: defaultsPure( {}, mod, this.hierarchicalFacetsRefinements ), }); }, /** * Adds a refinement on a hierarchical facet. * @param {string} facet the facet name * @param {string} path the hierarchical facet path * @return {SearchParameter} the new state * @throws Error if the facet is not defined or if the facet is refined */ addHierarchicalFacetRefinement: function (facet, path) { if (this.isHierarchicalFacetRefined(facet)) { throw new Error(facet + ' is already refined.'); } if (!this.isHierarchicalFacet(facet)) { throw new Error( facet + ' is not defined in the hierarchicalFacets attribute of the helper configuration.' ); } var mod = {}; mod[facet] = [path]; return this.setQueryParameters({ hierarchicalFacetsRefinements: defaultsPure( {}, mod, this.hierarchicalFacetsRefinements ), }); }, /** * Removes the refinement set on a hierarchical facet. * @param {string} facet the facet name * @return {SearchParameter} the new state * @throws Error if the facet is not defined or if the facet is not refined */ removeHierarchicalFacetRefinement: function (facet) { if (!this.isHierarchicalFacetRefined(facet)) { return this; } var mod = {}; mod[facet] = []; return this.setQueryParameters({ hierarchicalFacetsRefinements: defaultsPure( {}, mod, this.hierarchicalFacetsRefinements ), }); }, /** * Switch the tag refinement * @method * @param {string} tag the tag to remove or add * @return {SearchParameters} new instance */ toggleTagRefinement: function toggleTagRefinement(tag) { if (this.isTagRefined(tag)) { return this.removeTagRefinement(tag); } return this.addTagRefinement(tag); }, /** * Test if the facet name is from one of the disjunctive facets * @method * @param {string} facet facet name to test * @return {boolean} true if facet is a disjunctive facet */ isDisjunctiveFacet: function (facet) { return this.disjunctiveFacets.indexOf(facet) > -1; }, /** * Test if the facet name is from one of the hierarchical facets * @method * @param {string} facetName facet name to test * @return {boolean} true if facetName is a hierarchical facet */ isHierarchicalFacet: function (facetName) { return this.getHierarchicalFacetByName(facetName) !== undefined; }, /** * Test if the facet name is from one of the conjunctive/normal facets * @method * @param {string} facet facet name to test * @return {boolean} true if facet is a conjunctive facet */ isConjunctiveFacet: function (facet) { return this.facets.indexOf(facet) > -1; }, /** * Returns true if the facet is refined, either for a specific value or in * general. * @method * @param {string} facet name of the attribute for used for faceting * @param {string} value, optional value. If passed will test that this value * is filtering the given facet. * @return {boolean} returns true if refined */ isFacetRefined: function isFacetRefined(facet, value) { if (!this.isConjunctiveFacet(facet)) { return false; } return RefinementList.isRefined(this.facetsRefinements, facet, value); }, /** * Returns true if the facet contains exclusions or if a specific value is * excluded. * * @method * @param {string} facet name of the attribute for used for faceting * @param {string} [value] optional value. If passed will test that this value * is filtering the given facet. * @return {boolean} returns true if refined */ isExcludeRefined: function isExcludeRefined(facet, value) { if (!this.isConjunctiveFacet(facet)) { return false; } return RefinementList.isRefined(this.facetsExcludes, facet, value); }, /** * Returns true if the facet contains a refinement, or if a value passed is a * refinement for the facet. * @method * @param {string} facet name of the attribute for used for faceting * @param {string} value optional, will test if the value is used for refinement * if there is one, otherwise will test if the facet contains any refinement * @return {boolean} true if the facet is refined */ isDisjunctiveFacetRefined: function isDisjunctiveFacetRefined(facet, value) { if (!this.isDisjunctiveFacet(facet)) { return false; } return RefinementList.isRefined( this.disjunctiveFacetsRefinements, facet, value ); }, /** * Returns true if the facet contains a refinement, or if a value passed is a * refinement for the facet. * @method * @param {string} facet name of the attribute for used for faceting * @param {string} value optional, will test if the value is used for refinement * if there is one, otherwise will test if the facet contains any refinement * @return {boolean} true if the facet is refined */ isHierarchicalFacetRefined: function isHierarchicalFacetRefined( facet, value ) { if (!this.isHierarchicalFacet(facet)) { return false; } var refinements = this.getHierarchicalRefinement(facet); if (!value) { return refinements.length > 0; } return refinements.indexOf(value) !== -1; }, /** * Test if the triple (attribute, operator, value) is already refined. * If only the attribute and the operator are provided, it tests if the * contains any refinement value. * @method * @param {string} attribute attribute for which the refinement is applied * @param {string} [operator] operator of the refinement * @param {string} [value] value of the refinement * @return {boolean} true if it is refined */ isNumericRefined: function isNumericRefined(attribute, operator, value) { if (value === undefined && operator === undefined) { return Boolean(this.numericRefinements[attribute]); } var isOperatorDefined = this.numericRefinements[attribute] && this.numericRefinements[attribute][operator] !== undefined; if (value === undefined || !isOperatorDefined) { return isOperatorDefined; } var parsedValue = valToNumber(value); var isAttributeValueDefined = findArray(this.numericRefinements[attribute][operator], parsedValue) !== undefined; return isOperatorDefined && isAttributeValueDefined; }, /** * Returns true if the tag refined, false otherwise * @method * @param {string} tag the tag to check * @return {boolean} true if tag is refined */ isTagRefined: function isTagRefined(tag) { return this.tagRefinements.indexOf(tag) !== -1; }, /** * Returns the list of all disjunctive facets refined * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {string[]} returns the list of refinements */ getRefinedDisjunctiveFacets: function getRefinedDisjunctiveFacets() { // eslint-disable-next-line consistent-this var self = this; // attributes used for numeric filter can also be disjunctive var disjunctiveNumericRefinedFacets = intersection( Object.keys(this.numericRefinements).filter(function (facet) { return Object.keys(self.numericRefinements[facet]).length > 0; }), this.disjunctiveFacets ); return Object.keys(this.disjunctiveFacetsRefinements) .filter(function (facet) { return self.disjunctiveFacetsRefinements[facet].length > 0; }) .concat(disjunctiveNumericRefinedFacets) .concat(this.getRefinedHierarchicalFacets()) .sort(); }, /** * Returns the list of all disjunctive facets refined * @method * @param {string} facet name of the attribute used for faceting * @param {value} value value used for filtering * @return {string[]} returns the list of refinements */ getRefinedHierarchicalFacets: function getRefinedHierarchicalFacets() { // eslint-disable-next-line consistent-this var self = this; return intersection( // enforce the order between the two arrays, // so that refinement name index === hierarchical facet index this.hierarchicalFacets.map(function (facet) { return facet.name; }), Object.keys(this.hierarchicalFacetsRefinements).filter(function (facet) { return self.hierarchicalFacetsRefinements[facet].length > 0; }) ).sort(); }, /** * Returned the list of all disjunctive facets not refined * @method * @return {string[]} returns the list of facets that are not refined */ getUnrefinedDisjunctiveFacets: function () { var refinedFacets = this.getRefinedDisjunctiveFacets(); return this.disjunctiveFacets.filter(function (f) { return refinedFacets.indexOf(f) === -1; }); }, managedParameters: [ 'index', 'facets', 'disjunctiveFacets', 'facetsRefinements', 'hierarchicalFacets', 'facetsExcludes', 'disjunctiveFacetsRefinements', 'numericRefinements', 'tagRefinements', 'hierarchicalFacetsRefinements', ], getQueryParams: function getQueryParams() { var managedParameters = this.managedParameters; var queryParams = {}; // eslint-disable-next-line consistent-this var self = this; Object.keys(this).forEach(function (paramName) { var paramValue = self[paramName]; if ( managedParameters.indexOf(paramName) === -1 && paramValue !== undefined ) { queryParams[paramName] = paramValue; } }); return queryParams; }, /** * Let the user set a specific value for a given parameter. Will return the * same instance if the parameter is invalid or if the value is the same as the * previous one. * @method * @param {string} parameter the parameter name * @param {any} value the value to be set, must be compliant with the definition * of the attribute on the object * @return {SearchParameters} the updated state */ setQueryParameter: function setParameter(parameter, value) { if (this[parameter] === value) return this; var modification = {}; modification[parameter] = value; return this.setQueryParameters(modification); }, /** * Let the user set any of the parameters with a plain object. * @method * @param {object} params all the keys and the values to be updated * @return {SearchParameters} a new updated instance */ setQueryParameters: function setQueryParameters(params) { if (!params) return this; var error = SearchParameters.validate(this, params); if (error) { throw error; } // eslint-disable-next-line consistent-this var self = this; var nextWithNumbers = SearchParameters._parseNumbers(params); var previousPlainObject = Object.keys(this).reduce(function (acc, key) { acc[key] = self[key]; return acc; }, {}); var nextPlainObject = Object.keys(nextWithNumbers).reduce(function ( previous, key ) { var isPreviousValueDefined = previous[key] !== undefined; var isNextValueDefined = nextWithNumbers[key] !== undefined; if (isPreviousValueDefined && !isNextValueDefined) { return omit(previous, [key]); } if (isNextValueDefined) { previous[key] = nextWithNumbers[key]; } return previous; }, previousPlainObject); return new this.constructor(nextPlainObject); }, /** * Returns a new instance with the page reset. Two scenarios possible: * the page is omitted -> return the given instance * the page is set -> return a new instance with a page of 0 * @return {SearchParameters} a new updated instance */ resetPage: function () { if (this.page === undefined) { return this; } return this.setPage(0); }, /** * Helper function to get the hierarchicalFacet separator or the default one (`>`) * @param {object} hierarchicalFacet the hierarchicalFacet object * @return {string} returns the hierarchicalFacet.separator or `>` as default */ _getHierarchicalFacetSortBy: function (hierarchicalFacet) { return hierarchicalFacet.sortBy || ['isRefined:desc', 'name:asc']; }, /** * Helper function to get the hierarchicalFacet separator or the default one (`>`) * @private * @param {object} hierarchicalFacet the hierarchicalFacet object * @return {string} returns the hierarchicalFacet.separator or `>` as default */ _getHierarchicalFacetSeparator: function (hierarchicalFacet) { return hierarchicalFacet.separator || ' > '; }, /** * Helper function to get the hierarchicalFacet prefix path or null * @private * @param {object} hierarchicalFacet the hierarchicalFacet object * @return {string} returns the hierarchicalFacet.rootPath or null as default */ _getHierarchicalRootPath: function (hierarchicalFacet) { return hierarchicalFacet.rootPath || null; }, /** * Helper function to check if we show the parent level of the hierarchicalFacet * @private * @param {object} hierarchicalFacet the hierarchicalFacet object * @return {string} returns the hierarchicalFacet.showParentLevel or true as default */ _getHierarchicalShowParentLevel: function (hierarchicalFacet) { if (typeof hierarchicalFacet.showParentLevel === 'boolean') { return hierarchicalFacet.showParentLevel; } return true; }, /** * Helper function to get the hierarchicalFacet by it's name * @param {string} hierarchicalFacetName the hierarchicalFacet name * @return {object} a hierarchicalFacet */ getHierarchicalFacetByName: function (hierarchicalFacetName) { return find(this.hierarchicalFacets, function (f) { return f.name === hierarchicalFacetName; }); }, /** * Get the current breadcrumb for a hierarchical facet, as an array * @param {string} facetName Hierarchical facet name * @return {array.} the path as an array of string */ getHierarchicalFacetBreadcrumb: function (facetName) { if (!this.isHierarchicalFacet(facetName)) { return []; } var refinement = this.getHierarchicalRefinement(facetName)[0]; if (!refinement) return []; var separator = this._getHierarchicalFacetSeparator( this.getHierarchicalFacetByName(facetName) ); var path = refinement.split(separator); return path.map(function (part) { return part.trim(); }); }, toString: function () { return JSON.stringify(this, null, 2); }, }; /** * Callback used for clearRefinement method * @callback SearchParameters.clearCallback * @param {OperatorList|FacetList} value the value of the filter * @param {string} key the current attribute name * @param {string} type `numeric`, `disjunctiveFacet`, `conjunctiveFacet`, `hierarchicalFacet` or `exclude` * depending on the type of facet * @return {boolean} `true` if the element should be removed. `false` otherwise. */ module.exports = SearchParameters; },{"../functions/defaultsPure":10,"../functions/find":12,"../functions/intersection":16,"../functions/merge":17,"../functions/objectHasKeys":18,"../functions/omit":19,"../functions/valToNumber":21,"../utils/isValidUserToken":23,"./RefinementList":4}],6:[function(require,module,exports){ 'use strict'; module.exports = generateTrees; var fv = require('../functions/escapeFacetValue'); var find = require('../functions/find'); var prepareHierarchicalFacetSortBy = require('../functions/formatSort'); var orderBy = require('../functions/orderBy'); var escapeFacetValue = fv.escapeFacetValue; var unescapeFacetValue = fv.unescapeFacetValue; function generateTrees(state) { return function generate(hierarchicalFacetResult, hierarchicalFacetIndex) { var hierarchicalFacet = state.hierarchicalFacets[hierarchicalFacetIndex]; var hierarchicalFacetRefinement = (state.hierarchicalFacetsRefinements[hierarchicalFacet.name] && state.hierarchicalFacetsRefinements[hierarchicalFacet.name][0]) || ''; var hierarchicalSeparator = state._getHierarchicalFacetSeparator(hierarchicalFacet); var hierarchicalRootPath = state._getHierarchicalRootPath(hierarchicalFacet); var hierarchicalShowParentLevel = state._getHierarchicalShowParentLevel(hierarchicalFacet); var sortBy = prepareHierarchicalFacetSortBy( state._getHierarchicalFacetSortBy(hierarchicalFacet) ); var rootExhaustive = hierarchicalFacetResult.every(function (facetResult) { return facetResult.exhaustive; }); var generateTreeFn = generateHierarchicalTree( sortBy, hierarchicalSeparator, hierarchicalRootPath, hierarchicalShowParentLevel, hierarchicalFacetRefinement ); var results = hierarchicalFacetResult; if (hierarchicalRootPath) { results = hierarchicalFacetResult.slice( hierarchicalRootPath.split(hierarchicalSeparator).length ); } return results.reduce(generateTreeFn, { name: state.hierarchicalFacets[hierarchicalFacetIndex].name, count: null, // root level, no count isRefined: true, // root level, always refined path: null, // root level, no path escapedValue: null, exhaustive: rootExhaustive, data: null, }); }; } function generateHierarchicalTree( sortBy, hierarchicalSeparator, hierarchicalRootPath, hierarchicalShowParentLevel, currentRefinement ) { return function generateTree( hierarchicalTree, hierarchicalFacetResult, currentHierarchicalLevel ) { var parent = hierarchicalTree; if (currentHierarchicalLevel > 0) { var level = 0; parent = hierarchicalTree; while (level < currentHierarchicalLevel) { /** * @type {object[]]} hierarchical data */ var data = parent && Array.isArray(parent.data) ? parent.data : []; parent = find(data, function (subtree) { return subtree.isRefined; }); level++; } } // we found a refined parent, let's add current level data under it if (parent) { // filter values in case an object has multiple categories: // { // categories: { // level0: ['beers', 'bières'], // level1: ['beers > IPA', 'bières > Belges'] // } // } // // If parent refinement is `beers`, then we do not want to have `bières > Belges` // showing up var picked = Object.keys(hierarchicalFacetResult.data) .map(function (facetValue) { return [facetValue, hierarchicalFacetResult.data[facetValue]]; }) .filter(function (tuple) { var facetValue = tuple[0]; return onlyMatchingTree( facetValue, parent.path || hierarchicalRootPath, currentRefinement, hierarchicalSeparator, hierarchicalRootPath, hierarchicalShowParentLevel ); }); parent.data = orderBy( picked.map(function (tuple) { var facetValue = tuple[0]; var facetCount = tuple[1]; return format( facetCount, facetValue, hierarchicalSeparator, unescapeFacetValue(currentRefinement), hierarchicalFacetResult.exhaustive ); }), sortBy[0], sortBy[1] ); } return hierarchicalTree; }; } // eslint-disable-next-line max-params function onlyMatchingTree( facetValue, parentPath, currentRefinement, hierarchicalSeparator, hierarchicalRootPath, hierarchicalShowParentLevel ) { // we want the facetValue is a child of hierarchicalRootPath if ( hierarchicalRootPath && (facetValue.indexOf(hierarchicalRootPath) !== 0 || hierarchicalRootPath === facetValue) ) { return false; } // we always want root levels (only when there is no prefix path) return ( (!hierarchicalRootPath && facetValue.indexOf(hierarchicalSeparator) === -1) || // if there is a rootPath, being root level mean 1 level under rootPath (hierarchicalRootPath && facetValue.split(hierarchicalSeparator).length - hierarchicalRootPath.split(hierarchicalSeparator).length === 1) || // if current refinement is a root level and current facetValue is a root level, // keep the facetValue (facetValue.indexOf(hierarchicalSeparator) === -1 && currentRefinement.indexOf(hierarchicalSeparator) === -1) || // currentRefinement is a child of the facet value currentRefinement.indexOf(facetValue) === 0 || // facetValue is a child of the current parent, add it (facetValue.indexOf(parentPath + hierarchicalSeparator) === 0 && (hierarchicalShowParentLevel || facetValue.indexOf(currentRefinement) === 0)) ); } function format( facetCount, facetValue, hierarchicalSeparator, currentRefinement, exhaustive ) { var parts = facetValue.split(hierarchicalSeparator); return { name: parts[parts.length - 1].trim(), path: facetValue, escapedValue: escapeFacetValue(facetValue), count: facetCount, isRefined: currentRefinement === facetValue || currentRefinement.indexOf(facetValue + hierarchicalSeparator) === 0, exhaustive: exhaustive, data: null, }; } },{"../functions/escapeFacetValue":11,"../functions/find":12,"../functions/formatSort":14,"../functions/orderBy":20}],7:[function(require,module,exports){ 'use strict'; var compact = require('../functions/compact'); var defaultsPure = require('../functions/defaultsPure'); var fv = require('../functions/escapeFacetValue'); var find = require('../functions/find'); var findIndex = require('../functions/findIndex'); var formatSort = require('../functions/formatSort'); var merge = require('../functions/merge'); var orderBy = require('../functions/orderBy'); var escapeFacetValue = fv.escapeFacetValue; var unescapeFacetValue = fv.unescapeFacetValue; var generateHierarchicalTree = require('./generate-hierarchical-tree'); /** * @typedef SearchResults.Facet * @type {object} * @property {string} name name of the attribute in the record * @property {object} data the faceting data: value, number of entries * @property {object} stats undefined unless facet_stats is retrieved from algolia */ /** * @typedef SearchResults.HierarchicalFacet * @type {object} * @property {string} name name of the current value given the hierarchical level, trimmed. * If root node, you get the facet name * @property {number} count number of objects matching this hierarchical value * @property {string} path the current hierarchical value full path * @property {boolean} isRefined `true` if the current value was refined, `false` otherwise * @property {HierarchicalFacet[]} data sub values for the current level */ /** * @typedef SearchResults.FacetValue * @type {object} * @property {string} name the facet value itself * @property {number} count times this facet appears in the results * @property {boolean} isRefined is the facet currently selected * @property {boolean} isExcluded is the facet currently excluded (only for conjunctive facets) */ /** * @typedef Refinement * @type {object} * @property {string} type the type of filter used: * `numeric`, `facet`, `exclude`, `disjunctive`, `hierarchical` * @property {string} attributeName name of the attribute used for filtering * @property {string} name the value of the filter * @property {number} numericValue the value as a number. Only for numeric filters. * @property {string} operator the operator used. Only for numeric filters. * @property {number} count the number of computed hits for this filter. Only on facets. * @property {boolean} exhaustive if the count is exhaustive */ /** * Turn an array of attributes in an object of attributes with their position in the array as value * @param {string[]} attributes the list of attributes in the record * @return {object} the list of attributes indexed by attribute name */ function getIndices(attributes) { var indices = {}; attributes.forEach(function (val, idx) { indices[val] = idx; }); return indices; } function assignFacetStats(dest, facetStats, key) { if (facetStats && facetStats[key]) { dest.stats = facetStats[key]; } } /** * @typedef {Object} HierarchicalFacet * @property {string} name * @property {string[]} attributes */ /** * @param {HierarchicalFacet[]} hierarchicalFacets All hierarchical facets * @param {string} hierarchicalAttributeName The name of the hierarchical attribute * @return {HierarchicalFacet} The hierarchical facet matching the attribute name */ function findMatchingHierarchicalFacetFromAttributeName( hierarchicalFacets, hierarchicalAttributeName ) { return find( hierarchicalFacets, function facetKeyMatchesAttribute(hierarchicalFacet) { var facetNames = hierarchicalFacet.attributes || []; return facetNames.indexOf(hierarchicalAttributeName) > -1; } ); } /** * Constructor for SearchResults * @class * @classdesc SearchResults contains the results of a query to Algolia using the * {@link AlgoliaSearchHelper}. * @param {SearchParameters} state state that led to the response * @param {array.} results the results from algolia client * @param {object} options options to control results content * @example SearchResults of the first query in * the instant search demo { "hitsPerPage": 10, "processingTimeMS": 2, "facets": [ { "name": "type", "data": { "HardGood": 6627, "BlackTie": 550, "Music": 665, "Software": 131, "Game": 456, "Movie": 1571 }, "exhaustive": false }, { "exhaustive": false, "data": { "Free shipping": 5507 }, "name": "shipping" } ], "hits": [ { "thumbnailImage": "http://img.bbystatic.com/BestBuy_US/images/products/1688/1688832_54x108_s.gif", "_highlightResult": { "shortDescription": { "matchLevel": "none", "value": "Safeguard your PC, Mac, Android and iOS devices with comprehensive Internet protection", "matchedWords": [] }, "category": { "matchLevel": "none", "value": "Computer Security Software", "matchedWords": [] }, "manufacturer": { "matchedWords": [], "value": "Webroot", "matchLevel": "none" }, "name": { "value": "Webroot SecureAnywhere Internet Security (3-Device) (1-Year Subscription) - Mac/Windows", "matchedWords": [], "matchLevel": "none" } }, "image": "http://img.bbystatic.com/BestBuy_US/images/products/1688/1688832_105x210_sc.jpg", "shipping": "Free shipping", "bestSellingRank": 4, "shortDescription": "Safeguard your PC, Mac, Android and iOS devices with comprehensive Internet protection", "url": "http://www.bestbuy.com/site/webroot-secureanywhere-internet-security-3-devi…d=1219060687969&skuId=1688832&cmp=RMX&ky=2d3GfEmNIzjA0vkzveHdZEBgpPCyMnLTJ", "name": "Webroot SecureAnywhere Internet Security (3-Device) (1-Year Subscription) - Mac/Windows", "category": "Computer Security Software", "salePrice_range": "1 - 50", "objectID": "1688832", "type": "Software", "customerReviewCount": 5980, "salePrice": 49.99, "manufacturer": "Webroot" }, .... ], "nbHits": 10000, "disjunctiveFacets": [ { "exhaustive": false, "data": { "5": 183, "12": 112, "7": 149, ... }, "name": "customerReviewCount", "stats": { "max": 7461, "avg": 157.939, "min": 1 } }, { "data": { "Printer Ink": 142, "Wireless Speakers": 60, "Point & Shoot Cameras": 48, ... }, "name": "category", "exhaustive": false }, { "exhaustive": false, "data": { "> 5000": 2, "1 - 50": 6524, "501 - 2000": 566, "201 - 500": 1501, "101 - 200": 1360, "2001 - 5000": 47 }, "name": "salePrice_range" }, { "data": { "Dynex™": 202, "Insignia™": 230, "PNY": 72, ... }, "name": "manufacturer", "exhaustive": false } ], "query": "", "nbPages": 100, "page": 0, "index": "bestbuy" } **/ function SearchResults(state, results, options) { var mainSubResponse = results[0]; this._rawResults = results; // eslint-disable-next-line consistent-this var self = this; // https://www.algolia.com/doc/api-reference/api-methods/search/#response Object.keys(mainSubResponse).forEach(function (key) { self[key] = mainSubResponse[key]; }); // Make every key of the result options reachable from the instance var opts = merge( { persistHierarchicalRootCount: false, }, options ); Object.keys(opts).forEach(function (key) { self[key] = opts[key]; }); /** * query used to generate the results * @name query * @member {string} * @memberof SearchResults * @instance */ /** * The query as parsed by the engine given all the rules. * @name parsedQuery * @member {string} * @memberof SearchResults * @instance */ /** * all the records that match the search parameters. Each record is * augmented with a new attribute `_highlightResult` * which is an object keyed by attribute and with the following properties: * - `value` : the value of the facet highlighted (html) * - `matchLevel`: `full`, `partial` or `none`, depending on how the query terms match * @name hits * @member {object[]} * @memberof SearchResults * @instance */ /** * index where the results come from * @name index * @member {string} * @memberof SearchResults * @instance */ /** * number of hits per page requested * @name hitsPerPage * @member {number} * @memberof SearchResults * @instance */ /** * total number of hits of this query on the index * @name nbHits * @member {number} * @memberof SearchResults * @instance */ /** * total number of pages with respect to the number of hits per page and the total number of hits * @name nbPages * @member {number} * @memberof SearchResults * @instance */ /** * current page * @name page * @member {number} * @memberof SearchResults * @instance */ /** * The position if the position was guessed by IP. * @name aroundLatLng * @member {string} * @memberof SearchResults * @instance * @example "48.8637,2.3615", */ /** * The radius computed by Algolia. * @name automaticRadius * @member {string} * @memberof SearchResults * @instance * @example "126792922", */ /** * String identifying the server used to serve this request. * * getRankingInfo needs to be set to `true` for this to be returned * * @name serverUsed * @member {string} * @memberof SearchResults * @instance * @example "c7-use-2.algolia.net", */ /** * Boolean that indicates if the computation of the counts did time out. * @deprecated * @name timeoutCounts * @member {boolean} * @memberof SearchResults * @instance */ /** * Boolean that indicates if the computation of the hits did time out. * @deprecated * @name timeoutHits * @member {boolean} * @memberof SearchResults * @instance */ /** * True if the counts of the facets is exhaustive * @name exhaustiveFacetsCount * @member {boolean} * @memberof SearchResults * @instance */ /** * True if the number of hits is exhaustive * @name exhaustiveNbHits * @member {boolean} * @memberof SearchResults * @instance */ /** * Contains the userData if they are set by a [query rule](https://www.algolia.com/doc/guides/query-rules/query-rules-overview/). * @name userData * @member {object[]} * @memberof SearchResults * @instance */ /** * queryID is the unique identifier of the query used to generate the current search results. * This value is only available if the `clickAnalytics` search parameter is set to `true`. * @name queryID * @member {string} * @memberof SearchResults * @instance */ /** * sum of the processing time of all the queries * @name processingTimeMS * @member {number} * @memberof SearchResults * @instance */ this.processingTimeMS = results.reduce(function (sum, result) { return result.processingTimeMS === undefined ? sum : sum + result.processingTimeMS; }, 0); /** * disjunctive facets results * @member {SearchResults.Facet[]} */ this.disjunctiveFacets = []; /** * disjunctive facets results * @member {SearchResults.HierarchicalFacet[]} */ this.hierarchicalFacets = state.hierarchicalFacets.map( function initFutureTree() { return []; } ); /** * other facets results * @member {SearchResults.Facet[]} */ this.facets = []; var disjunctiveFacets = state.getRefinedDisjunctiveFacets(); var facetsIndices = getIndices(state.facets); var disjunctiveFacetsIndices = getIndices(state.disjunctiveFacets); var nextDisjunctiveResult = 1; // Since we send request only for disjunctive facets that have been refined, // we get the facets information from the first, general, response. var mainFacets = mainSubResponse.facets || {}; Object.keys(mainFacets).forEach(function (facetKey) { var facetValueObject = mainFacets[facetKey]; var hierarchicalFacet = findMatchingHierarchicalFacetFromAttributeName( state.hierarchicalFacets, facetKey ); if (hierarchicalFacet) { // Place the hierarchicalFacet data at the correct index depending on // the attributes order that was defined at the helper initialization var facetIndex = hierarchicalFacet.attributes.indexOf(facetKey); var idxAttributeName = findIndex(state.hierarchicalFacets, function (f) { return f.name === hierarchicalFacet.name; }); self.hierarchicalFacets[idxAttributeName][facetIndex] = { attribute: facetKey, data: facetValueObject, exhaustive: mainSubResponse.exhaustiveFacetsCount, }; } else { var isFacetDisjunctive = state.disjunctiveFacets.indexOf(facetKey) !== -1; var isFacetConjunctive = state.facets.indexOf(facetKey) !== -1; var position; if (isFacetDisjunctive) { position = disjunctiveFacetsIndices[facetKey]; self.disjunctiveFacets[position] = { name: facetKey, data: facetValueObject, exhaustive: mainSubResponse.exhaustiveFacetsCount, }; assignFacetStats( self.disjunctiveFacets[position], mainSubResponse.facets_stats, facetKey ); } if (isFacetConjunctive) { position = facetsIndices[facetKey]; self.facets[position] = { name: facetKey, data: facetValueObject, exhaustive: mainSubResponse.exhaustiveFacetsCount, }; assignFacetStats( self.facets[position], mainSubResponse.facets_stats, facetKey ); } } }); // Make sure we do not keep holes within the hierarchical facets this.hierarchicalFacets = compact(this.hierarchicalFacets); // aggregate the refined disjunctive facets disjunctiveFacets.forEach(function (disjunctiveFacet) { var result = results[nextDisjunctiveResult]; var facets = result && result.facets ? result.facets : {}; var hierarchicalFacet = state.getHierarchicalFacetByName(disjunctiveFacet); // There should be only item in facets. Object.keys(facets).forEach(function (dfacet) { var facetResults = facets[dfacet]; var position; if (hierarchicalFacet) { position = findIndex(state.hierarchicalFacets, function (f) { return f.name === hierarchicalFacet.name; }); var attributeIndex = findIndex( self.hierarchicalFacets[position], function (f) { return f.attribute === dfacet; } ); // previous refinements and no results so not able to find it if (attributeIndex === -1) { return; } self.hierarchicalFacets[position][attributeIndex].data = merge( {}, self.hierarchicalFacets[position][attributeIndex].data, facetResults ); } else { position = disjunctiveFacetsIndices[dfacet]; var dataFromMainRequest = (mainSubResponse.facets && mainSubResponse.facets[dfacet]) || {}; self.disjunctiveFacets[position] = { name: dfacet, data: defaultsPure({}, facetResults, dataFromMainRequest), exhaustive: result.exhaustiveFacetsCount, }; assignFacetStats( self.disjunctiveFacets[position], result.facets_stats, dfacet ); if (state.disjunctiveFacetsRefinements[dfacet]) { state.disjunctiveFacetsRefinements[dfacet].forEach(function ( refinementValue ) { // add the disjunctive refinements if it is no more retrieved if ( !self.disjunctiveFacets[position].data[refinementValue] && state.disjunctiveFacetsRefinements[dfacet].indexOf( unescapeFacetValue(refinementValue) ) > -1 ) { self.disjunctiveFacets[position].data[refinementValue] = 0; } }); } } }); nextDisjunctiveResult++; }); // if we have some parent level values for hierarchical facets, merge them state.getRefinedHierarchicalFacets().forEach(function (refinedFacet) { var hierarchicalFacet = state.getHierarchicalFacetByName(refinedFacet); var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet); var currentRefinement = state.getHierarchicalRefinement(refinedFacet); // if we are already at a root refinement (or no refinement at all), there is no // root level values request if ( currentRefinement.length === 0 || currentRefinement[0].split(separator).length < 2 ) { return; } results.slice(nextDisjunctiveResult).forEach(function (result) { var facets = result && result.facets ? result.facets : {}; Object.keys(facets).forEach(function (dfacet) { var facetResults = facets[dfacet]; var position = findIndex(state.hierarchicalFacets, function (f) { return f.name === hierarchicalFacet.name; }); var attributeIndex = findIndex( self.hierarchicalFacets[position], function (f) { return f.attribute === dfacet; } ); // previous refinements and no results so not able to find it if (attributeIndex === -1) { return; } // when we always get root levels, if the hits refinement is `beers > IPA` (count: 5), // then the disjunctive values will be `beers` (count: 100), // but we do not want to display // | beers (100) // > IPA (5) // We want // | beers (5) // > IPA (5) // @MAJOR: remove this legacy behaviour in next major version var defaultData = {}; if ( currentRefinement.length > 0 && !self.persistHierarchicalRootCount ) { var root = currentRefinement[0].split(separator)[0]; defaultData[root] = self.hierarchicalFacets[position][attributeIndex].data[root]; } self.hierarchicalFacets[position][attributeIndex].data = defaultsPure( defaultData, facetResults, self.hierarchicalFacets[position][attributeIndex].data ); }); nextDisjunctiveResult++; }); }); // add the excludes Object.keys(state.facetsExcludes).forEach(function (facetName) { var excludes = state.facetsExcludes[facetName]; var position = facetsIndices[facetName]; self.facets[position] = { name: facetName, data: mainFacets[facetName], exhaustive: mainSubResponse.exhaustiveFacetsCount, }; excludes.forEach(function (facetValue) { self.facets[position] = self.facets[position] || { name: facetName }; self.facets[position].data = self.facets[position].data || {}; self.facets[position].data[facetValue] = 0; }); }); /** * @type {Array} */ this.hierarchicalFacets = this.hierarchicalFacets.map( generateHierarchicalTree(state) ); /** * @type {Array} */ this.facets = compact(this.facets); /** * @type {Array} */ this.disjunctiveFacets = compact(this.disjunctiveFacets); this._state = state; } /** * Get a facet object with its name * @deprecated * @param {string} name name of the faceted attribute * @return {SearchResults.Facet} the facet object */ SearchResults.prototype.getFacetByName = function (name) { function predicate(facet) { return facet.name === name; } return ( find(this.facets, predicate) || find(this.disjunctiveFacets, predicate) || find(this.hierarchicalFacets, predicate) ); }; /** * Get the facet values of a specified attribute from a SearchResults object. * @private * @param {SearchResults} results the search results to search in * @param {string} attribute name of the faceted attribute to search for * @return {array|object} facet values. For the hierarchical facets it is an object. */ function extractNormalizedFacetValues(results, attribute) { function predicate(facet) { return facet.name === attribute; } if (results._state.isConjunctiveFacet(attribute)) { var facet = find(results.facets, predicate); if (!facet) return []; return Object.keys(facet.data).map(function (name) { var value = escapeFacetValue(name); return { name: name, escapedValue: value, count: facet.data[name], isRefined: results._state.isFacetRefined(attribute, value), isExcluded: results._state.isExcludeRefined(attribute, name), }; }); } else if (results._state.isDisjunctiveFacet(attribute)) { var disjunctiveFacet = find(results.disjunctiveFacets, predicate); if (!disjunctiveFacet) return []; return Object.keys(disjunctiveFacet.data).map(function (name) { var value = escapeFacetValue(name); return { name: name, escapedValue: value, count: disjunctiveFacet.data[name], isRefined: results._state.isDisjunctiveFacetRefined(attribute, value), }; }); } else if (results._state.isHierarchicalFacet(attribute)) { var hierarchicalFacetValues = find(results.hierarchicalFacets, predicate); if (!hierarchicalFacetValues) return hierarchicalFacetValues; var hierarchicalFacet = results._state.getHierarchicalFacetByName(attribute); var separator = results._state._getHierarchicalFacetSeparator(hierarchicalFacet); var currentRefinement = unescapeFacetValue( results._state.getHierarchicalRefinement(attribute)[0] || '' ); if (currentRefinement.indexOf(hierarchicalFacet.rootPath) === 0) { currentRefinement = currentRefinement.replace( hierarchicalFacet.rootPath + separator, '' ); } var currentRefinementSplit = currentRefinement.split(separator); currentRefinementSplit.unshift(attribute); setIsRefined(hierarchicalFacetValues, currentRefinementSplit, 0); return hierarchicalFacetValues; } return undefined; } /** * Set the isRefined of a hierarchical facet result based on the current state. * @param {SearchResults.HierarchicalFacet} item Hierarchical facet to fix * @param {string[]} currentRefinement array of parts of the current hierarchical refinement * @param {number} depth recursion depth in the currentRefinement * @return {undefined} function mutates the item */ function setIsRefined(item, currentRefinement, depth) { item.isRefined = item.name === (currentRefinement[depth] && currentRefinement[depth].trim()); if (item.data) { item.data.forEach(function (child) { setIsRefined(child, currentRefinement, depth + 1); }); } } /** * Sort nodes of a hierarchical or disjunctive facet results * @private * @param {function} sortFn sort function to apply * @param {HierarchicalFacet|Array} node node upon which we want to apply the sort * @param {string[]} names attribute names * @param {number} [level=0] current index in the names array * @return {HierarchicalFacet|Array} sorted node */ function recSort(sortFn, node, names, level) { level = level || 0; if (Array.isArray(node)) { return sortFn(node, names[level]); } if (!node.data || node.data.length === 0) { return node; } var children = node.data.map(function (childNode) { return recSort(sortFn, childNode, names, level + 1); }); var sortedChildren = sortFn(children, names[level]); var newNode = defaultsPure({ data: sortedChildren }, node); return newNode; } SearchResults.DEFAULT_SORT = ['isRefined:desc', 'count:desc', 'name:asc']; function vanillaSortFn(order, data) { return data.sort(order); } /** * @typedef FacetOrdering * @type {Object} * @property {string[]} [order] * @property {'count' | 'alpha' | 'hidden'} [sortRemainingBy] */ /** * Sorts facet arrays via their facet ordering * @param {Array} facetValues the values * @param {FacetOrdering} facetOrdering the ordering * @returns {Array} the sorted facet values */ function sortViaFacetOrdering(facetValues, facetOrdering) { var orderedFacets = []; var remainingFacets = []; var order = facetOrdering.order || []; /** * an object with the keys being the values in order, the values their index: * ['one', 'two'] -> { one: 0, two: 1 } */ var reverseOrder = order.reduce(function (acc, name, i) { acc[name] = i; return acc; }, {}); facetValues.forEach(function (item) { // hierarchical facets get sorted using their raw name var name = item.path || item.name; if (reverseOrder[name] !== undefined) { orderedFacets[reverseOrder[name]] = item; } else { remainingFacets.push(item); } }); orderedFacets = orderedFacets.filter(function (facet) { return facet; }); var sortRemainingBy = facetOrdering.sortRemainingBy; var ordering; if (sortRemainingBy === 'hidden') { return orderedFacets; } else if (sortRemainingBy === 'alpha') { ordering = [ ['path', 'name'], ['asc', 'asc'], ]; } else { ordering = [['count'], ['desc']]; } return orderedFacets.concat( orderBy(remainingFacets, ordering[0], ordering[1]) ); } /** * @param {SearchResults} results the search results class * @param {string} attribute the attribute to retrieve ordering of * @returns {FacetOrdering | undefined} the facet ordering */ function getFacetOrdering(results, attribute) { return ( results.renderingContent && results.renderingContent.facetOrdering && results.renderingContent.facetOrdering.values && results.renderingContent.facetOrdering.values[attribute] ); } /** * Get a the list of values for a given facet attribute. Those values are sorted * refinement first, descending count (bigger value on top), and name ascending * (alphabetical order). The sort formula can overridden using either string based * predicates or a function. * * This method will return all the values returned by the Algolia engine plus all * the values already refined. This means that it can happen that the * `maxValuesPerFacet` [configuration](https://www.algolia.com/doc/rest-api/search#param-maxValuesPerFacet) * might not be respected if you have facet values that are already refined. * @param {string} attribute attribute name * @param {object} opts configuration options. * @param {boolean} [opts.facetOrdering] * Force the use of facetOrdering from the result if a sortBy is present. If * sortBy isn't present, facetOrdering will be used automatically. * @param {Array. | function} opts.sortBy * When using strings, it consists of * the name of the [FacetValue](#SearchResults.FacetValue) or the * [HierarchicalFacet](#SearchResults.HierarchicalFacet) attributes with the * order (`asc` or `desc`). For example to order the value by count, the * argument would be `['count:asc']`. * * If only the attribute name is specified, the ordering defaults to the one * specified in the default value for this attribute. * * When not specified, the order is * ascending. This parameter can also be a function which takes two facet * values and should return a number, 0 if equal, 1 if the first argument is * bigger or -1 otherwise. * * The default value for this attribute `['isRefined:desc', 'count:desc', 'name:asc']` * @return {FacetValue[]|HierarchicalFacet|undefined} depending on the type of facet of * the attribute requested (hierarchical, disjunctive or conjunctive) * @example * helper.on('result', function(event){ * //get values ordered only by name ascending using the string predicate * event.results.getFacetValues('city', {sortBy: ['name:asc']}); * //get values ordered only by count ascending using a function * event.results.getFacetValues('city', { * // this is equivalent to ['count:asc'] * sortBy: function(a, b) { * if (a.count === b.count) return 0; * if (a.count > b.count) return 1; * if (b.count > a.count) return -1; * } * }); * }); */ SearchResults.prototype.getFacetValues = function (attribute, opts) { var facetValues = extractNormalizedFacetValues(this, attribute); if (!facetValues) { return undefined; } var options = defaultsPure({}, opts, { sortBy: SearchResults.DEFAULT_SORT, // if no sortBy is given, attempt to sort based on facetOrdering // if it is given, we still allow to sort via facet ordering first facetOrdering: !(opts && opts.sortBy), }); // eslint-disable-next-line consistent-this var results = this; var attributes; if (Array.isArray(facetValues)) { attributes = [attribute]; } else { var config = results._state.getHierarchicalFacetByName(facetValues.name); attributes = config.attributes; } return recSort( function (data, facetName) { if (options.facetOrdering) { var facetOrdering = getFacetOrdering(results, facetName); if (facetOrdering) { return sortViaFacetOrdering(data, facetOrdering); } } if (Array.isArray(options.sortBy)) { var order = formatSort(options.sortBy, SearchResults.DEFAULT_SORT); return orderBy(data, order[0], order[1]); } else if (typeof options.sortBy === 'function') { return vanillaSortFn(options.sortBy, data); } throw new Error( 'options.sortBy is optional but if defined it must be ' + 'either an array of string (predicates) or a sorting function' ); }, facetValues, attributes ); }; /** * Returns the facet stats if attribute is defined and the facet contains some. * Otherwise returns undefined. * @param {string} attribute name of the faceted attribute * @return {object} The stats of the facet */ SearchResults.prototype.getFacetStats = function (attribute) { if (this._state.isConjunctiveFacet(attribute)) { return getFacetStatsIfAvailable(this.facets, attribute); } else if (this._state.isDisjunctiveFacet(attribute)) { return getFacetStatsIfAvailable(this.disjunctiveFacets, attribute); } return undefined; }; /** * @typedef {Object} FacetListItem * @property {string} name */ /** * @param {FacetListItem[]} facetList (has more items, but enough for here) * @param {string} facetName The attribute to look for * @return {object|undefined} The stats of the facet */ function getFacetStatsIfAvailable(facetList, facetName) { var data = find(facetList, function (facet) { return facet.name === facetName; }); return data && data.stats; } /** * Returns all refinements for all filters + tags. It also provides * additional information: count and exhaustiveness for each filter. * * See the [refinement type](#Refinement) for an exhaustive view of the available * data. * * Note that for a numeric refinement, results are grouped per operator, this * means that it will return responses for operators which are empty. * * @return {Array.} all the refinements */ SearchResults.prototype.getRefinements = function () { var state = this._state; // eslint-disable-next-line consistent-this var results = this; var res = []; Object.keys(state.facetsRefinements).forEach(function (attributeName) { state.facetsRefinements[attributeName].forEach(function (name) { res.push( getRefinement(state, 'facet', attributeName, name, results.facets) ); }); }); Object.keys(state.facetsExcludes).forEach(function (attributeName) { state.facetsExcludes[attributeName].forEach(function (name) { res.push( getRefinement(state, 'exclude', attributeName, name, results.facets) ); }); }); Object.keys(state.disjunctiveFacetsRefinements).forEach(function ( attributeName ) { state.disjunctiveFacetsRefinements[attributeName].forEach(function (name) { res.push( getRefinement( state, 'disjunctive', attributeName, name, results.disjunctiveFacets ) ); }); }); Object.keys(state.hierarchicalFacetsRefinements).forEach(function ( attributeName ) { state.hierarchicalFacetsRefinements[attributeName].forEach(function (name) { res.push( getHierarchicalRefinement( state, attributeName, name, results.hierarchicalFacets ) ); }); }); Object.keys(state.numericRefinements).forEach(function (attributeName) { var operators = state.numericRefinements[attributeName]; Object.keys(operators).forEach(function (operator) { operators[operator].forEach(function (value) { res.push({ type: 'numeric', attributeName: attributeName, name: value, numericValue: value, operator: operator, }); }); }); }); state.tagRefinements.forEach(function (name) { res.push({ type: 'tag', attributeName: '_tags', name: name }); }); return res; }; /** * @typedef {Object} Facet * @property {string} name * @property {Object} data * @property {boolean} exhaustive */ /** * @param {SearchParameters} state the current state * @param {string} type the type of the refinement * @param {string} attributeName The attribute of the facet * @param {*} name The name of the facet * @param {Facet[]} resultsFacets facets from the results * @return {Refinement} the refinement */ function getRefinement(state, type, attributeName, name, resultsFacets) { var facet = find(resultsFacets, function (f) { return f.name === attributeName; }); var count = facet && facet.data && facet.data[name] ? facet.data[name] : 0; var exhaustive = (facet && facet.exhaustive) || false; return { type: type, attributeName: attributeName, name: name, count: count, exhaustive: exhaustive, }; } /** * @param {SearchParameters} state the current state * @param {string} attributeName the attribute of the hierarchical facet * @param {string} name the name of the facet * @param {Facet[]} resultsFacets facets from the results * @return {HierarchicalFacet} the hierarchical facet */ function getHierarchicalRefinement(state, attributeName, name, resultsFacets) { var facetDeclaration = state.getHierarchicalFacetByName(attributeName); var separator = state._getHierarchicalFacetSeparator(facetDeclaration); var split = name.split(separator); var rootFacet = find(resultsFacets, function (facet) { return facet.name === attributeName; }); var facet = split.reduce(function (intermediateFacet, part) { var newFacet = intermediateFacet && find(intermediateFacet.data, function (f) { return f.name === part; }); return newFacet !== undefined ? newFacet : intermediateFacet; }, rootFacet); var count = (facet && facet.count) || 0; var exhaustive = (facet && facet.exhaustive) || false; var path = (facet && facet.path) || ''; return { type: 'hierarchical', attributeName: attributeName, name: path, count: count, exhaustive: exhaustive, }; } module.exports = SearchResults; },{"../functions/compact":9,"../functions/defaultsPure":10,"../functions/escapeFacetValue":11,"../functions/find":12,"../functions/findIndex":13,"../functions/formatSort":14,"../functions/merge":17,"../functions/orderBy":20,"./generate-hierarchical-tree":6}],8:[function(require,module,exports){ 'use strict'; var EventEmitter = require('@algolia/events'); var DerivedHelper = require('./DerivedHelper'); var escapeFacetValue = require('./functions/escapeFacetValue').escapeFacetValue; var inherits = require('./functions/inherits'); var merge = require('./functions/merge'); var objectHasKeys = require('./functions/objectHasKeys'); var omit = require('./functions/omit'); var requestBuilder = require('./requestBuilder'); var SearchParameters = require('./SearchParameters'); var SearchResults = require('./SearchResults'); var version = require('./version'); /** * Event triggered when a parameter is set or updated * @event AlgoliaSearchHelper#event:change * @property {object} event * @property {SearchParameters} event.state the current parameters with the latest changes applied * @property {SearchResults} event.results the previous results received from Algolia. `null` before the first request * @example * helper.on('change', function(event) { * console.log('The parameters have changed'); * }); */ /** * Event triggered when a main search is sent to Algolia * @event AlgoliaSearchHelper#event:search * @property {object} event * @property {SearchParameters} event.state the parameters used for this search * @property {SearchResults} event.results the results from the previous search. `null` if it is the first search. * @example * helper.on('search', function(event) { * console.log('Search sent'); * }); */ /** * Event triggered when a search using `searchForFacetValues` is sent to Algolia * @event AlgoliaSearchHelper#event:searchForFacetValues * @property {object} event * @property {SearchParameters} event.state the parameters used for this search it is the first search. * @property {string} event.facet the facet searched into * @property {string} event.query the query used to search in the facets * @example * helper.on('searchForFacetValues', function(event) { * console.log('searchForFacetValues sent'); * }); */ /** * Event triggered when a search using `searchOnce` is sent to Algolia * @event AlgoliaSearchHelper#event:searchOnce * @property {object} event * @property {SearchParameters} event.state the parameters used for this search it is the first search. * @example * helper.on('searchOnce', function(event) { * console.log('searchOnce sent'); * }); */ /** * Event triggered when the results are retrieved from Algolia * @event AlgoliaSearchHelper#event:result * @property {object} event * @property {SearchResults} event.results the results received from Algolia * @property {SearchParameters} event.state the parameters used to query Algolia. Those might be different from the one in the helper instance (for example if the network is unreliable). * @example * helper.on('result', function(event) { * console.log('Search results received'); * }); */ /** * Event triggered when Algolia sends back an error. For example, if an unknown parameter is * used, the error can be caught using this event. * @event AlgoliaSearchHelper#event:error * @property {object} event * @property {Error} event.error the error returned by the Algolia. * @example * helper.on('error', function(event) { * console.log('Houston we got a problem.'); * }); */ /** * Event triggered when the queue of queries have been depleted (with any result or outdated queries) * @event AlgoliaSearchHelper#event:searchQueueEmpty * @example * helper.on('searchQueueEmpty', function() { * console.log('No more search pending'); * // This is received before the result event if we're not expecting new results * }); * * helper.search(); */ /** * Initialize a new AlgoliaSearchHelper * @class * @classdesc The AlgoliaSearchHelper is a class that ease the management of the * search. It provides an event based interface for search callbacks: * - change: when the internal search state is changed. * This event contains a {@link SearchParameters} object and the * {@link SearchResults} of the last result if any. * - 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. * - error: when the response is an error. This event contains the error returned by the server. * @param {AlgoliaSearch} client an AlgoliaSearch client * @param {string} index the index name to query * @param {SearchParameters | object} options an object defining the initial * config of the search. It doesn't have to be a {SearchParameters}, * just an object containing the properties you need from it. * @param {SearchResultsOptions|object} searchResultsOptions an object defining the options to use when creating the search results. */ function AlgoliaSearchHelper(client, index, options, searchResultsOptions) { if (typeof client.addAlgoliaAgent === 'function') { client.addAlgoliaAgent('JS Helper (' + version + ')'); } this.setClient(client); var opts = options || {}; opts.index = index; this.state = SearchParameters.make(opts); this.lastResults = null; this._queryId = 0; this._lastQueryIdReceived = -1; this.derivedHelpers = []; this._currentNbQueries = 0; this._searchResultsOptions = searchResultsOptions; } inherits(AlgoliaSearchHelper, EventEmitter); /** * Start the search with the parameters set in the state. When the * method is called, it triggers a `search` event. The results will * be available through the `result` event. If an error occurs, an * `error` will be fired instead. * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires search * @fires result * @fires error * @chainable */ AlgoliaSearchHelper.prototype.search = function () { this._search({ onlyWithDerivedHelpers: false }); return this; }; AlgoliaSearchHelper.prototype.searchOnlyWithDerivedHelpers = function () { this._search({ onlyWithDerivedHelpers: true }); return this; }; /** * Gets the search query parameters that would be sent to the Algolia Client * for the hits * @return {object} Query Parameters */ AlgoliaSearchHelper.prototype.getQuery = function () { var state = this.state; return requestBuilder._getHitsSearchParams(state); }; /** * Start a search using a modified version of the current state. This method does * not trigger the helper lifecycle and does not modify the state kept internally * by the helper. This second aspect means that the next search call will be the * same as a search call before calling searchOnce. * @param {object} options can contain all the parameters that can be set to SearchParameters * plus the index * @param {function} [cb] optional callback executed when the response from the * server is back. * @return {promise|undefined} if a callback is passed the method returns undefined * otherwise it returns a promise containing an object with two keys : * - content with a SearchResults * - state with the state used for the query as a SearchParameters * @example * // Changing the number of records returned per page to 1 * // This example uses the callback API * var state = helper.searchOnce({hitsPerPage: 1}, * function(error, content, state) { * // if an error occurred it will be passed in error, otherwise its value is null * // content contains the results formatted as a SearchResults * // state is the instance of SearchParameters used for this search * }); * @example * // Changing the number of records returned per page to 1 * // This example uses the promise API * var state1 = helper.searchOnce({hitsPerPage: 1}) * .then(promiseHandler); * * function promiseHandler(res) { * // res contains * // { * // content : SearchResults * // state : SearchParameters (the one used for this specific search) * // } * } */ AlgoliaSearchHelper.prototype.searchOnce = function (options, cb) { var tempState = !options ? this.state : this.state.setQueryParameters(options); var queries = requestBuilder._getQueries(tempState.index, tempState); // eslint-disable-next-line consistent-this var self = this; this._currentNbQueries++; this.emit('searchOnce', { state: tempState, }); if (cb) { this.client .search(queries) .then(function (content) { self._currentNbQueries--; if (self._currentNbQueries === 0) { self.emit('searchQueueEmpty'); } cb(null, new SearchResults(tempState, content.results), tempState); }) .catch(function (err) { self._currentNbQueries--; if (self._currentNbQueries === 0) { self.emit('searchQueueEmpty'); } cb(err, null, tempState); }); return undefined; } return this.client.search(queries).then( function (content) { self._currentNbQueries--; if (self._currentNbQueries === 0) self.emit('searchQueueEmpty'); return { content: new SearchResults(tempState, content.results), state: tempState, _originalResponse: content, }; }, function (e) { self._currentNbQueries--; if (self._currentNbQueries === 0) self.emit('searchQueueEmpty'); throw e; } ); }; /** * Start the search for answers with the parameters set in the state. * This method returns a promise. * @param {Object} options - the options for answers API call * @param {string[]} options.attributesForPrediction - Attributes to use for predictions. If empty, `searchableAttributes` is used instead. * @param {string[]} options.queryLanguages - The languages in the query. Currently only supports ['en']. * @param {number} options.nbHits - Maximum number of answers to retrieve from the Answers Engine. Cannot be greater than 1000. * * @return {promise} the answer results * @deprecated answers is deprecated and will be replaced with new initiatives */ AlgoliaSearchHelper.prototype.findAnswers = function (options) { // eslint-disable-next-line no-console console.warn('[algoliasearch-helper] answers is no longer supported'); var state = this.state; var derivedHelper = this.derivedHelpers[0]; if (!derivedHelper) { return Promise.resolve([]); } var derivedState = derivedHelper.getModifiedState(state); var data = merge( { attributesForPrediction: options.attributesForPrediction, nbHits: options.nbHits, }, { params: omit(requestBuilder._getHitsSearchParams(derivedState), [ 'attributesToSnippet', 'hitsPerPage', 'restrictSearchableAttributes', 'snippetEllipsisText', ]), } ); var errorMessage = 'search for answers was called, but this client does not have a function client.initIndex(index).findAnswers'; if (typeof this.client.initIndex !== 'function') { throw new Error(errorMessage); } var index = this.client.initIndex(derivedState.index); if (typeof index.findAnswers !== 'function') { throw new Error(errorMessage); } return index.findAnswers(derivedState.query, options.queryLanguages, data); }; /** * Structure of each result when using * [`searchForFacetValues()`](reference.html#AlgoliaSearchHelper#searchForFacetValues) * @typedef FacetSearchHit * @type {object} * @property {string} value the facet value * @property {string} highlighted the facet value highlighted with the query string * @property {number} count number of occurrence of this facet value * @property {boolean} isRefined true if the value is already refined */ /** * Structure of the data resolved by the * [`searchForFacetValues()`](reference.html#AlgoliaSearchHelper#searchForFacetValues) * promise. * @typedef FacetSearchResult * @type {object} * @property {FacetSearchHit} facetHits the results for this search for facet values * @property {number} processingTimeMS time taken by the query inside the engine */ /** * Search for facet values based on an query and the name of a faceted attribute. This * triggers a search and will return a promise. On top of using the query, it also sends * the parameters from the state so that the search is narrowed down to only the possible values. * * See the description of [FacetSearchResult](reference.html#FacetSearchResult) * @param {string} facet the name of the faceted attribute * @param {string} query the string query for the search * @param {number} [maxFacetHits] the maximum number values returned. Should be > 0 and <= 100 * @param {object} [userState] the set of custom parameters to use on top of the current state. Setting a property to `undefined` removes * it in the generated query. * @return {promise.} the results of the search */ AlgoliaSearchHelper.prototype.searchForFacetValues = function ( facet, query, maxFacetHits, userState ) { var clientHasSFFV = typeof this.client.searchForFacetValues === 'function'; var clientHasInitIndex = typeof this.client.initIndex === 'function'; if ( !clientHasSFFV && !clientHasInitIndex && typeof this.client.search !== 'function' ) { throw new Error( 'search for facet values (searchable) was called, but this client does not have a function client.searchForFacetValues or client.initIndex(index).searchForFacetValues' ); } var state = this.state.setQueryParameters(userState || {}); var isDisjunctive = state.isDisjunctiveFacet(facet); var algoliaQuery = requestBuilder.getSearchForFacetQuery( facet, query, maxFacetHits, state ); this._currentNbQueries++; // eslint-disable-next-line consistent-this var self = this; var searchForFacetValuesPromise; // newer algoliasearch ^3.27.1 - ~4.0.0 if (clientHasSFFV) { searchForFacetValuesPromise = this.client.searchForFacetValues([ { indexName: state.index, params: algoliaQuery }, ]); // algoliasearch < 3.27.1 } else if (clientHasInitIndex) { searchForFacetValuesPromise = this.client .initIndex(state.index) .searchForFacetValues(algoliaQuery); // algoliasearch ~5.0.0 } else { // @MAJOR only use client.search delete algoliaQuery.facetName; searchForFacetValuesPromise = this.client .search([ { type: 'facet', facet: facet, indexName: state.index, params: algoliaQuery, }, ]) .then(function processResponse(response) { return response.results[0]; }); } this.emit('searchForFacetValues', { state: state, facet: facet, query: query, }); return searchForFacetValuesPromise.then( function addIsRefined(content) { self._currentNbQueries--; if (self._currentNbQueries === 0) self.emit('searchQueueEmpty'); content = Array.isArray(content) ? content[0] : content; content.facetHits.forEach(function (f) { f.escapedValue = escapeFacetValue(f.value); f.isRefined = isDisjunctive ? state.isDisjunctiveFacetRefined(facet, f.escapedValue) : state.isFacetRefined(facet, f.escapedValue); }); return content; }, function (e) { self._currentNbQueries--; if (self._currentNbQueries === 0) self.emit('searchQueueEmpty'); throw e; } ); }; /** * Sets the text query used for the search. * * This method resets the current page to 0. * @param {string} q the user query * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.setQuery = function (q) { this._change({ state: this.state.resetPage().setQuery(q), isPageReset: true, }); return this; }; /** * Remove all the types of refinements except tags. A string can be provided to remove * only the refinements of a specific attribute. For more advanced use case, you can * provide a function instead. This function should follow the * [clearCallback definition](#SearchParameters.clearCallback). * * This method resets the current page to 0. * @param {string} [name] optional name of the facet / attribute on which we want to remove all refinements * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable * @example * // Removing all the refinements * helper.clearRefinements().search(); * @example * // Removing all the filters on a the category attribute. * helper.clearRefinements('category').search(); * @example * // Removing only the exclude filters on the category facet. * helper.clearRefinements(function(value, attribute, type) { * return type === 'exclude' && attribute === 'category'; * }).search(); */ AlgoliaSearchHelper.prototype.clearRefinements = function (name) { this._change({ state: this.state.resetPage().clearRefinements(name), isPageReset: true, }); return this; }; /** * Remove all the tag filters. * * This method resets the current page to 0. * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.clearTags = function () { this._change({ state: this.state.resetPage().clearTags(), isPageReset: true, }); return this; }; /** * Adds a disjunctive filter to a faceted attribute with the `value` provided. If the * filter is already set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value (will be converted to string) * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.addDisjunctiveFacetRefinement = function ( facet, value ) { this._change({ state: this.state.resetPage().addDisjunctiveFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#addDisjunctiveFacetRefinement} */ AlgoliaSearchHelper.prototype.addDisjunctiveRefine = function () { return this.addDisjunctiveFacetRefinement.apply(this, arguments); }; /** * Adds a refinement on a hierarchical facet. It will throw * an exception if the facet is not defined or if the facet * is already refined. * * This method resets the current page to 0. * @param {string} facet the facet name * @param {string} path the hierarchical facet path * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @throws Error if the facet is not defined or if the facet is refined * @chainable * @fires change */ AlgoliaSearchHelper.prototype.addHierarchicalFacetRefinement = function ( facet, path ) { this._change({ state: this.state.resetPage().addHierarchicalFacetRefinement(facet, path), isPageReset: true, }); return this; }; /** * Adds a an numeric filter to an attribute with the `operator` and `value` provided. If the * filter is already set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} attribute the attribute on which the numeric filter applies * @param {string} operator the operator of the filter * @param {number} value the value of the filter * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.addNumericRefinement = function ( attribute, operator, value ) { this._change({ state: this.state .resetPage() .addNumericRefinement(attribute, operator, value), isPageReset: true, }); return this; }; /** * Adds a filter to a faceted attribute with the `value` provided. If the * filter is already set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value (will be converted to string) * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.addFacetRefinement = function (facet, value) { this._change({ state: this.state.resetPage().addFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#addFacetRefinement} */ AlgoliaSearchHelper.prototype.addRefine = function () { return this.addFacetRefinement.apply(this, arguments); }; /** * Adds a an exclusion filter to a faceted attribute with the `value` provided. If the * filter is already set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value (will be converted to string) * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.addFacetExclusion = function (facet, value) { this._change({ state: this.state.resetPage().addExcludeRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#addFacetExclusion} */ AlgoliaSearchHelper.prototype.addExclude = function () { return this.addFacetExclusion.apply(this, arguments); }; /** * Adds a tag filter with the `tag` provided. If the * filter is already set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} tag the tag to add to the filter * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.addTag = function (tag) { this._change({ state: this.state.resetPage().addTagRefinement(tag), isPageReset: true, }); return this; }; /** * Removes an numeric filter to an attribute with the `operator` and `value` provided. If the * filter is not set, it doesn't change the filters. * * Some parameters are optional, triggering different behavior: * - if the value is not provided, then all the numeric value will be removed for the * specified attribute/operator couple. * - if the operator is not provided either, then all the numeric filter on this attribute * will be removed. * * This method resets the current page to 0. * @param {string} attribute the attribute on which the numeric filter applies * @param {string} [operator] the operator of the filter * @param {number} [value] the value of the filter * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeNumericRefinement = function ( attribute, operator, value ) { this._change({ state: this.state .resetPage() .removeNumericRefinement(attribute, operator, value), isPageReset: true, }); return this; }; /** * Removes a disjunctive filter to a faceted attribute with the `value` provided. If the * filter is not set, it doesn't change the filters. * * If the value is omitted, then this method will remove all the filters for the * attribute. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} [value] the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeDisjunctiveFacetRefinement = function ( facet, value ) { this._change({ state: this.state .resetPage() .removeDisjunctiveFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#removeDisjunctiveFacetRefinement} */ AlgoliaSearchHelper.prototype.removeDisjunctiveRefine = function () { return this.removeDisjunctiveFacetRefinement.apply(this, arguments); }; /** * Removes the refinement set on a hierarchical facet. * @param {string} facet the facet name * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @throws Error if the facet is not defined or if the facet is not refined * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeHierarchicalFacetRefinement = function ( facet ) { this._change({ state: this.state.resetPage().removeHierarchicalFacetRefinement(facet), isPageReset: true, }); return this; }; /** * Removes a filter to a faceted attribute with the `value` provided. If the * filter is not set, it doesn't change the filters. * * If the value is omitted, then this method will remove all the filters for the * attribute. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} [value] the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeFacetRefinement = function (facet, value) { this._change({ state: this.state.resetPage().removeFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#removeFacetRefinement} */ AlgoliaSearchHelper.prototype.removeRefine = function () { return this.removeFacetRefinement.apply(this, arguments); }; /** * Removes an exclusion filter to a faceted attribute with the `value` provided. If the * filter is not set, it doesn't change the filters. * * If the value is omitted, then this method will remove all the filters for the * attribute. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} [value] the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeFacetExclusion = function (facet, value) { this._change({ state: this.state.resetPage().removeExcludeRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#removeFacetExclusion} */ AlgoliaSearchHelper.prototype.removeExclude = function () { return this.removeFacetExclusion.apply(this, arguments); }; /** * Removes a tag filter with the `tag` provided. If the * filter is not set, it doesn't change the filters. * * This method resets the current page to 0. * @param {string} tag tag to remove from the filter * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.removeTag = function (tag) { this._change({ state: this.state.resetPage().removeTagRefinement(tag), isPageReset: true, }); return this; }; /** * Adds or removes an exclusion filter to a faceted attribute with the `value` provided. If * the value is set then it removes it, otherwise it adds the filter. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.toggleFacetExclusion = function (facet, value) { this._change({ state: this.state.resetPage().toggleExcludeFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#toggleFacetExclusion} */ AlgoliaSearchHelper.prototype.toggleExclude = function () { return this.toggleFacetExclusion.apply(this, arguments); }; /** * Adds or removes a filter to a faceted attribute with the `value` provided. If * the value is set then it removes it, otherwise it adds the filter. * * This method can be used for conjunctive, disjunctive and hierarchical filters. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @throws Error will throw an error if the facet is not declared in the settings of the helper * @fires change * @chainable * @deprecated since version 2.19.0, see {@link AlgoliaSearchHelper#toggleFacetRefinement} */ AlgoliaSearchHelper.prototype.toggleRefinement = function (facet, value) { return this.toggleFacetRefinement(facet, value); }; /** * Adds or removes a filter to a faceted attribute with the `value` provided. If * the value is set then it removes it, otherwise it adds the filter. * * This method can be used for conjunctive, disjunctive and hierarchical filters. * * This method resets the current page to 0. * @param {string} facet the facet to refine * @param {string} value the associated value * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @throws Error will throw an error if the facet is not declared in the settings of the helper * @fires change * @chainable */ AlgoliaSearchHelper.prototype.toggleFacetRefinement = function (facet, value) { this._change({ state: this.state.resetPage().toggleFacetRefinement(facet, value), isPageReset: true, }); return this; }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since version 2.4.0, see {@link AlgoliaSearchHelper#toggleFacetRefinement} */ AlgoliaSearchHelper.prototype.toggleRefine = function () { return this.toggleFacetRefinement.apply(this, arguments); }; /** * Adds or removes a tag filter with the `value` provided. If * the value is set then it removes it, otherwise it adds the filter. * * This method resets the current page to 0. * @param {string} tag tag to remove or add * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.toggleTag = function (tag) { this._change({ state: this.state.resetPage().toggleTagRefinement(tag), isPageReset: true, }); return this; }; /** * Increments the page number by one. * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable * @example * helper.setPage(0).nextPage().getPage(); * // returns 1 */ AlgoliaSearchHelper.prototype.nextPage = function () { var page = this.state.page || 0; return this.setPage(page + 1); }; /** * Decrements the page number by one. * @fires change * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @chainable * @example * helper.setPage(1).previousPage().getPage(); * // returns 0 */ AlgoliaSearchHelper.prototype.previousPage = function () { var page = this.state.page || 0; return this.setPage(page - 1); }; /** * @private * @param {number} page The page number * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @chainable * @fires change */ function setCurrentPage(page) { if (page < 0) throw new Error('Page requested below 0.'); this._change({ state: this.state.setPage(page), isPageReset: false, }); return this; } /** * Change the current page * @deprecated * @param {number} page The page number * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.setCurrentPage = setCurrentPage; /** * Updates the current page. * @function * @param {number} page The page number * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.setPage = setCurrentPage; /** * Updates the name of the index that will be targeted by the query. * * This method resets the current page to 0. * @param {string} name the index name * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.setIndex = function (name) { this._change({ state: this.state.resetPage().setIndex(name), isPageReset: true, }); return this; }; /** * Update a parameter of the search. This method reset the page * * The complete list of parameters is available on the * [Algolia website](https://www.algolia.com/doc/rest#query-an-index). * The most commonly used parameters have their own [shortcuts](#query-parameters-shortcuts) * or benefit from higher-level APIs (all the kind of filters and facets have their own API) * * This method resets the current page to 0. * @param {string} parameter name of the parameter to update * @param {any} value new value of the parameter * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable * @example * helper.setQueryParameter('hitsPerPage', 20).search(); */ AlgoliaSearchHelper.prototype.setQueryParameter = function (parameter, value) { this._change({ state: this.state.resetPage().setQueryParameter(parameter, value), isPageReset: true, }); return this; }; /** * Set the whole state (warning: will erase previous state) * @param {SearchParameters} newState the whole new state * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @fires change * @chainable */ AlgoliaSearchHelper.prototype.setState = function (newState) { this._change({ state: SearchParameters.make(newState), isPageReset: false, }); return this; }; /** * Override the current state without triggering a change event. * Do not use this method unless you know what you are doing. (see the example * for a legit use case) * @param {SearchParameters} newState the whole new state * @return {AlgoliaSearchHelper} Method is chainable, it returns itself * @example * helper.on('change', function(state){ * // In this function you might want to find a way to store the state in the url/history * updateYourURL(state) * }) * window.onpopstate = function(event){ * // This is naive though as you should check if the state is really defined etc. * helper.overrideStateWithoutTriggeringChangeEvent(event.state).search() * } * @chainable */ AlgoliaSearchHelper.prototype.overrideStateWithoutTriggeringChangeEvent = function (newState) { this.state = new SearchParameters(newState); return this; }; /** * Check if an attribute has any numeric, conjunctive, disjunctive or hierarchical filters. * @param {string} attribute the name of the attribute * @return {boolean} true if the attribute is filtered by at least one value * @example * // hasRefinements works with numeric, conjunctive, disjunctive and hierarchical filters * helper.hasRefinements('price'); // false * helper.addNumericRefinement('price', '>', 100); * helper.hasRefinements('price'); // true * * helper.hasRefinements('color'); // false * helper.addFacetRefinement('color', 'blue'); * helper.hasRefinements('color'); // true * * helper.hasRefinements('material'); // false * helper.addDisjunctiveFacetRefinement('material', 'plastic'); * helper.hasRefinements('material'); // true * * helper.hasRefinements('categories'); // false * helper.toggleFacetRefinement('categories', 'kitchen > knife'); * helper.hasRefinements('categories'); // true * */ AlgoliaSearchHelper.prototype.hasRefinements = function (attribute) { if (objectHasKeys(this.state.getNumericRefinements(attribute))) { return true; } else if (this.state.isConjunctiveFacet(attribute)) { return this.state.isFacetRefined(attribute); } else if (this.state.isDisjunctiveFacet(attribute)) { return this.state.isDisjunctiveFacetRefined(attribute); } else if (this.state.isHierarchicalFacet(attribute)) { return this.state.isHierarchicalFacetRefined(attribute); } // there's currently no way to know that the user did call `addNumericRefinement` at some point // thus we cannot distinguish if there once was a numeric refinement that was cleared // so we will return false in every other situations to be consistent // while what we should do here is throw because we did not find the attribute in any type // of refinement return false; }; /** * Check if a value is excluded for a specific faceted attribute. If the value * is omitted then the function checks if there is any excluding refinements. * * @param {string} facet name of the attribute for used for faceting * @param {string} [value] optional value. If passed will test that this value * is filtering the given facet. * @return {boolean} true if refined * @example * helper.isExcludeRefined('color'); // false * helper.isExcludeRefined('color', 'blue') // false * helper.isExcludeRefined('color', 'red') // false * * helper.addFacetExclusion('color', 'red'); * * helper.isExcludeRefined('color'); // true * helper.isExcludeRefined('color', 'blue') // false * helper.isExcludeRefined('color', 'red') // true */ AlgoliaSearchHelper.prototype.isExcluded = function (facet, value) { return this.state.isExcludeRefined(facet, value); }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since 2.4.0, see {@link AlgoliaSearchHelper#hasRefinements} */ AlgoliaSearchHelper.prototype.isDisjunctiveRefined = function (facet, value) { return this.state.isDisjunctiveFacetRefined(facet, value); }; /** * Check if the string is a currently filtering tag. * @param {string} tag tag to check * @return {boolean} true if the tag is currently refined */ AlgoliaSearchHelper.prototype.hasTag = function (tag) { return this.state.isTagRefined(tag); }; // eslint-disable-next-line valid-jsdoc /** * @deprecated since 2.4.0, see {@link AlgoliaSearchHelper#hasTag} */ AlgoliaSearchHelper.prototype.isTagRefined = function () { return this.hasTagRefinements.apply(this, arguments); }; /** * Get the name of the currently used index. * @return {string} name of the index * @example * helper.setIndex('highestPrice_products').getIndex(); * // returns 'highestPrice_products' */ AlgoliaSearchHelper.prototype.getIndex = function () { return this.state.index; }; function getCurrentPage() { return this.state.page; } /** * Get the currently selected page * @deprecated * @return {number} the current page */ AlgoliaSearchHelper.prototype.getCurrentPage = getCurrentPage; /** * Get the currently selected page * @function * @return {number} the current page */ AlgoliaSearchHelper.prototype.getPage = getCurrentPage; /** * Get all the tags currently set to filters the results. * * @return {string[]} The list of tags currently set. */ AlgoliaSearchHelper.prototype.getTags = function () { return this.state.tagRefinements; }; /** * Get the list of refinements for a given attribute. This method works with * conjunctive, disjunctive, excluding and numerical filters. * * See also SearchResults#getRefinements * * @param {string} facetName attribute name used for faceting * @return {Array.} All Refinement are objects that contain a value, and * a type. Numeric also contains an operator. * @example * helper.addNumericRefinement('price', '>', 100); * helper.getRefinements('price'); * // [ * // { * // "value": [ * // 100 * // ], * // "operator": ">", * // "type": "numeric" * // } * // ] * @example * helper.addFacetRefinement('color', 'blue'); * helper.addFacetExclusion('color', 'red'); * helper.getRefinements('color'); * // [ * // { * // "value": "blue", * // "type": "conjunctive" * // }, * // { * // "value": "red", * // "type": "exclude" * // } * // ] * @example * helper.addDisjunctiveFacetRefinement('material', 'plastic'); * // [ * // { * // "value": "plastic", * // "type": "disjunctive" * // } * // ] */ AlgoliaSearchHelper.prototype.getRefinements = function (facetName) { var refinements = []; if (this.state.isConjunctiveFacet(facetName)) { var conjRefinements = this.state.getConjunctiveRefinements(facetName); conjRefinements.forEach(function (r) { refinements.push({ value: r, type: 'conjunctive', }); }); var excludeRefinements = this.state.getExcludeRefinements(facetName); excludeRefinements.forEach(function (r) { refinements.push({ value: r, type: 'exclude', }); }); } else if (this.state.isDisjunctiveFacet(facetName)) { var disjunctiveRefinements = this.state.getDisjunctiveRefinements(facetName); disjunctiveRefinements.forEach(function (r) { refinements.push({ value: r, type: 'disjunctive', }); }); } var numericRefinements = this.state.getNumericRefinements(facetName); Object.keys(numericRefinements).forEach(function (operator) { var value = numericRefinements[operator]; refinements.push({ value: value, operator: operator, type: 'numeric', }); }); return refinements; }; /** * Return the current refinement for the (attribute, operator) * @param {string} attribute attribute in the record * @param {string} operator operator applied on the refined values * @return {Array.} refined values */ AlgoliaSearchHelper.prototype.getNumericRefinement = function ( attribute, operator ) { return this.state.getNumericRefinement(attribute, operator); }; /** * Get the current breadcrumb for a hierarchical facet, as an array * @param {string} facetName Hierarchical facet name * @return {array.} the path as an array of string */ AlgoliaSearchHelper.prototype.getHierarchicalFacetBreadcrumb = function ( facetName ) { return this.state.getHierarchicalFacetBreadcrumb(facetName); }; // /////////// PRIVATE /** * Perform the underlying queries * @private * @param {object} options options for the query * @param {boolean} [options.onlyWithDerivedHelpers=false] if true, only the derived helpers will be queried * @return {undefined} does not return anything * @fires search * @fires result * @fires error */ AlgoliaSearchHelper.prototype._search = function (options) { var state = this.state; var states = []; var mainQueries = []; if (!options.onlyWithDerivedHelpers) { mainQueries = requestBuilder._getQueries(state.index, state); states.push({ state: state, queriesCount: mainQueries.length, helper: this, }); this.emit('search', { state: state, results: this.lastResults, }); } var derivedQueries = this.derivedHelpers.map(function (derivedHelper) { var derivedState = derivedHelper.getModifiedState(state); var derivedStateQueries = derivedState.index ? requestBuilder._getQueries(derivedState.index, derivedState) : []; states.push({ state: derivedState, queriesCount: derivedStateQueries.length, helper: derivedHelper, }); derivedHelper.emit('search', { state: derivedState, results: derivedHelper.lastResults, }); return derivedStateQueries; }); var queries = Array.prototype.concat.apply(mainQueries, derivedQueries); var queryId = this._queryId++; this._currentNbQueries++; if (!queries.length) { return Promise.resolve({ results: [] }).then( this._dispatchAlgoliaResponse.bind(this, states, queryId) ); } try { this.client .search(queries) .then(this._dispatchAlgoliaResponse.bind(this, states, queryId)) .catch(this._dispatchAlgoliaError.bind(this, queryId)); } catch (error) { // If we reach this part, we're in an internal error state this.emit('error', { error: error, }); } return undefined; }; /** * Transform the responses as sent by the server and transform them into a user * usable object that merge the results of all the batch requests. It will dispatch * over the different helper + derived helpers (when there are some). * @private * @param {array.<{SearchParameters, AlgoliaQueries, AlgoliaSearchHelper}>} states state used to generate the request * @param {number} queryId id of the current request * @param {object} content content of the response * @return {undefined} */ AlgoliaSearchHelper.prototype._dispatchAlgoliaResponse = function ( states, queryId, content ) { // eslint-disable-next-line consistent-this var self = this; // @TODO remove the number of outdated queries discarded instead of just one if (queryId < this._lastQueryIdReceived) { // Outdated answer return; } this._currentNbQueries -= queryId - this._lastQueryIdReceived; this._lastQueryIdReceived = queryId; if (this._currentNbQueries === 0) this.emit('searchQueueEmpty'); var results = content.results.slice(); states.forEach(function (s) { var state = s.state; var queriesCount = s.queriesCount; var helper = s.helper; var specificResults = results.splice(0, queriesCount); if (!state.index) { helper.emit('result', { results: null, state: state, }); return; } helper.lastResults = new SearchResults( state, specificResults, self._searchResultsOptions ); helper.emit('result', { results: helper.lastResults, state: state, }); }); }; AlgoliaSearchHelper.prototype._dispatchAlgoliaError = function ( queryId, error ) { if (queryId < this._lastQueryIdReceived) { // Outdated answer return; } this._currentNbQueries -= queryId - this._lastQueryIdReceived; this._lastQueryIdReceived = queryId; this.emit('error', { error: error, }); if (this._currentNbQueries === 0) this.emit('searchQueueEmpty'); }; AlgoliaSearchHelper.prototype.containsRefinement = function ( query, facetFilters, numericFilters, tagFilters ) { return ( query || facetFilters.length !== 0 || numericFilters.length !== 0 || tagFilters.length !== 0 ); }; /** * Test if there are some disjunctive refinements on the facet * @private * @param {string} facet the attribute to test * @return {boolean} true if there are refinements on this attribute */ AlgoliaSearchHelper.prototype._hasDisjunctiveRefinements = function (facet) { return ( this.state.disjunctiveRefinements[facet] && this.state.disjunctiveRefinements[facet].length > 0 ); }; AlgoliaSearchHelper.prototype._change = function (event) { var state = event.state; var isPageReset = event.isPageReset; if (state !== this.state) { this.state = state; this.emit('change', { state: this.state, results: this.lastResults, isPageReset: isPageReset, }); } }; /** * Clears the cache of the underlying Algolia client. * @return {AlgoliaSearchHelper} Method is chainable, it returns itself */ AlgoliaSearchHelper.prototype.clearCache = function () { if (this.client.clearCache) this.client.clearCache(); return this; }; /** * Updates the internal client instance. If the reference of the clients * are equal then no update is actually done. * @param {AlgoliaSearch} newClient an AlgoliaSearch client * @return {AlgoliaSearchHelper} Method is chainable, it returns itself */ AlgoliaSearchHelper.prototype.setClient = function (newClient) { if (this.client === newClient) return this; if (typeof newClient.addAlgoliaAgent === 'function') { newClient.addAlgoliaAgent('JS Helper (' + version + ')'); } this.client = newClient; return this; }; /** * Gets the instance of the currently used client. * @return {AlgoliaSearch} the currently used client */ AlgoliaSearchHelper.prototype.getClient = function () { return this.client; }; /** * Creates an derived instance of the Helper. A derived helper * is a way to request other indices synchronised with the lifecycle * of the main Helper. This mechanism uses the multiqueries feature * of Algolia to aggregate all the requests in a single network call. * * This method takes a function that is used to create a new SearchParameter * that will be used to create requests to Algolia. Those new requests * are created just before the `search` event. The signature of the function * is `SearchParameters -> SearchParameters`. * * This method returns a new DerivedHelper which is an EventEmitter * that fires the same `search`, `result` and `error` events. Those * events, however, will receive data specific to this DerivedHelper * and the SearchParameters that is returned by the call of the * parameter function. * @param {function} fn SearchParameters -> SearchParameters * @return {DerivedHelper} a new DerivedHelper */ AlgoliaSearchHelper.prototype.derive = function (fn) { var derivedHelper = new DerivedHelper(this, fn); this.derivedHelpers.push(derivedHelper); return derivedHelper; }; /** * This method detaches a derived Helper from the main one. Prefer using the one from the * derived helper itself, to remove the event listeners too. * @private * @param {DerivedHelper} derivedHelper the derived helper to detach * @return {undefined} nothing is returned * @throws Error */ AlgoliaSearchHelper.prototype.detachDerivedHelper = function (derivedHelper) { var pos = this.derivedHelpers.indexOf(derivedHelper); if (pos === -1) throw new Error('Derived helper already detached'); this.derivedHelpers.splice(pos, 1); }; /** * This method returns true if there is currently at least one on-going search. * @return {boolean} true if there is a search pending */ AlgoliaSearchHelper.prototype.hasPendingRequests = function () { return this._currentNbQueries > 0; }; /** * @typedef AlgoliaSearchHelper.NumericRefinement * @type {object} * @property {number[]} value the numbers that are used for filtering this attribute with * the operator specified. * @property {string} operator the faceting data: value, number of entries * @property {string} type will be 'numeric' */ /** * @typedef AlgoliaSearchHelper.FacetRefinement * @type {object} * @property {string} value the string use to filter the attribute * @property {string} type the type of filter: 'conjunctive', 'disjunctive', 'exclude' */ module.exports = AlgoliaSearchHelper; },{"./DerivedHelper":3,"./SearchParameters":5,"./SearchResults":7,"./functions/escapeFacetValue":11,"./functions/inherits":15,"./functions/merge":17,"./functions/objectHasKeys":18,"./functions/omit":19,"./requestBuilder":22,"./version":24,"@algolia/events":1}],9:[function(require,module,exports){ 'use strict'; module.exports = function compact(array) { if (!Array.isArray(array)) { return []; } return array.filter(Boolean); }; },{}],10:[function(require,module,exports){ 'use strict'; // NOTE: this behaves like lodash/defaults, but doesn't mutate the target // it also preserve keys order module.exports = 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; }, {}); }; },{}],11:[function(require,module,exports){ 'use strict'; /** * 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(/^\\-/, '-'); } module.exports = { escapeFacetValue: escapeFacetValue, unescapeFacetValue: unescapeFacetValue, }; },{}],12:[function(require,module,exports){ 'use strict'; // @MAJOR can be replaced by native Array#find when we change support module.exports = 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; }; },{}],13:[function(require,module,exports){ 'use strict'; // @MAJOR can be replaced by native Array#findIndex when we change support module.exports = function find(array, comparator) { if (!Array.isArray(array)) { return -1; } for (var i = 0; i < array.length; i++) { if (comparator(array[i])) { return i; } } return -1; }; },{}],14:[function(require,module,exports){ 'use strict'; var find = require('./find'); /** * Transform sort format from user friendly notation to lodash format * @param {string[]} sortBy array of predicate of the form "attribute:order" * @param {string[]} [defaults] array of predicate of the form "attribute:order" * @return {array.} array containing 2 elements : attributes, orders */ module.exports = function formatSort(sortBy, defaults) { var defaultInstructions = (defaults || []).map(function (sort) { return sort.split(':'); }); return sortBy.reduce( function preparePredicate(out, sort) { var sortInstruction = sort.split(':'); var matchingDefault = find( defaultInstructions, function (defaultInstruction) { return defaultInstruction[0] === sortInstruction[0]; } ); if (sortInstruction.length > 1 || !matchingDefault) { out[0].push(sortInstruction[0]); out[1].push(sortInstruction[1]); return out; } out[0].push(matchingDefault[0]); out[1].push(matchingDefault[1]); return out; }, [[], []] ); }; },{"./find":12}],15:[function(require,module,exports){ 'use strict'; function inherits(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true, }, }); } module.exports = inherits; },{}],16:[function(require,module,exports){ 'use strict'; function intersection(arr1, arr2) { return arr1.filter(function (value, index) { return ( arr2.indexOf(value) > -1 && arr1.indexOf(value) === index /* skips duplicates */ ); }); } module.exports = intersection; },{}],17:[function(require,module,exports){ 'use strict'; 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; } module.exports = merge; },{}],18:[function(require,module,exports){ 'use strict'; function objectHasKeys(obj) { return obj && Object.keys(obj).length > 0; } module.exports = objectHasKeys; },{}],19:[function(require,module,exports){ 'use strict'; // https://github.com/babel/babel/blob/3aaafae053fa75febb3aa45d45b6f00646e30ba4/packages/babel-helpers/src/helpers.js#L604-L620 function _objectWithoutPropertiesLoose(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; } module.exports = _objectWithoutPropertiesLoose; },{}],20:[function(require,module,exports){ 'use strict'; function compareAscending(value, other) { if (value !== other) { var valIsDefined = value !== undefined; var valIsNull = value === null; var othIsDefined = other !== undefined; var othIsNull = other === null; if ( (!othIsNull && value > other) || (valIsNull && othIsDefined) || !valIsDefined ) { return 1; } if ( (!valIsNull && value < other) || (othIsNull && valIsDefined) || !othIsDefined ) { return -1; } } return 0; } /** * @param {Array} collection object with keys in attributes * @param {Array} iteratees attributes * @param {Array} orders asc | desc * @return {Array} sorted collection */ function orderBy(collection, iteratees, orders) { if (!Array.isArray(collection)) { return []; } if (!Array.isArray(orders)) { orders = []; } var result = collection.map(function (value, index) { return { criteria: iteratees.map(function (iteratee) { return value[iteratee]; }), index: index, value: value, }; }); result.sort(function comparer(object, other) { var index = -1; while (++index < object.criteria.length) { var res = compareAscending(object.criteria[index], other.criteria[index]); if (res) { if (index >= orders.length) { return res; } if (orders[index] === 'desc') { return -res; } return res; } } // This ensures a stable sort in V8 and other engines. // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. return object.index - other.index; }); return result.map(function (res) { return res.value; }); } module.exports = orderBy; },{}],21:[function(require,module,exports){ 'use strict'; 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.' ); } module.exports = valToNumber; },{}],22:[function(require,module,exports){ 'use strict'; var merge = require('./functions/merge'); 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 { params.facetFilters = filteredFacetFilters.length > 0 ? filteredFacetFilters : undefined; } queries.push({ indexName: index, params: params }); }); } }); return queries; }, /** * Build search parameters used to fetch hits * @private * @param {SearchParameters} state The state from which to get the queries * @return {object.} 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 = { facets: facets.indexOf('*') > -1 ? ['*'] : facets, tagFilters: tagFilters, }; if (facetFilters.length > 0) { additionalParams.facetFilters = facetFilters; } if (numericFilters.length > 0) { additionalParams.numericFilters = numericFilters; } return sortObject(merge({}, state.getQueryParams(), 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({}, 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.} 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( {}, requestBuilder._getHitsSearchParams(stateForSearchForFacetValues), searchForFacetSearchParameters ) ); }, }; module.exports = requestBuilder; },{"./functions/merge":17}],23:[function(require,module,exports){ 'use strict'; module.exports = function isValidUserToken(userToken) { if (userToken === null) { return false; } return /^[a-zA-Z0-9_-]{1,64}$/.test(userToken); }; },{}],24:[function(require,module,exports){ 'use strict'; module.exports = '3.16.3'; },{}]},{},[2])(2) }); //# sourceMappingURL=algoliasearch.helper.js.map