Files
documentation/node_modules/algoliasearch-helper/dist/algoliasearch.helper.js
2024-03-22 03:47:51 +05:30

6116 lines
188 KiB
JavaScript

(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<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
function EventEmitter() {
this._events = this._events || {};
this._maxListeners = this._maxListeners || undefined;
}
module.exports = EventEmitter;
// Backwards-compat with node 0.10.x
// EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function(n) {
if (!isNumber(n) || n < 0 || isNaN(n))
throw TypeError('n must be a positive number');
this._maxListeners = n;
return this;
};
EventEmitter.prototype.emit = function(type) {
var er, handler, len, args, i, listeners;
if (!this._events)
this._events = {};
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events.error ||
(isObject(this._events.error) && !this._events.error.length)) {
er = arguments[1];
if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
// At least give some kind of context to the user
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
err.context = er;
throw err;
}
}
}
handler = this._events[type];
if (isUndefined(handler))
return false;
if (isFunction(handler)) {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
args = Array.prototype.slice.call(arguments, 1);
handler.apply(this, args);
}
} else if (isObject(handler)) {
args = Array.prototype.slice.call(arguments, 1);
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++)
listeners[i].apply(this, args);
}
return true;
};
EventEmitter.prototype.addListener = function(type, listener) {
var m;
if (!isFunction(listener))
throw TypeError('listener must be a function');
if (!this._events)
this._events = {};
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (this._events.newListener)
this.emit('newListener', type,
isFunction(listener.listener) ?
listener.listener : listener);
if (!this._events[type])
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
else if (isObject(this._events[type]))
// If we've already got an array, just append.
this._events[type].push(listener);
else
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
// Check for listener leak
if (isObject(this._events[type]) && !this._events[type].warned) {
if (!isUndefined(this._maxListeners)) {
m = this._maxListeners;
} else {
m = EventEmitter.defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
this._events[type].length);
if (typeof console.trace === 'function') {
// not supported in IE 10
console.trace();
}
}
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function(type, listener) {
if (!isFunction(listener))
throw TypeError('listener must be a function');
var fired = false;
function g() {
this.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(this, arguments);
}
}
g.listener = listener;
this.on(type, g);
return this;
};
// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener = function(type, listener) {
var list, position, length, i;
if (!isFunction(listener))
throw TypeError('listener must be a function');
if (!this._events || !this._events[type])
return this;
list = this._events[type];
length = list.length;
position = -1;
if (list === listener ||
(isFunction(list.listener) && list.listener === listener)) {
delete this._events[type];
if (this._events.removeListener)
this.emit('removeListener', type, listener);
} else if (isObject(list)) {
for (i = length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list.length = 0;
delete this._events[type];
} else {
list.splice(position, 1);
}
if (this._events.removeListener)
this.emit('removeListener', type, listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners = function(type) {
var key, listeners;
if (!this._events)
return this;
// not listening for removeListener, no need to emit
if (!this._events.removeListener) {
if (arguments.length === 0)
this._events = {};
else if (this._events[type])
delete this._events[type];
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
for (key in this._events) {
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = {};
return this;
}
listeners = this._events[type];
if (isFunction(listeners)) {
this.removeListener(type, listeners);
} else if (listeners) {
// LIFO order
while (listeners.length)
this.removeListener(type, listeners[listeners.length - 1]);
}
delete this._events[type];
return this;
};
EventEmitter.prototype.listeners = function(type) {
var ret;
if (!this._events || !this._events[type])
ret = [];
else if (isFunction(this._events[type]))
ret = [this._events[type]];
else
ret = this._events[type].slice();
return ret;
};
EventEmitter.prototype.listenerCount = function(type) {
if (this._events) {
var evlistener = this._events[type];
if (isFunction(evlistener))
return 1;
else if (evlistener)
return evlistener.length;
}
return 0;
};
EventEmitter.listenerCount = function(emitter, type) {
return emitter.listenerCount(type);
};
function isFunction(arg) {
return typeof arg === 'function';
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isUndefined(arg) {
return arg === void 0;
}
},{}],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.<string, SearchParameters.refinementList.Refinements>} 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.<string, Array.<number|number[]>>} 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 <caption>SearchParameters of the first query in
* <a href="http://demos.algolia.com/instant-search-demo/">the instant search demo</a></caption>
{
"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.<string, SearchParameters.FacetList>}
*/
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.<string, SearchParameters.FacetList>}
*/
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.<string, SearchParameters.FacetList>}
*/
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.<string, SearchParameters.OperatorList>}
*/
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.<string, SearchParameters.FacetList>}
*/
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.<number|number[]>} 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.<string, OperatorList>} 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.<string>} 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.<object>} results the results from algolia client
* @param {object} options options to control results content
* @example <caption>SearchResults of the first query in
* <a href="http://demos.algolia.com/instant-search-demo">the instant search demo</a></caption>
{
"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.<string> | 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.<Refinement>} 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.<FacetSearchResult>} 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.<FacetRefinement|NumericRefinement>} 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.<number|number[]>} 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.<string>} 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.<string[]>} 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<object>} collection object with keys in attributes
* @param {Array<string>} iteratees attributes
* @param {Array<string>} orders asc | desc
* @return {Array<object>} 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.<string, any>} The search parameters for hits
*/
_getHitsSearchParams: function (state) {
var facets = state.facets
.concat(state.disjunctiveFacets)
.concat(requestBuilder._getHitsHierarchicalFacetsAttributes(state))
.sort();
var facetFilters = requestBuilder._getFacetFilters(state);
var numericFilters = requestBuilder._getNumericFilters(state);
var tagFilters = requestBuilder._getTagFilters(state);
var additionalParams = {
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.<string>} The facet filters in the algolia format
*/
_getFacetFilters: function (state, facet, hierarchicalRootLevel) {
var facetFilters = [];
var facetsRefinements = state.facetsRefinements || {};
Object.keys(facetsRefinements)
.sort()
.forEach(function (facetName) {
var facetValues = facetsRefinements[facetName] || [];
facetValues
.slice()
.sort()
.forEach(function (facetValue) {
facetFilters.push(facetName + ':' + facetValue);
});
});
var facetsExcludes = state.facetsExcludes || {};
Object.keys(facetsExcludes)
.sort()
.forEach(function (facetName) {
var facetValues = facetsExcludes[facetName] || [];
facetValues.sort().forEach(function (facetValue) {
facetFilters.push(facetName + ':-' + facetValue);
});
});
var disjunctiveFacetsRefinements = state.disjunctiveFacetsRefinements || {};
Object.keys(disjunctiveFacetsRefinements)
.sort()
.forEach(function (facetName) {
var facetValues = disjunctiveFacetsRefinements[facetName] || [];
if (facetName === facet || !facetValues || facetValues.length === 0) {
return;
}
var orFilters = [];
facetValues
.slice()
.sort()
.forEach(function (facetValue) {
orFilters.push(facetName + ':' + facetValue);
});
facetFilters.push(orFilters);
});
var hierarchicalFacetsRefinements =
state.hierarchicalFacetsRefinements || {};
Object.keys(hierarchicalFacetsRefinements)
.sort()
.forEach(function (facetName) {
var facetValues = hierarchicalFacetsRefinements[facetName] || [];
var facetValue = facetValues[0];
if (facetValue === undefined) {
return;
}
var hierarchicalFacet = state.getHierarchicalFacetByName(facetName);
var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet);
var rootPath = state._getHierarchicalRootPath(hierarchicalFacet);
var attributeToRefine;
var attributesIndex;
// we ask for parent facet values only when the `facet` is the current hierarchical facet
if (facet === facetName) {
// if we are at the root level already, no need to ask for facet values, we get them from
// the hits query
if (
facetValue.indexOf(separator) === -1 ||
(!rootPath && hierarchicalRootLevel === true) ||
(rootPath &&
rootPath.split(separator).length ===
facetValue.split(separator).length)
) {
return;
}
if (!rootPath) {
attributesIndex = facetValue.split(separator).length - 2;
facetValue = facetValue.slice(0, facetValue.lastIndexOf(separator));
} else {
attributesIndex = rootPath.split(separator).length - 1;
facetValue = rootPath;
}
attributeToRefine = hierarchicalFacet.attributes[attributesIndex];
} else {
attributesIndex = facetValue.split(separator).length - 1;
attributeToRefine = hierarchicalFacet.attributes[attributesIndex];
}
if (attributeToRefine) {
facetFilters.push([attributeToRefine + ':' + facetValue]);
}
});
return facetFilters;
},
_getHitsHierarchicalFacetsAttributes: function (state) {
var out = [];
return state.hierarchicalFacets.reduce(
// ask for as much levels as there's hierarchical refinements
function getHitsAttributesForHierarchicalFacet(
allAttributes,
hierarchicalFacet
) {
var hierarchicalRefinement = state.getHierarchicalRefinement(
hierarchicalFacet.name
)[0];
// if no refinement, ask for root level
if (!hierarchicalRefinement) {
allAttributes.push(hierarchicalFacet.attributes[0]);
return allAttributes;
}
var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet);
var level = hierarchicalRefinement.split(separator).length;
var newAttributes = hierarchicalFacet.attributes.slice(0, level + 1);
return allAttributes.concat(newAttributes);
},
out
);
},
_getDisjunctiveHierarchicalFacetAttribute: function (
state,
hierarchicalFacet,
rootLevel
) {
var separator = state._getHierarchicalFacetSeparator(hierarchicalFacet);
if (rootLevel === true) {
var rootPath = state._getHierarchicalRootPath(hierarchicalFacet);
var attributeIndex = 0;
if (rootPath) {
attributeIndex = rootPath.split(separator).length;
}
return [hierarchicalFacet.attributes[attributeIndex]];
}
var hierarchicalRefinement =
state.getHierarchicalRefinement(hierarchicalFacet.name)[0] || '';
// if refinement is 'beers > IPA > Flying dog',
// then we want `facets: ['beers > IPA']` as disjunctive facet (parent level values)
var parentLevel = hierarchicalRefinement.split(separator).length - 1;
return hierarchicalFacet.attributes.slice(0, parentLevel + 1);
},
getSearchForFacetQuery: function (facetName, query, maxFacetHits, state) {
var stateForSearchForFacetValues = state.isDisjunctiveFacet(facetName)
? state.clearRefinements(facetName)
: state;
var searchForFacetSearchParameters = {
facetQuery: query,
facetName: facetName,
};
if (typeof maxFacetHits === 'number') {
searchForFacetSearchParameters.maxFacetHits = maxFacetHits;
}
return sortObject(
merge(
{},
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