From 6f9fd3bb76dd746519a4bf00586e178b7e7136af Mon Sep 17 00:00:00 2001 From: Jonas Grimfelt Date: Tue, 19 Jan 2016 05:32:46 -0500 Subject: [PATCH] dist: version only bower assets really in use (ghpages) --- Gruntfile.js | 56 +- bower.json | 5 +- .../dist/algoliasearch.angular.js | 2678 +++++ .../dist/algoliasearch.angular.min.js | 7 + .../dist/algoliasearch.jquery.js | 2678 +++++ .../dist/algoliasearch.jquery.min.js | 7 + .../algoliasearch/dist/algoliasearch.js | 2663 +++++ .../algoliasearch/dist/algoliasearch.min.js | 7 + .../algoliasearch/dist/algoliasearch.parse.js | 6085 +++++++++++ .../bootstrap/dist/css/bootstrap-theme.css | 476 + .../dist/css/bootstrap-theme.min.css | 5 + .../bootstrap/dist/css/bootstrap.css | 6566 ++++++++++++ .../bootstrap/dist/css/bootstrap.min.css | 5 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../fixed-sticky/fixedsticky.css | 22 + .../fixed-sticky/fixedsticky.js | 191 + .../font-awesome/css/font-awesome.css | 2086 ++++ .../font-awesome/css/font-awesome.min.css | 4 + .../fonts/fontawesome-webfont.eot | Bin 0 -> 70807 bytes .../fonts/fontawesome-webfont.svg | 655 ++ .../fonts/fontawesome-webfont.ttf | Bin 0 -> 142072 bytes .../fonts/fontawesome-webfont.woff | Bin 0 -> 83588 bytes .../fonts/fontawesome-webfont.woff2 | Bin 0 -> 66624 bytes .../foundation-icons.css | 594 ++ .../foundation-icons.eot | Bin 0 -> 54568 bytes .../foundation-icons.svg | 970 ++ .../foundation-icons.ttf | Bin 0 -> 56976 bytes .../foundation-icons.woff | Bin 0 -> 32020 bytes .../hogan/web/builds/2.0.0/hogan-2.0.0.amd.js | 580 ++ .../web/builds/2.0.0/hogan-2.0.0.common.js | 580 ++ .../hogan/web/builds/2.0.0/hogan-2.0.0.js | 576 ++ .../web/builds/2.0.0/hogan-2.0.0.min.amd.js | 5 + .../builds/2.0.0/hogan-2.0.0.min.common.js | 5 + .../hogan/web/builds/2.0.0/hogan-2.0.0.min.js | 5 + .../builds/2.0.0/hogan-2.0.0.min.mustache.js | 5 + .../web/builds/2.0.0/hogan-2.0.0.mustache.js | 623 ++ .../hogan/web/builds/2.0.0/template-2.0.0.js | 241 + .../web/builds/2.0.0/template-2.0.0.min.js | 5 + .../icomoon/dist/css/style.css | 1392 +++ .../icomoon/dist/fonts/icomoon.dev.svg | 1778 ++++ .../icomoon/dist/fonts/icomoon.eot | Bin 0 -> 67228 bytes .../icomoon/dist/fonts/icomoon.svg | 1778 ++++ .../icomoon/dist/fonts/icomoon.ttf | Bin 0 -> 67064 bytes .../icomoon/dist/fonts/icomoon.woff | Bin 0 -> 90620 bytes .../ionicons/css/ionicons.css | 1480 +++ .../ionicons/css/ionicons.min.css | 11 + .../ionicons/fonts/ionicons.eot | Bin 0 -> 120724 bytes .../ionicons/fonts/ionicons.svg | 2230 ++++ .../ionicons/fonts/ionicons.ttf | Bin 0 -> 188508 bytes .../ionicons/fonts/ionicons.woff | Bin 0 -> 67904 bytes dist/bower_components/jquery/dist/jquery.js | 9205 +++++++++++++++++ .../jquery/dist/jquery.min.js | 5 + .../jquery/dist/jquery.min.map | 1 + .../iconfont/MaterialIcons-Regular.eot | Bin 0 -> 137002 bytes .../iconfont/MaterialIcons-Regular.ttf | Bin 0 -> 122640 bytes .../iconfont/MaterialIcons-Regular.woff | Bin 0 -> 56792 bytes .../iconfont/MaterialIcons-Regular.woff2 | Bin 0 -> 42284 bytes .../iconfont/material-icons.css | 38 + .../octicons/octicons/octicons-local.ttf | Bin 0 -> 51868 bytes .../octicons/octicons/octicons.css | 224 + .../octicons/octicons/octicons.eot | Bin 0 -> 30172 bytes .../octicons/octicons/octicons.svg | 186 + .../octicons/octicons/octicons.ttf | Bin 0 -> 30004 bytes .../octicons/octicons/octicons.woff | Bin 0 -> 16740 bytes dist/bower_components/purl/purl.js | 265 + .../zeroclipboard/dist/ZeroClipboard.Core.js | 2017 ++++ .../dist/ZeroClipboard.Core.min.js | 10 + .../dist/ZeroClipboard.Core.min.map | 1 + .../zeroclipboard/dist/ZeroClipboard.js | 2581 +++++ .../zeroclipboard/dist/ZeroClipboard.min.js | 10 + .../zeroclipboard/dist/ZeroClipboard.min.map | 1 + .../zeroclipboard/dist/ZeroClipboard.swf | Bin 0 -> 6580 bytes index.html | 28 +- package.json | 4 +- 78 files changed, 51899 insertions(+), 19 deletions(-) create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.angular.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.angular.min.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.jquery.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.jquery.min.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.min.js create mode 100644 dist/bower_components/algoliasearch/dist/algoliasearch.parse.js create mode 100644 dist/bower_components/bootstrap/dist/css/bootstrap-theme.css create mode 100644 dist/bower_components/bootstrap/dist/css/bootstrap-theme.min.css create mode 100644 dist/bower_components/bootstrap/dist/css/bootstrap.css create mode 100644 dist/bower_components/bootstrap/dist/css/bootstrap.min.css create mode 100644 dist/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot create mode 100644 dist/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg create mode 100644 dist/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf create mode 100644 dist/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff create mode 100644 dist/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 create mode 100644 dist/bower_components/fixed-sticky/fixedsticky.css create mode 100644 dist/bower_components/fixed-sticky/fixedsticky.js create mode 100644 dist/bower_components/font-awesome/css/font-awesome.css create mode 100644 dist/bower_components/font-awesome/css/font-awesome.min.css create mode 100644 dist/bower_components/font-awesome/fonts/fontawesome-webfont.eot create mode 100644 dist/bower_components/font-awesome/fonts/fontawesome-webfont.svg create mode 100644 dist/bower_components/font-awesome/fonts/fontawesome-webfont.ttf create mode 100644 dist/bower_components/font-awesome/fonts/fontawesome-webfont.woff create mode 100644 dist/bower_components/font-awesome/fonts/fontawesome-webfont.woff2 create mode 100644 dist/bower_components/foundation-icon-fonts/foundation-icons.css create mode 100644 dist/bower_components/foundation-icon-fonts/foundation-icons.eot create mode 100644 dist/bower_components/foundation-icon-fonts/foundation-icons.svg create mode 100644 dist/bower_components/foundation-icon-fonts/foundation-icons.ttf create mode 100644 dist/bower_components/foundation-icon-fonts/foundation-icons.woff create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.amd.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.common.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.min.amd.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.min.common.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.min.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.min.mustache.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/hogan-2.0.0.mustache.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/template-2.0.0.js create mode 100644 dist/bower_components/hogan/web/builds/2.0.0/template-2.0.0.min.js create mode 100644 dist/bower_components/icomoon/dist/css/style.css create mode 100644 dist/bower_components/icomoon/dist/fonts/icomoon.dev.svg create mode 100644 dist/bower_components/icomoon/dist/fonts/icomoon.eot create mode 100644 dist/bower_components/icomoon/dist/fonts/icomoon.svg create mode 100644 dist/bower_components/icomoon/dist/fonts/icomoon.ttf create mode 100644 dist/bower_components/icomoon/dist/fonts/icomoon.woff create mode 100644 dist/bower_components/ionicons/css/ionicons.css create mode 100644 dist/bower_components/ionicons/css/ionicons.min.css create mode 100644 dist/bower_components/ionicons/fonts/ionicons.eot create mode 100644 dist/bower_components/ionicons/fonts/ionicons.svg create mode 100644 dist/bower_components/ionicons/fonts/ionicons.ttf create mode 100644 dist/bower_components/ionicons/fonts/ionicons.woff create mode 100644 dist/bower_components/jquery/dist/jquery.js create mode 100644 dist/bower_components/jquery/dist/jquery.min.js create mode 100644 dist/bower_components/jquery/dist/jquery.min.map create mode 100644 dist/bower_components/material-design-icons/iconfont/MaterialIcons-Regular.eot create mode 100644 dist/bower_components/material-design-icons/iconfont/MaterialIcons-Regular.ttf create mode 100644 dist/bower_components/material-design-icons/iconfont/MaterialIcons-Regular.woff create mode 100644 dist/bower_components/material-design-icons/iconfont/MaterialIcons-Regular.woff2 create mode 100644 dist/bower_components/material-design-icons/iconfont/material-icons.css create mode 100644 dist/bower_components/octicons/octicons/octicons-local.ttf create mode 100644 dist/bower_components/octicons/octicons/octicons.css create mode 100644 dist/bower_components/octicons/octicons/octicons.eot create mode 100644 dist/bower_components/octicons/octicons/octicons.svg create mode 100644 dist/bower_components/octicons/octicons/octicons.ttf create mode 100644 dist/bower_components/octicons/octicons/octicons.woff create mode 100644 dist/bower_components/purl/purl.js create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.Core.js create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.js create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.map create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.js create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.min.js create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.min.map create mode 100644 dist/bower_components/zeroclipboard/dist/ZeroClipboard.swf diff --git a/Gruntfile.js b/Gruntfile.js index 0a208db11..b7ef4e412 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,28 +1,80 @@ + module.exports = function(grunt) { + grunt.initConfig({ + 'merge-json': { icons: { src: [ "data/icons-*.json" ], dest: "data/icons.json" } }, + 'compile-handlebars': { remote: { template: 'templates/batch.handlebars', templateData: 'data/icons.json', output: 'data/batch.json' } - } + }, + + 'copy': { + 'vendor': { + files: [ + + // icons: glyphicons + 'bootstrap/dist/css/*.css', + 'bootstrap/dist/fonts/*.{eot,svg,ttf,woff,woff2}', + + // icons: fontawesome + 'font-awesome/css/*.css', + 'font-awesome/fonts/*.{eot,svg,ttf,woff,woff2}', + + // icons: foundation + 'foundation-icon-fonts/*.css', + 'foundation-icon-fonts/*.{eot,svg,ttf,woff,woff2}', + + // icons: icomoon + 'icomoon/dist/css/*.css', + 'icomoon/dist/fonts/*.{eot,svg,ttf,woff,woff2}', + + // icons: ionicons + 'ionicons/css/*.css', + 'ionicons/fonts/*.{eot,svg,ttf,woff,woff2}', + + // icons: material + 'material-design-icons/iconfont/*.css', + 'material-design-icons/iconfont/*.{eot,svg,ttf,woff,woff2}', + + // icons: octicons + 'octicons/octicons/*.css', + 'octicons/octicons/*.{eot,svg,ttf,woff,woff2}', + + // other: + 'fixed-sticky/fixedsticky.*', + 'algoliasearch/dist/*', + 'hogan/web/builds/2.0.0/*', + 'jquery/dist/*', + 'purl/purl.js', + 'zeroclipboard/dist/*' + + ].map(function(path) { + return {expand: true, src: ['bower_components/' + path], dest: 'dist/', filter: 'isFile'}; + }) + } + } // copy + }); grunt.loadNpmTasks('grunt-merge-json'); grunt.loadNpmTasks('grunt-compile-handlebars'); + grunt.loadNpmTasks('grunt-contrib-copy'); grunt.registerTask('default', function(){ grunt.task.run('build'); }); - grunt.registerTask('build', ['merge-json', 'compile-handlebars']); + grunt.registerTask('build', ['merge-json', 'compile-handlebars', 'copy']); grunt.registerTask('index', 'Push batch.json to Algolia\'s server', function() { // required api key diff --git a/bower.json b/bower.json index 2d27c70d1..bdb029c46 100644 --- a/bower.json +++ b/bower.json @@ -16,7 +16,8 @@ "foundation-icon-fonts": "zurb/foundation-icon-fonts", "icomoon": "0.0.3", "ionicons": "2.0.1", - "material-design-icons": "2.0.0", - "octicons": "3.3.0" + "material-design-icons": "2.1.3", + "octicons": "3.3.0", + "algoliasearch": "~2.9.7" } } diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.angular.js b/dist/bower_components/algoliasearch/dist/algoliasearch.angular.js new file mode 100644 index 000000000..ec791b8d4 --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.angular.js @@ -0,0 +1,2678 @@ +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +var ALGOLIA_VERSION = '2.9.7'; + +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +/* + * Algolia Search library initialization + * @param applicationID the application ID you have in your admin interface + * @param apiKey a valid API key for the service + * @param methodOrOptions the hash of parameters for initialization. It can contains: + * - method (optional) specify if the protocol used is http or https (http by default to make the first search query faster). + * You need to use https is you are doing something else than just search queries. + * - hosts (optional) the list of hosts that you have received for the service + * - dsn (optional) set to true if your account has the Distributed Search Option + * - dsnHost (optional) override the automatic computation of dsn hostname + */ +var AlgoliaSearch = function(applicationID, apiKey, methodOrOptions, resolveDNS, hosts) { + var self = this; + this.applicationID = applicationID; + this.apiKey = apiKey; + this.dsn = true; + this.dsnHost = null; + this.hosts = []; + this.currentHostIndex = 0; + this.requestTimeoutInMs = 2000; + this.extraHeaders = []; + this.jsonp = null; + this.options = {}; + + // make sure every client instance has it's own cache + this.cache = {}; + + var method; + var tld = 'net'; + if (typeof methodOrOptions === 'string') { // Old initialization + method = methodOrOptions; + } else { + // Take all option from the hash + var options = methodOrOptions || {}; + this.options = options; + if (!this._isUndefined(options.method)) { + method = options.method; + } + if (!this._isUndefined(options.tld)) { + tld = options.tld; + } + if (!this._isUndefined(options.dsn)) { + this.dsn = options.dsn; + } + if (!this._isUndefined(options.hosts)) { + hosts = options.hosts; + } + if (!this._isUndefined(options.dsnHost)) { + this.dsnHost = options.dsnHost; + } + if (!this._isUndefined(options.requestTimeoutInMs)) { + this.requestTimeoutInMs = +options.requestTimeoutInMs; + } + if (!this._isUndefined(options.jsonp)) { + this.jsonp = options.jsonp; + } + } + // If hosts is undefined, initialize it with applicationID + if (this._isUndefined(hosts)) { + hosts = [ + this.applicationID + '-1.algolianet.com', + this.applicationID + '-2.algolianet.com', + this.applicationID + '-3.algolianet.com' + ]; + } + // detect is we use http or https + this.host_protocol = 'http://'; + if (this._isUndefined(method) || method === null) { + this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://'; + } else if (method === 'https' || method === 'HTTPS') { + this.host_protocol = 'https://'; + } + // Add protocol to hosts + for (var i = 0; i < hosts.length; ++i) { + this.hosts.push(this.host_protocol + hosts[i]); + } + // then add Distributed Search Network host if there is one + if (this.dsn || this.dsnHost != null) { + if (this.dsnHost) { + this.hosts.unshift(this.host_protocol + this.dsnHost); + } else { + this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld); + } + } + // angular dependencies injection + if (this.options.angular) { + this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) { + self.options.angular.$q = $q; + self.options.angular.$http = $http; + }]); + } + + this._ua = this.options._ua || 'Algolia for vanilla JavaScript ' + window.ALGOLIA_VERSION; +}; + +// This holds the number of JSONP requests done accross clients +// It's used as part of the ?callback=JSONP_$JSONPCounter when we do JSONP requests +AlgoliaSearch.JSONPCounter = 0; + +function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) { + + function _getHitExplanationForOneAttr_recurse(obj, foundWords) { + var res = []; + if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) { + var match = false; + for (var j = 0; j < obj.matchedWords.length; ++j) { + var word = obj.matchedWords[j]; + if (!(word in foundWords)) { + foundWords[word] = 1; + match = true; + } + } + if (match) { + res.push(obj.value); + } + } else if (Object.prototype.toString.call(obj) === '[object Array]') { + for (var i = 0; i < obj.length; ++i) { + var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords); + res = res.concat(array); + } + } else if (typeof obj === 'object') { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)){ + res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords)); + } + } + } + return res; + } + + function _getHitExplanationForOneAttr(hit, foundWords, attr) { + var base = hit._highlightResult || hit; + if (attr.indexOf('.') === -1) { + if (attr in base) { + return _getHitExplanationForOneAttr_recurse(base[attr], foundWords); + } + return []; + } + var array = attr.split('.'); + var obj = base; + for (var i = 0; i < array.length; ++i) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + var res = []; + for (var j = 0; j < obj.length; ++j) { + res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.'))); + } + return res; + } + if (array[i] in obj) { + obj = obj[array[i]]; + } else { + return []; + } + } + return _getHitExplanationForOneAttr_recurse(obj, foundWords); + } + + var res = {}; + var foundWords = {}; + var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute); + res.title = (title.length > 0) ? title[0] : ''; + res.subtitles = []; + + if (typeof otherAttributes !== 'undefined') { + for (var i = 0; i < otherAttributes.length; ++i) { + var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]); + for (var j = 0; j < attr.length; ++j) { + res.subtitles.push({ attr: otherAttributes[i], value: attr[j] }); + } + } + } + return res; +} + + +AlgoliaSearch.prototype = { + /* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + deleteIndex: function(indexName, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + callback: callback }); + }, + /** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + moveIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'move', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + + }, + /** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + copyIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'copy', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + }, + /** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + getLogs: function(callback, offset, length) { + if (this._isUndefined(offset)) { + offset = 0; + } + if (this._isUndefined(length)) { + length = 10; + } + + return this._jsonRequest({ method: 'GET', + url: '/1/logs?offset=' + offset + '&length=' + length, + callback: callback }); + }, + /* + * List all existing indexes (paginated) + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with index list or error description if success is false. + * @param page The page to retrieve, starting at 0. + */ + listIndexes: function(callback, page) { + var params = typeof page !== 'undefined' ? '?page=' + page : ''; + return this._jsonRequest({ method: 'GET', + url: '/1/indexes' + params, + callback: callback }); + }, + + /* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ + initIndex: function(indexName) { + return new this.Index(this, indexName); + }, + /* + * List all existing user keys with their associated ACLs + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys', + callback: callback }); + }, + /* + * Get ACL of a user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + return this.addUserKeyWithValidity(acls, 0, 0, 0, callback); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this._jsonRequest({ method: 'POST', + url: '/1/keys', + body: aclsObject, + callback: callback }); + }, + + /** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ + setSecurityTags: function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + this.tagFilters = tags; + }, + + /** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ + setUserToken: function(userToken) { + this.userToken = userToken; + }, + + /* + * Initialize a new batch of search queries + */ + startQueriesBatch: function() { + this.batch = []; + }, + /* + * Add a search query in the batch + * + * @param query the full text query + * @param args (optional) if set, contains an object with query parameters: + * - attributes: an array of object attribute names to retrieve + * (if not set all attributes are retrieve) + * - attributesToHighlight: an array of object attribute names to highlight + * (if not set indexed attributes are highlighted) + * - minWordSizefor1Typo: the minimum number of characters to accept one typo. + * Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters to accept two typos. + * Defaults to 7. + * - getRankingInfo: if set, the result hits will contain ranking information in + * _rankingInfo attribute + * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0. + * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10. + */ + addQueryInBatch: function(indexName, query, args) { + var params = 'query=' + encodeURIComponent(query); + if (!this._isUndefined(args) && args !== null) { + params = this._getSearchParams(args, params); + } + this.batch.push({ indexName: indexName, params: params }); + }, + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Launch the batch of queries using XMLHttpRequest. + * (Optimized for browser using a POST query to minimize number of OPTIONS queries) + * + * @param callback the function that will receive results + * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime. + */ + sendQueriesBatch: function(callback, delay) { + var as = this; + var params = {requests: []}; + for (var i = 0; i < as.batch.length; ++i) { + params.requests.push(as.batch[i]); + } + window.clearTimeout(as.onDelayTrigger); + if (!this._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + as._sendQueriesBatch(params, callback); + }, delay); + as.onDelayTrigger = onDelayTrigger; + } else { + return this._sendQueriesBatch(params, callback); + } + }, + + /** + * Set the number of milliseconds a request can take before automatically being terminated. + * + * @param {Number} milliseconds + */ + setRequestTimeout: function(milliseconds) + { + if (milliseconds) { + this.requestTimeoutInMs = parseInt(milliseconds, 10); + } + }, + + /* + * Index class constructor. + * You should not use this method directly but use initIndex() function + */ + Index: function(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; + }, + /** + * Add an extra field to the HTTP request + * + * @param key the header field name + * @param value the header field value + */ + setExtraHeader: function(key, value) { + this.extraHeaders.push({ key: key, value: value}); + }, + + _sendQueriesBatch: function(params, callback) { + if (this.jsonp === null) { + var self = this; + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: function(success, content) { + if (!success) { + // retry first with JSONP + self.jsonp = true; + self._sendQueriesBatch(params, callback); + } else { + self.jsonp = false; + callback && callback(success, content); + } + } + }); + } else if (this.jsonp) { + var jsonpParams = ''; + for (var i = 0; i < params.requests.length; ++i) { + var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params; + jsonpParams += i + '=' + encodeURIComponent(q) + '&'; + } + var pObj = {params: jsonpParams}; + return this._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/*', + body: pObj, + callback: callback }); + } else { + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: callback}); + } + }, + /* + * Wrapper that try all hosts to maximize the quality of service + */ + _jsonRequest: function(opts) { + var self = this; + var callback = opts.callback; + var cache = null; + var cacheID = opts.url; + var deferred = null; + if (this.options.jQuery) { + deferred = this.options.jQuery.$.Deferred(); + deferred.promise = deferred.promise(); // promise is a property in angular + } else if (this.options.angular) { + deferred = this.options.angular.$q.defer(); + } + + if (!this._isUndefined(opts.body)) { + cacheID = opts.url + '_body_' + JSON.stringify(opts.body); + } + if (!this._isUndefined(opts.cache)) { + cache = opts.cache; + if (!this._isUndefined(cache[cacheID])) { + if (!this._isUndefined(callback) && callback) { + setTimeout(function () { callback(true, cache[cacheID]); }, 1); + } + deferred && deferred.resolve(cache[cacheID]); + return deferred && deferred.promise; + } + } + + opts.successiveRetryCount = 0; + var impl = function() { + + if (opts.successiveRetryCount >= self.hosts.length) { + var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' }; + if (!self._isUndefined(callback) && callback) { + opts.successiveRetryCount = 0; + callback(false, error); + } + deferred && deferred.reject(error); + return; + } + opts.callback = function(retry, success, body) { + if (success && !self._isUndefined(opts.cache)) { + cache[cacheID] = body; + } + if (!success && retry) { + self.currentHostIndex = ++self.currentHostIndex % self.hosts.length; + opts.successiveRetryCount += 1; + impl(); + } else { + opts.successiveRetryCount = 0; + deferred && (success ? deferred.resolve(body) : deferred.reject(body)); + if (!self._isUndefined(callback) && callback) { + callback(success, body); + } + } + }; + opts.hostname = self.hosts[self.currentHostIndex]; + self._jsonRequestByHost(opts); + }; + impl(); + + return deferred && deferred.promise; + }, + + _jsonRequestByHost: function(opts) { + var self = this; + var url = opts.hostname + opts.url; + + if (this.jsonp) { + this._makeJsonpRequestByHost(url, opts); + } else if (this.options.jQuery) { + this._makejQueryRequestByHost(url, opts); + } else if (this.options.angular) { + this._makeAngularRequestByHost(url, opts); + } else { + this._makeXmlHttpRequestByHost(url, opts); + } + }, + + /** + * Make a $http + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeAngularRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.angular.$http({ + url: url, + method: opts.method, + data: body, + cache: false, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)) + }).then(function(response) { + opts.callback(false, true, response.data); + }, function(response) { + if (response.status === 0) { + // xhr.timeout is not handled by Angular.js right now + // let's retry + opts.callback(true, false, response.data); + } else if (response.status == 400 || response.status === 403 || response.status === 404) { + opts.callback(false, false, response.data); + } else { + opts.callback(true, false, response.data); + } + }); + }, + + /** + * Make a $.ajax + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makejQueryRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.jQuery.$.ajax(url, { + type: opts.method, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)), + dataType: 'json', + data: body, + error: function(xhr, textStatus, error) { + if (textStatus === 'timeout') { + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) { + opts.callback(false, false, xhr.responseJSON ); + } else { + opts.callback(true, false, { 'message': error } ); + } + }, + success: function(data, textStatus, xhr) { + opts.callback(false, true, data); + } + }); + }, + + /** + * Make a JSONP request + * + * @param url request url (includes endpoint and path) + * @param opts all request options + */ + _makeJsonpRequestByHost: function(url, opts) { + if (opts.method !== 'GET') { + opts.callback(true, false, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' }); + return; + } + + var cbCalled = false; + var timedOut = false; + + AlgoliaSearch.JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cb = 'algoliaJSONP_' + AlgoliaSearch.JSONPCounter; + var done = false; + var ontimeout; + var success; + var clean; + + window[cb] = function(data) { + try { delete window[cb]; } catch (e) { window[cb] = undefined; } + + if (timedOut) { + return; + } + + var status = + data && data.message && data.status || + data && 200; + + var ok = status === 200; + var retry = !ok && status !== 400 && status !== 403 && status !== 404; + cbCalled = true; + opts.callback(retry, ok, data); + }; + + script.type = 'text/javascript'; + url += '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey; + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + if (opts.body && opts.body.params) { + url += '&' + opts.body.params; + } + + ontimeout = setTimeout(function() { + timedOut = true; + clean(); + + opts.callback(true, false, { 'message': 'Timeout - Failed to load JSONP script.' }); + }, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + + success = function() { + if (done || timedOut) { + return; + } + + done = true; + clean(); + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + } + }; + + clean = function() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + + try { + delete window[cb]; + delete window[cb + '_loaded']; + } catch (e) { + window[cb] = null; + window[cb + '_loaded'] = null; + } + }; + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = function() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + }; + + script.onload = function() { + success(); + }; + + script.onerror = function() { + if (done || timedOut) { + return; + } + + clean(); + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + }; + + script.async = true; + script.defer = true; + script.src = url; + + head.appendChild(script); + }, + + /** + * Make a XmlHttpRequest + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeXmlHttpRequestByHost: function(url, opts) { + // no cors or XDomainRequest, no request + if (!this._support.cors && !this._support.hasXDomainRequest) { + // very old browser, not supported + opts.callback(false, false, { 'message': 'CORS not supported' }); + return; + } + + var body = null; + var request = this._support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var ontimeout; + var self = this; + var timedOut; + var timeoutListener; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + timeoutListener = function() { + if (!self._support.timeout) { + timedOut = true; + request.abort(); + } + + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + }; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (request instanceof XMLHttpRequest) { + request.open(opts.method, url, true); + } else { + request.open(opts.method, url); + } + + if (this._support.cors && body !== null && opts.method !== 'GET') { + request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + request.onload = function(/*event*/) { + // When browser does not supports request.timeout, we can + // have both a load and timeout event + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + var response = null; + + try { + response = JSON.parse(request.responseText); + } catch(e) {} + + var status = + // XHR provides a `status` property + request.status || + + // XDR does not have a `status` property, + // we rely on our own API response `status`, only + // provided when an error occurs, so we expect a .message + response && response.message && response.status || + + // XDR default to success when no response.status + response && 200; + + var success = status === 200 || status === 201; + var retry = !success && status !== 400 && status !== 403 && status !== 404; + + opts.callback(retry, success, response); + }; + + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + request.onprogress = function noop() {}; + + if (this._support.timeout) { + // .timeout supported by both XHR and XDR, + // we do receive timeout event, tested + request.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1); + + request.ontimeout = timeoutListener; + } else { + ontimeout = setTimeout(timeoutListener, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + } + + request.onerror = function(event) { + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + opts.callback(true, false, { 'message': 'Could not connect to host', 'error': event } ); + }; + + request.send(body); + }, + + /* + * Transform search param object in query string + */ + _getSearchParams: function(args, params) { + if (this._isUndefined(args) || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args.hasOwnProperty(key)) { + params += (params.length === 0) ? '?' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]); + } + } + return params; + }, + _isUndefined: function(obj) { + return obj === void 0; + }, + + _support: { + hasXMLHttpRequest: 'XMLHttpRequest' in window, + hasXDomainRequest: 'XDomainRequest' in window, + cors: 'withCredentials' in new XMLHttpRequest(), + timeout: 'timeout' in new XMLHttpRequest() + } +}; + +/* + * Contains all the functions related to one index + * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object + */ +AlgoliaSearch.prototype.Index.prototype = { + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Add an object in this index + * + * @param content contains the javascript object to add inside the index + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + * @param objectID (optional) an objectID you want to attribute to this object + * (if the attribute already exist the old object will be overwrite) + */ + addObject: function(content, callback, objectID) { + var indexObj = this; + if (this.as._isUndefined(objectID)) { + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName), + body: content, + callback: callback }); + } else { + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + body: content, + callback: callback }); + } + + }, + /* + * Add several objects + * + * @param objects contains an array of objects to add + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + addObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'addObject', + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Get an object from this index + * + * @param objectID the unique identifier of the object to retrieve + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the object to retrieve or the error message if a failure occured + * @param attributes (optional) if set, contains the array of attribute names to retrieve + */ + getObject: function(objectID, callback, attributes) { + if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) { + attributes = callback; + callback = null; + } + var indexObj = this; + var params = ''; + if (!this.as._isUndefined(attributes)) { + params = '?attributes='; + for (var i = 0; i < attributes.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attributes[i]; + } + } + + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + callback: callback }); + }, + + /* + * Update partially an object (only update attributes passed in argument) + * + * @param partialObject contains the javascript attributes to override, the + * object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + partialUpdateObject: function(partialObject, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial', + body: partialObject, + callback: callback }); + }, + /* + * Partially Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + partialUpdateObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'partialUpdateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Override the content of object + * + * @param object contains the javascript object to save, the object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObject: function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + callback: callback }); + }, + /* + * Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Delete an object from the index + * + * @param objectID the unique identifier of object to delete + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + deleteObject: function(objectID, callback) { + if (objectID === null || objectID.length === 0) { + callback(false, { message: 'empty objectID'}); + return; + } + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + callback: callback }); + }, + /* + * Search inside the index using XMLHttpRequest request (Using a POST query to + * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). + * + * @param query the full text query + * @param callback the result callback with two arguments: + * success: boolean set to true if the request was successfull. If false, the content contains the error. + * content: the server answer that contains the list of results. + * @param args (optional) if set, contains an object with query parameters: + * - page: (integer) Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. + * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). + * Attributes are separated with a comma (for example "name,address"). + * You can also use an array (for example ["name","address"]). + * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. + * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. + * Attributes are separated by a comma. You can also use an array (for example ["name","address"]). + * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. + * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. + * A matchLevel is returned for each highlighted attribute and can contain: + * - full: if all the query terms were found in the attribute, + * - partial: if only some of the query terms were found, + * - none: if none of the query terms were found. + * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). + * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). + * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). By default no snippet is computed. + * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. + * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. + * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). + * For example aroundLatLng=47.316669,5.016670). + * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision + * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). + * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. + * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. + * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. + * You can also use an array (for example numericFilters: ["price>100","price<1000"]). + * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. + * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). + * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). + * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). + * - facetFilters: filter the query by a list of facets. + * Facets are separated by commas and each facet is encoded as `attributeName:value`. + * For example: `facetFilters=category:Book,author:John%20Doe`. + * You can also use an array (for example `["category:Book","author:John%20Doe"]`). + * - facets: List of object attributes that you want to use for faceting. + * Comma separated list: `"category,author"` or array `['category','author']` + * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. + * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. + * - queryType: select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. + * Comma separated and array are accepted. + * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. + * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, + * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best + * one is kept and others are removed. + * - restrictSearchableAttributes: List of attributes you want to use for textual search (must be a subset of the attributesToIndex index setting) + * either comma separated or as an array + * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime. + */ + search: function(query, callback, args, delay) { + if (query === undefined || query === null) { + query = ''; + } + + // no query = getAllObjects + if (typeof query === 'function') { + callback = query; + query = ''; + } + + if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) { + args = callback; + callback = null; + } + + var indexObj = this; + var params = 'query=' + encodeURIComponent(query); + if (!this.as._isUndefined(args) && args !== null) { + params = this.as._getSearchParams(args, params); + } + window.clearTimeout(indexObj.onDelayTrigger); + if (!this.as._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + indexObj._search(params, callback); + }, delay); + indexObj.onDelayTrigger = onDelayTrigger; + } else { + return this._search(params, callback); + } + }, + + /* + * Browse all index content + * + * @param page Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. + */ + browse: function(page, callback, hitsPerPage) { + if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) { + hitsPerPage = callback; + callback = null; + } + var indexObj = this; + var params = '?page=' + page; + if (!this.as._isUndefined(hitsPerPage)) { + params += '&hitsPerPage=' + hitsPerPage; + } + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params, + callback: callback }); + }, + + /* + * Get a Typeahead.js adapter + * @param searchParams contains an object with query parameters (see search for details) + */ + ttAdapter: function(params) { + var self = this; + return function(query, cb) { + self.search(query, function(success, content) { + if (success) { + cb(content.hits); + } else { + cb(content && content.message); + } + }, params); + }; + }, + + /* + * Wait the publication of a task on the server. + * All server task are asynchronous and you can check with this method that the task is published. + * + * @param taskID the id of the task returned by server + * @param callback the result callback with with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains the list of results + */ + waitTask: function(taskID, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID, + callback: function(success, body) { + if (success) { + if (body.status === 'published') { + callback(true, body); + } else { + setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100); + } + } else { + callback(false, body); + } + }}); + }, + + /* + * This function deletes the index content. Settings and index specific API keys are kept untouched. + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + clearIndex: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + callback: callback }); + }, + /* + * Get settings of this index + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + getSettings: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + callback: callback }); + }, + + /* + * Set settings for this index + * + * @param settigns the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). + * By default no snippet is computed. If set to null, no snippet is computed. + * - attributesToIndex: (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. + * This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to + * retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of + * the list will be considered more important than matches in attributes further down the list. + * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable + * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled + * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreassing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by attributesToIndex, + * - exact: + * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. + * For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV + * show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. + * For example `"customRanking" => ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer or the error message if a failure occured + */ + setSettings: function(settings, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + body: settings, + callback: callback }); + }, + /* + * List all existing user keys associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + callback: callback }); + }, + /* + * Get ACL of a user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /// + /// Internal methods only after this line + /// + _search: function(params, callback) { + var pObj = {params: params}; + if (this.as.jsonp === null) { + var self = this; + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: function(success, content) { + var status = content && content.status; + if (success || status && Math.floor(status / 100) === 4 || Math.floor(status / 100) === 1) { + self.as.jsonp = false; + callback && callback(success, content); + } else { + self.as.jsonp = true; + self._search(params, callback); + } + } + }); + } else if (this.as.jsonp) { + return this.as._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: pObj, + callback: callback }); + } else { + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: callback}); + } + }, + + // internal attributes + as: null, + indexName: null, + typeAheadArgs: null, + typeAheadValueOption: null +}; + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + var extend = function(out) { + out = out || {}; + for (var i = 1; i < arguments.length; i++) { + if (!arguments[i]) { + continue; + } + for (var key in arguments[i]) { + if (arguments[i].hasOwnProperty(key)) { + out[key] = arguments[i][key]; + } + } + } + return out; + }; + + /** + * Algolia Search Helper providing faceting and disjunctive faceting + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets, the list of disjunctive facets and the default facet filters + */ + window.AlgoliaSearchHelper = function(client, index, options) { + /// Default options + var defaults = { + facets: [], // list of facets to compute + disjunctiveFacets: [], // list of disjunctive facets to compute + hitsPerPage: 20, // number of hits per page + defaultFacetFilters: [] // the default list of facetFilters + }; + + this.init(client, index, extend({}, defaults, options)); + }; + + AlgoliaSearchHelper.prototype = { + /** + * Initialize a new AlgoliaSearchHelper + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets + * @return {AlgoliaSearchHelper} + */ + init: function(client, index, options) { + this.client = client; + this.index = index; + this.options = options; + this.page = 0; + this.refinements = {}; + this.excludes = {}; + this.disjunctiveRefinements = {}; + this.extraQueries = []; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + */ + search: function(q, searchCallback, searchParams) { + this.q = q; + this.searchCallback = searchCallback; + this.searchParams = searchParams || {}; + this.page = this.page || 0; + this.refinements = this.refinements || {}; + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this._search(); + }, + + /** + * Remove all refinements (disjunctive + conjunctive) + */ + clearRefinements: function() { + this.disjunctiveRefinements = {}; + this.refinements = {}; + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + this.disjunctiveRefinements[facet][value] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + try { + delete this.disjunctiveRefinements[facet][value]; + } catch (e) { + this.disjunctiveRefinements[facet][value] = undefined; // IE compat + } + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = false; + }, + + /** + * Ensure a facet exclude exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = true; + }, + + /** + * Ensure a facet exclude does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = false; + }, + + /** + * Toggle refinement state of an exclude + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleExclude: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':-' + value; + this.excludes[refinement] = !this.excludes[refinement]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Toggle refinement state of a facet + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleRefine: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':' + value; + this.refinements[refinement] = !this.refinements[refinement]; + this.page = 0; + this._search(); + return true; + } + } + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) { + if (this.options.disjunctiveFacets[j] == facet) { + this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Check the refinement state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isRefined: function(facet, value) { + var refinement = facet + ':' + value; + if (this.refinements[refinement]) { + return true; + } + if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) { + return true; + } + return false; + }, + + /** + * Check the exclude state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isExcluded: function(facet, value) { + var refinement = facet + ':-' + value; + if (this.excludes[refinement]) { + return true; + } + return false; + }, + + /** + * Go to next page + */ + nextPage: function() { + this._gotoPage(this.page + 1); + }, + + /** + * Go to previous page + */ + previousPage: function() { + if (this.page > 0) { + this._gotoPage(this.page - 1); + } + }, + + /** + * Goto a page + * @param {integer} page The page number + */ + gotoPage: function(page) { + this._gotoPage(page); + }, + + /** + * Configure the page but do not trigger a reload + * @param {integer} page The page number + */ + setPage: function(page) { + this.page = page; + }, + + /** + * Configure the underlying index name + * @param {string} name the index name + */ + setIndex: function(name) { + this.index = name; + }, + + /** + * Get the underlying configured index name + */ + getIndex: function() { + return this.index; + }, + + /** + * Clear the extra queries added to the underlying batch of queries + */ + clearExtraQueries: function() { + this.extraQueries = []; + }, + + /** + * Add an extra query to the underlying batch of queries. Once you add queries + * to the batch, the 2nd parameter of the searchCallback will be an object with a `results` + * attribute listing all search results. + */ + addExtraQuery: function(index, query, params) { + this.extraQueries.push({ index: index, query: query, params: (params || {}) }); + }, + + ///////////// PRIVATE + + /** + * Goto a page + * @param {integer} page The page number + */ + _gotoPage: function(page) { + this.page = page; + this._search(); + }, + + /** + * Perform the underlying queries + */ + _search: function() { + this.client.startQueriesBatch(); + this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams()); + var disjunctiveFacets = []; + var unusedDisjunctiveFacets = {}; + var i = 0; + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (this._hasDisjunctiveRefinements(facet)) { + disjunctiveFacets.push(facet); + } else { + unusedDisjunctiveFacets[facet] = true; + } + } + for (i = 0; i < disjunctiveFacets.length; ++i) { + this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(disjunctiveFacets[i])); + } + for (i = 0; i < this.extraQueries.length; ++i) { + this.client.addQueryInBatch(this.extraQueries[i].index, this.extraQueries[i].query, this.extraQueries[i].params); + } + var self = this; + this.client.sendQueriesBatch(function(success, content) { + if (!success) { + self.searchCallback(false, content); + return; + } + var aggregatedAnswer = content.results[0]; + aggregatedAnswer.disjunctiveFacets = aggregatedAnswer.disjunctiveFacets || {}; + aggregatedAnswer.facets_stats = aggregatedAnswer.facets_stats || {}; + // create disjunctive facets from facets (disjunctive facets without refinements) + for (var facet in unusedDisjunctiveFacets) { + if (aggregatedAnswer.facets[facet] && !aggregatedAnswer.disjunctiveFacets[facet]) { + aggregatedAnswer.disjunctiveFacets[facet] = aggregatedAnswer.facets[facet]; + try { + delete aggregatedAnswer.facets[facet]; + } catch (e) { + aggregatedAnswer.facets[facet] = undefined; // IE compat + } + } + } + // aggregate the disjunctive facets + for (i = 0; i < disjunctiveFacets.length; ++i) { + for (var dfacet in content.results[i + 1].facets) { + aggregatedAnswer.disjunctiveFacets[dfacet] = content.results[i + 1].facets[dfacet]; + if (self.disjunctiveRefinements[dfacet]) { + for (var value in self.disjunctiveRefinements[dfacet]) { + // add the disjunctive reginements if it is no more retrieved + if (!aggregatedAnswer.disjunctiveFacets[dfacet][value] && self.disjunctiveRefinements[dfacet][value]) { + aggregatedAnswer.disjunctiveFacets[dfacet][value] = 0; + } + } + } + } + // aggregate the disjunctive facets stats + for (var stats in content.results[i + 1].facets_stats) { + aggregatedAnswer.facets_stats[stats] = content.results[i + 1].facets_stats[stats]; + } + } + + // Backward compatibility + aggregatedAnswer.facetStats = aggregatedAnswer.facets_stats; + + // add the excludes + for (var exclude in self.excludes) { + if (self.excludes[exclude]) { + var e = exclude.indexOf(':-'); + var facet = exclude.slice(0, e); + var value = exclude.slice(e + 2); + aggregatedAnswer.facets[facet] = aggregatedAnswer.facets[facet] || {}; + if (!aggregatedAnswer.facets[facet][value]) { + aggregatedAnswer.facets[facet][value] = 0; + } + } + } + // call the actual callback + if (self.extraQueries.length === 0) { + self.searchCallback(true, aggregatedAnswer); + } else { + // append the extra queries + var c = { results: [ aggregatedAnswer ] }; + for (i = 0; i < self.extraQueries.length; ++i) { + c.results.push(content.results[1 + disjunctiveFacets.length + i]); + } + self.searchCallback(true, c); + } + }); + }, + + /** + * Build search parameters used to fetch hits + * @return {hash} + */ + _getHitsSearchParams: function() { + var facets = []; + var i = 0; + for (i = 0; i < this.options.facets.length; ++i) { + facets.push(this.options.facets[i]); + } + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (!this._hasDisjunctiveRefinements(facet)) { + facets.push(facet); + } + } + return extend({}, { + hitsPerPage: this.options.hitsPerPage, + page: this.page, + facets: facets, + facetFilters: this._getFacetFilters() + }, this.searchParams); + }, + + /** + * Build search parameters used to fetch a disjunctive facet + * @param {string} facet the associated facet name + * @return {hash} + */ + _getDisjunctiveFacetSearchParams: function(facet) { + return extend({}, this.searchParams, { + hitsPerPage: 1, + page: 0, + attributesToRetrieve: [], + attributesToHighlight: [], + attributesToSnippet: [], + facets: facet, + facetFilters: this._getFacetFilters(facet), + analytics: false + }); + }, + + /** + * Test if there are some disjunctive refinements on the facet + */ + _hasDisjunctiveRefinements: function(facet) { + for (var value in this.disjunctiveRefinements[facet]) { + if (this.disjunctiveRefinements[facet][value]) { + return true; + } + } + return false; + }, + + /** + * Build facetFilters parameter based on current refinements + * @param {string} facet if set, the current disjunctive facet + * @return {hash} + */ + _getFacetFilters: function(facet) { + var facetFilters = []; + if (this.options.defaultFacetFilters) { + for (var i = 0; i < this.options.defaultFacetFilters.length; ++i) { + facetFilters.push(this.options.defaultFacetFilters[i]); + } + } + for (var refinement in this.refinements) { + if (this.refinements[refinement]) { + facetFilters.push(refinement); + } + } + for (var refinement in this.excludes) { + if (this.excludes[refinement]) { + facetFilters.push(refinement); + } + } + for (var disjunctiveRefinement in this.disjunctiveRefinements) { + if (disjunctiveRefinement != facet) { + var refinements = []; + for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) { + if (this.disjunctiveRefinements[disjunctiveRefinement][value]) { + refinements.push(disjunctiveRefinement + ':' + value); + } + } + if (refinements.length > 0) { + facetFilters.push(refinements); + } + } + } + return facetFilters; + } + }; +})(); + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + + /** + * Algolia Places API + * @param {string} Your application ID + * @param {string} Your API Key + */ + window.AlgoliaPlaces = function(applicationID, apiKey) { + this.init(applicationID, apiKey); + }; + + AlgoliaPlaces.prototype = { + /** + * @param {string} Your application ID + * @param {string} Your API Key + */ + init: function(applicationID, apiKey) { + this.client = new AlgoliaSearch(applicationID, apiKey, 'http', true, ['places-1.algolia.io', 'places-2.algolia.io', 'places-3.algolia.io']); + this.cache = {}; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + * @param {hash} the list of search parameters + */ + search: function(q, searchCallback, searchParams) { + var indexObj = this; + var params = 'query=' + encodeURIComponent(q); + if (!this.client._isUndefined(searchParams) && searchParams != null) { + params = this.client._getSearchParams(searchParams, params); + } + var pObj = {params: params, apiKey: this.client.apiKey, appID: this.client.applicationID}; + this.client._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/places/query', + body: pObj, + callback: searchCallback, + removeCustomHTTPHeaders: true }); + } + }; +})(); + +/* + json2.js + 2014-02-04 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx, + escapable, + gap, + indent, + meta, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); + +/* global angular */ +angular.module('algoliasearch', []) + .service('algolia', ['$injector', function ($injector) { + return { + Client: function(applicationID, apiKey, options) { + options = options || {}; + options.angular = { + '$injector': $injector + }; + options._ua = 'Algolia for AngularJS ' + window.ALGOLIA_VERSION; + return new AlgoliaSearch(applicationID, apiKey, options); + } + }; + }]); diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.angular.min.js b/dist/bower_components/algoliasearch/dist/algoliasearch.angular.min.js new file mode 100644 index 000000000..83fec0505 --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.angular.min.js @@ -0,0 +1,7 @@ +/*! + * algoliasearch 2.9.7 + * https://github.com/algolia/algoliasearch-client-js + * Copyright 2014 Algolia SAS; Licensed MIT + */ + +function AlgoliaExplainResults(a,b,c){function d(a,b){var c=[];if("object"==typeof a&&"matchedWords"in a&&"value"in a){for(var e=!1,f=0;f0?h[0]:"",f.subtitles=[],"undefined"!=typeof c)for(var i=0;i0))return this._sendQueriesBatch(d,a);var f=window.setTimeout(function(){c._sendQueriesBatch(d,a)},b);c.onDelayTrigger=f},setRequestTimeout:function(a){a&&(this.requestTimeoutInMs=parseInt(a,10))},Index:function(a,b){this.indexName=b,this.as=a,this.typeAheadArgs=null,this.typeAheadValueOption=null,this.cache={}},setExtraHeader:function(a,b){this.extraHeaders.push({key:a,value:b})},_sendQueriesBatch:function(a,b){if(null===this.jsonp){var c=this;return this._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/*/queries",body:a,callback:function(d,e){d?(c.jsonp=!1,b&&b(d,e)):(c.jsonp=!0,c._sendQueriesBatch(a,b))}})}if(this.jsonp){for(var d="",e=0;e=b.hosts.length){var h={message:"Cannot connect the Algolia's Search API. Please send an email to support@algolia.com to report the issue."};return!b._isUndefined(c)&&c&&(a.successiveRetryCount=0,c(!1,h)),void(f&&f.reject(h))}a.callback=function(h,i,j){i&&!b._isUndefined(a.cache)&&(d[e]=j),!i&&h?(b.currentHostIndex=++b.currentHostIndex%b.hosts.length,a.successiveRetryCount+=1,g()):(a.successiveRetryCount=0,f&&(i?f.resolve(j):f.reject(j)),!b._isUndefined(c)&&c&&c(i,j))},a.hostname=b.hosts[b.currentHostIndex],b._jsonRequestByHost(a)};return g(),f&&f.promise},_jsonRequestByHost:function(a){var b=a.hostname+a.url;this.jsonp?this._makeJsonpRequestByHost(b,a):this.options.jQuery?this._makejQueryRequestByHost(b,a):this.options.angular?this._makeAngularRequestByHost(b,a):this._makeXmlHttpRequestByHost(b,a)},_makeAngularRequestByHost:function(a,b){var c=null;this._isUndefined(b.body)||(c=JSON.stringify(b.body)),a+=(-1===a.indexOf("?")?"?":"&")+"X-Algolia-API-Key="+this.apiKey,a+="&X-Algolia-Application-Id="+this.applicationID,this.userToken&&(a+="&X-Algolia-UserToken="+encodeURIComponent(this.userToken)),this.tagFilters&&(a+="&X-Algolia-TagFilters="+encodeURIComponent(this.tagFilters)),a+="&X-Algolia-Agent="+encodeURIComponent(this._ua);for(var d=0;d0))return this._search(f,b);var g=window.setTimeout(function(){e._search(f,b)},d);e.onDelayTrigger=g},browse:function(a,b,c){+b>0&&(this.as._isUndefined(c)||!c)&&(c=b,b=null);var d=this,e="?page="+a;return this.as._isUndefined(c)||(e+="&hitsPerPage="+c),this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/browse"+e,callback:b})},ttAdapter:function(a){var b=this;return function(c,d){b.search(c,function(a,b){d(a?b.hits:b&&b.message)},a)}},waitTask:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/task/"+a,callback:function(d,e){d?"published"===e.status?b(!0,e):setTimeout(function(){c.waitTask(a,b)},100):b(!1,e)}})},clearIndex:function(a){var b=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/clear",callback:a})},getSettings:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/settings",callback:a})},setSettings:function(a,b){var c=this;return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/settings",body:a,callback:b})},listUserKeys:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/keys",callback:a})},getUserKeyACL:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},deleteUserKey:function(a,b){var c=this;return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},addUserKey:function(a,b){var c=this,d={};return d.acl=a,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys",body:d,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};return g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(f.indexName)+"/keys",body:g,callback:e})},_search:function(a,b){var c={params:a};if(null===this.as.jsonp){var d=this;return this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:function(c,e){var f=e&&e.status;c||f&&4===Math.floor(f/100)||1===Math.floor(f/100)?(d.as.jsonp=!1,b&&b(c,e)):(d.as.jsonp=!0,d._search(a,b))}})}return this.as.jsonp?this.as._jsonRequest({cache:this.cache,method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName),body:c,callback:b}):this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:b})},as:null,indexName:null,typeAheadArgs:null,typeAheadValueOption:null},function(a){var b=function(a){a=a||{};for(var b=1;b0&&this._gotoPage(this.page-1)},gotoPage:function(a){this._gotoPage(a)},setPage:function(a){this.page=a},setIndex:function(a){this.index=a},getIndex:function(){return this.index},clearExtraQueries:function(){this.extraQueries=[]},addExtraQuery:function(a,b,c){this.extraQueries.push({index:a,query:b,params:c||{}})},_gotoPage:function(a){this.page=a,this._search()},_search:function(){this.client.startQueriesBatch(),this.client.addQueryInBatch(this.index,this.q,this._getHitsSearchParams());var a=[],b={},c=0;for(c=0;c0&&b.push(f)}return b}}}(),function(a){window.AlgoliaPlaces=function(a,b){this.init(a,b)},AlgoliaPlaces.prototype={init:function(a,b){this.client=new AlgoliaSearch(a,b,"http",!0,["places-1.algolia.io","places-2.algolia.io","places-3.algolia.io"]),this.cache={}},search:function(a,b,c){var d="query="+encodeURIComponent(a);this.client._isUndefined(c)||null==c||(d=this.client._getSearchParams(c,d));var e={params:d,apiKey:this.client.apiKey,appID:this.client.applicationID};this.client._jsonRequest({cache:this.cache,method:"POST",url:"/1/places/query",body:e,callback:b,removeCustomHTTPHeaders:!0})}}}(),"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return 10>a?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g,h=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,g=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;f>c;c+=1)g[c]=str(c,i)||"null";return e=0===g.length?"[]":gap?"[\n"+gap+g.join(",\n"+gap)+"\n"+h+"]":"["+g.join(",")+"]",gap=h,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;f>c;c+=1)"string"==typeof rep[c]&&(d=rep[c],e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));return e=0===g.length?"{}":gap?"{\n"+gap+g.join(",\n"+gap)+"\n"+h+"}":"{"+g.join(",")+"}",gap=h,e}}"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()});var cx,escapable,gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;c>d;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),angular.module("algoliasearch",[]).service("algolia",["$injector",function(a){return{Client:function(b,c,d){return d=d||{},d.angular={$injector:a},d._ua="Algolia for AngularJS "+window.ALGOLIA_VERSION,new AlgoliaSearch(b,c,d)}}}]); \ No newline at end of file diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.js b/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.js new file mode 100644 index 000000000..044559aeb --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.js @@ -0,0 +1,2678 @@ +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +var ALGOLIA_VERSION = '2.9.7'; + +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +/* + * Algolia Search library initialization + * @param applicationID the application ID you have in your admin interface + * @param apiKey a valid API key for the service + * @param methodOrOptions the hash of parameters for initialization. It can contains: + * - method (optional) specify if the protocol used is http or https (http by default to make the first search query faster). + * You need to use https is you are doing something else than just search queries. + * - hosts (optional) the list of hosts that you have received for the service + * - dsn (optional) set to true if your account has the Distributed Search Option + * - dsnHost (optional) override the automatic computation of dsn hostname + */ +var AlgoliaSearch = function(applicationID, apiKey, methodOrOptions, resolveDNS, hosts) { + var self = this; + this.applicationID = applicationID; + this.apiKey = apiKey; + this.dsn = true; + this.dsnHost = null; + this.hosts = []; + this.currentHostIndex = 0; + this.requestTimeoutInMs = 2000; + this.extraHeaders = []; + this.jsonp = null; + this.options = {}; + + // make sure every client instance has it's own cache + this.cache = {}; + + var method; + var tld = 'net'; + if (typeof methodOrOptions === 'string') { // Old initialization + method = methodOrOptions; + } else { + // Take all option from the hash + var options = methodOrOptions || {}; + this.options = options; + if (!this._isUndefined(options.method)) { + method = options.method; + } + if (!this._isUndefined(options.tld)) { + tld = options.tld; + } + if (!this._isUndefined(options.dsn)) { + this.dsn = options.dsn; + } + if (!this._isUndefined(options.hosts)) { + hosts = options.hosts; + } + if (!this._isUndefined(options.dsnHost)) { + this.dsnHost = options.dsnHost; + } + if (!this._isUndefined(options.requestTimeoutInMs)) { + this.requestTimeoutInMs = +options.requestTimeoutInMs; + } + if (!this._isUndefined(options.jsonp)) { + this.jsonp = options.jsonp; + } + } + // If hosts is undefined, initialize it with applicationID + if (this._isUndefined(hosts)) { + hosts = [ + this.applicationID + '-1.algolianet.com', + this.applicationID + '-2.algolianet.com', + this.applicationID + '-3.algolianet.com' + ]; + } + // detect is we use http or https + this.host_protocol = 'http://'; + if (this._isUndefined(method) || method === null) { + this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://'; + } else if (method === 'https' || method === 'HTTPS') { + this.host_protocol = 'https://'; + } + // Add protocol to hosts + for (var i = 0; i < hosts.length; ++i) { + this.hosts.push(this.host_protocol + hosts[i]); + } + // then add Distributed Search Network host if there is one + if (this.dsn || this.dsnHost != null) { + if (this.dsnHost) { + this.hosts.unshift(this.host_protocol + this.dsnHost); + } else { + this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld); + } + } + // angular dependencies injection + if (this.options.angular) { + this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) { + self.options.angular.$q = $q; + self.options.angular.$http = $http; + }]); + } + + this._ua = this.options._ua || 'Algolia for vanilla JavaScript ' + window.ALGOLIA_VERSION; +}; + +// This holds the number of JSONP requests done accross clients +// It's used as part of the ?callback=JSONP_$JSONPCounter when we do JSONP requests +AlgoliaSearch.JSONPCounter = 0; + +function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) { + + function _getHitExplanationForOneAttr_recurse(obj, foundWords) { + var res = []; + if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) { + var match = false; + for (var j = 0; j < obj.matchedWords.length; ++j) { + var word = obj.matchedWords[j]; + if (!(word in foundWords)) { + foundWords[word] = 1; + match = true; + } + } + if (match) { + res.push(obj.value); + } + } else if (Object.prototype.toString.call(obj) === '[object Array]') { + for (var i = 0; i < obj.length; ++i) { + var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords); + res = res.concat(array); + } + } else if (typeof obj === 'object') { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)){ + res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords)); + } + } + } + return res; + } + + function _getHitExplanationForOneAttr(hit, foundWords, attr) { + var base = hit._highlightResult || hit; + if (attr.indexOf('.') === -1) { + if (attr in base) { + return _getHitExplanationForOneAttr_recurse(base[attr], foundWords); + } + return []; + } + var array = attr.split('.'); + var obj = base; + for (var i = 0; i < array.length; ++i) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + var res = []; + for (var j = 0; j < obj.length; ++j) { + res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.'))); + } + return res; + } + if (array[i] in obj) { + obj = obj[array[i]]; + } else { + return []; + } + } + return _getHitExplanationForOneAttr_recurse(obj, foundWords); + } + + var res = {}; + var foundWords = {}; + var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute); + res.title = (title.length > 0) ? title[0] : ''; + res.subtitles = []; + + if (typeof otherAttributes !== 'undefined') { + for (var i = 0; i < otherAttributes.length; ++i) { + var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]); + for (var j = 0; j < attr.length; ++j) { + res.subtitles.push({ attr: otherAttributes[i], value: attr[j] }); + } + } + } + return res; +} + + +AlgoliaSearch.prototype = { + /* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + deleteIndex: function(indexName, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + callback: callback }); + }, + /** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + moveIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'move', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + + }, + /** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + copyIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'copy', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + }, + /** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + getLogs: function(callback, offset, length) { + if (this._isUndefined(offset)) { + offset = 0; + } + if (this._isUndefined(length)) { + length = 10; + } + + return this._jsonRequest({ method: 'GET', + url: '/1/logs?offset=' + offset + '&length=' + length, + callback: callback }); + }, + /* + * List all existing indexes (paginated) + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with index list or error description if success is false. + * @param page The page to retrieve, starting at 0. + */ + listIndexes: function(callback, page) { + var params = typeof page !== 'undefined' ? '?page=' + page : ''; + return this._jsonRequest({ method: 'GET', + url: '/1/indexes' + params, + callback: callback }); + }, + + /* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ + initIndex: function(indexName) { + return new this.Index(this, indexName); + }, + /* + * List all existing user keys with their associated ACLs + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys', + callback: callback }); + }, + /* + * Get ACL of a user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + return this.addUserKeyWithValidity(acls, 0, 0, 0, callback); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this._jsonRequest({ method: 'POST', + url: '/1/keys', + body: aclsObject, + callback: callback }); + }, + + /** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ + setSecurityTags: function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + this.tagFilters = tags; + }, + + /** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ + setUserToken: function(userToken) { + this.userToken = userToken; + }, + + /* + * Initialize a new batch of search queries + */ + startQueriesBatch: function() { + this.batch = []; + }, + /* + * Add a search query in the batch + * + * @param query the full text query + * @param args (optional) if set, contains an object with query parameters: + * - attributes: an array of object attribute names to retrieve + * (if not set all attributes are retrieve) + * - attributesToHighlight: an array of object attribute names to highlight + * (if not set indexed attributes are highlighted) + * - minWordSizefor1Typo: the minimum number of characters to accept one typo. + * Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters to accept two typos. + * Defaults to 7. + * - getRankingInfo: if set, the result hits will contain ranking information in + * _rankingInfo attribute + * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0. + * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10. + */ + addQueryInBatch: function(indexName, query, args) { + var params = 'query=' + encodeURIComponent(query); + if (!this._isUndefined(args) && args !== null) { + params = this._getSearchParams(args, params); + } + this.batch.push({ indexName: indexName, params: params }); + }, + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Launch the batch of queries using XMLHttpRequest. + * (Optimized for browser using a POST query to minimize number of OPTIONS queries) + * + * @param callback the function that will receive results + * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime. + */ + sendQueriesBatch: function(callback, delay) { + var as = this; + var params = {requests: []}; + for (var i = 0; i < as.batch.length; ++i) { + params.requests.push(as.batch[i]); + } + window.clearTimeout(as.onDelayTrigger); + if (!this._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + as._sendQueriesBatch(params, callback); + }, delay); + as.onDelayTrigger = onDelayTrigger; + } else { + return this._sendQueriesBatch(params, callback); + } + }, + + /** + * Set the number of milliseconds a request can take before automatically being terminated. + * + * @param {Number} milliseconds + */ + setRequestTimeout: function(milliseconds) + { + if (milliseconds) { + this.requestTimeoutInMs = parseInt(milliseconds, 10); + } + }, + + /* + * Index class constructor. + * You should not use this method directly but use initIndex() function + */ + Index: function(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; + }, + /** + * Add an extra field to the HTTP request + * + * @param key the header field name + * @param value the header field value + */ + setExtraHeader: function(key, value) { + this.extraHeaders.push({ key: key, value: value}); + }, + + _sendQueriesBatch: function(params, callback) { + if (this.jsonp === null) { + var self = this; + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: function(success, content) { + if (!success) { + // retry first with JSONP + self.jsonp = true; + self._sendQueriesBatch(params, callback); + } else { + self.jsonp = false; + callback && callback(success, content); + } + } + }); + } else if (this.jsonp) { + var jsonpParams = ''; + for (var i = 0; i < params.requests.length; ++i) { + var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params; + jsonpParams += i + '=' + encodeURIComponent(q) + '&'; + } + var pObj = {params: jsonpParams}; + return this._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/*', + body: pObj, + callback: callback }); + } else { + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: callback}); + } + }, + /* + * Wrapper that try all hosts to maximize the quality of service + */ + _jsonRequest: function(opts) { + var self = this; + var callback = opts.callback; + var cache = null; + var cacheID = opts.url; + var deferred = null; + if (this.options.jQuery) { + deferred = this.options.jQuery.$.Deferred(); + deferred.promise = deferred.promise(); // promise is a property in angular + } else if (this.options.angular) { + deferred = this.options.angular.$q.defer(); + } + + if (!this._isUndefined(opts.body)) { + cacheID = opts.url + '_body_' + JSON.stringify(opts.body); + } + if (!this._isUndefined(opts.cache)) { + cache = opts.cache; + if (!this._isUndefined(cache[cacheID])) { + if (!this._isUndefined(callback) && callback) { + setTimeout(function () { callback(true, cache[cacheID]); }, 1); + } + deferred && deferred.resolve(cache[cacheID]); + return deferred && deferred.promise; + } + } + + opts.successiveRetryCount = 0; + var impl = function() { + + if (opts.successiveRetryCount >= self.hosts.length) { + var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' }; + if (!self._isUndefined(callback) && callback) { + opts.successiveRetryCount = 0; + callback(false, error); + } + deferred && deferred.reject(error); + return; + } + opts.callback = function(retry, success, body) { + if (success && !self._isUndefined(opts.cache)) { + cache[cacheID] = body; + } + if (!success && retry) { + self.currentHostIndex = ++self.currentHostIndex % self.hosts.length; + opts.successiveRetryCount += 1; + impl(); + } else { + opts.successiveRetryCount = 0; + deferred && (success ? deferred.resolve(body) : deferred.reject(body)); + if (!self._isUndefined(callback) && callback) { + callback(success, body); + } + } + }; + opts.hostname = self.hosts[self.currentHostIndex]; + self._jsonRequestByHost(opts); + }; + impl(); + + return deferred && deferred.promise; + }, + + _jsonRequestByHost: function(opts) { + var self = this; + var url = opts.hostname + opts.url; + + if (this.jsonp) { + this._makeJsonpRequestByHost(url, opts); + } else if (this.options.jQuery) { + this._makejQueryRequestByHost(url, opts); + } else if (this.options.angular) { + this._makeAngularRequestByHost(url, opts); + } else { + this._makeXmlHttpRequestByHost(url, opts); + } + }, + + /** + * Make a $http + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeAngularRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.angular.$http({ + url: url, + method: opts.method, + data: body, + cache: false, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)) + }).then(function(response) { + opts.callback(false, true, response.data); + }, function(response) { + if (response.status === 0) { + // xhr.timeout is not handled by Angular.js right now + // let's retry + opts.callback(true, false, response.data); + } else if (response.status == 400 || response.status === 403 || response.status === 404) { + opts.callback(false, false, response.data); + } else { + opts.callback(true, false, response.data); + } + }); + }, + + /** + * Make a $.ajax + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makejQueryRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.jQuery.$.ajax(url, { + type: opts.method, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)), + dataType: 'json', + data: body, + error: function(xhr, textStatus, error) { + if (textStatus === 'timeout') { + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) { + opts.callback(false, false, xhr.responseJSON ); + } else { + opts.callback(true, false, { 'message': error } ); + } + }, + success: function(data, textStatus, xhr) { + opts.callback(false, true, data); + } + }); + }, + + /** + * Make a JSONP request + * + * @param url request url (includes endpoint and path) + * @param opts all request options + */ + _makeJsonpRequestByHost: function(url, opts) { + if (opts.method !== 'GET') { + opts.callback(true, false, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' }); + return; + } + + var cbCalled = false; + var timedOut = false; + + AlgoliaSearch.JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cb = 'algoliaJSONP_' + AlgoliaSearch.JSONPCounter; + var done = false; + var ontimeout; + var success; + var clean; + + window[cb] = function(data) { + try { delete window[cb]; } catch (e) { window[cb] = undefined; } + + if (timedOut) { + return; + } + + var status = + data && data.message && data.status || + data && 200; + + var ok = status === 200; + var retry = !ok && status !== 400 && status !== 403 && status !== 404; + cbCalled = true; + opts.callback(retry, ok, data); + }; + + script.type = 'text/javascript'; + url += '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey; + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + if (opts.body && opts.body.params) { + url += '&' + opts.body.params; + } + + ontimeout = setTimeout(function() { + timedOut = true; + clean(); + + opts.callback(true, false, { 'message': 'Timeout - Failed to load JSONP script.' }); + }, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + + success = function() { + if (done || timedOut) { + return; + } + + done = true; + clean(); + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + } + }; + + clean = function() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + + try { + delete window[cb]; + delete window[cb + '_loaded']; + } catch (e) { + window[cb] = null; + window[cb + '_loaded'] = null; + } + }; + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = function() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + }; + + script.onload = function() { + success(); + }; + + script.onerror = function() { + if (done || timedOut) { + return; + } + + clean(); + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + }; + + script.async = true; + script.defer = true; + script.src = url; + + head.appendChild(script); + }, + + /** + * Make a XmlHttpRequest + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeXmlHttpRequestByHost: function(url, opts) { + // no cors or XDomainRequest, no request + if (!this._support.cors && !this._support.hasXDomainRequest) { + // very old browser, not supported + opts.callback(false, false, { 'message': 'CORS not supported' }); + return; + } + + var body = null; + var request = this._support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var ontimeout; + var self = this; + var timedOut; + var timeoutListener; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + timeoutListener = function() { + if (!self._support.timeout) { + timedOut = true; + request.abort(); + } + + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + }; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (request instanceof XMLHttpRequest) { + request.open(opts.method, url, true); + } else { + request.open(opts.method, url); + } + + if (this._support.cors && body !== null && opts.method !== 'GET') { + request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + request.onload = function(/*event*/) { + // When browser does not supports request.timeout, we can + // have both a load and timeout event + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + var response = null; + + try { + response = JSON.parse(request.responseText); + } catch(e) {} + + var status = + // XHR provides a `status` property + request.status || + + // XDR does not have a `status` property, + // we rely on our own API response `status`, only + // provided when an error occurs, so we expect a .message + response && response.message && response.status || + + // XDR default to success when no response.status + response && 200; + + var success = status === 200 || status === 201; + var retry = !success && status !== 400 && status !== 403 && status !== 404; + + opts.callback(retry, success, response); + }; + + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + request.onprogress = function noop() {}; + + if (this._support.timeout) { + // .timeout supported by both XHR and XDR, + // we do receive timeout event, tested + request.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1); + + request.ontimeout = timeoutListener; + } else { + ontimeout = setTimeout(timeoutListener, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + } + + request.onerror = function(event) { + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + opts.callback(true, false, { 'message': 'Could not connect to host', 'error': event } ); + }; + + request.send(body); + }, + + /* + * Transform search param object in query string + */ + _getSearchParams: function(args, params) { + if (this._isUndefined(args) || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args.hasOwnProperty(key)) { + params += (params.length === 0) ? '?' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]); + } + } + return params; + }, + _isUndefined: function(obj) { + return obj === void 0; + }, + + _support: { + hasXMLHttpRequest: 'XMLHttpRequest' in window, + hasXDomainRequest: 'XDomainRequest' in window, + cors: 'withCredentials' in new XMLHttpRequest(), + timeout: 'timeout' in new XMLHttpRequest() + } +}; + +/* + * Contains all the functions related to one index + * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object + */ +AlgoliaSearch.prototype.Index.prototype = { + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Add an object in this index + * + * @param content contains the javascript object to add inside the index + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + * @param objectID (optional) an objectID you want to attribute to this object + * (if the attribute already exist the old object will be overwrite) + */ + addObject: function(content, callback, objectID) { + var indexObj = this; + if (this.as._isUndefined(objectID)) { + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName), + body: content, + callback: callback }); + } else { + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + body: content, + callback: callback }); + } + + }, + /* + * Add several objects + * + * @param objects contains an array of objects to add + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + addObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'addObject', + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Get an object from this index + * + * @param objectID the unique identifier of the object to retrieve + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the object to retrieve or the error message if a failure occured + * @param attributes (optional) if set, contains the array of attribute names to retrieve + */ + getObject: function(objectID, callback, attributes) { + if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) { + attributes = callback; + callback = null; + } + var indexObj = this; + var params = ''; + if (!this.as._isUndefined(attributes)) { + params = '?attributes='; + for (var i = 0; i < attributes.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attributes[i]; + } + } + + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + callback: callback }); + }, + + /* + * Update partially an object (only update attributes passed in argument) + * + * @param partialObject contains the javascript attributes to override, the + * object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + partialUpdateObject: function(partialObject, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial', + body: partialObject, + callback: callback }); + }, + /* + * Partially Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + partialUpdateObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'partialUpdateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Override the content of object + * + * @param object contains the javascript object to save, the object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObject: function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + callback: callback }); + }, + /* + * Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Delete an object from the index + * + * @param objectID the unique identifier of object to delete + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + deleteObject: function(objectID, callback) { + if (objectID === null || objectID.length === 0) { + callback(false, { message: 'empty objectID'}); + return; + } + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + callback: callback }); + }, + /* + * Search inside the index using XMLHttpRequest request (Using a POST query to + * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). + * + * @param query the full text query + * @param callback the result callback with two arguments: + * success: boolean set to true if the request was successfull. If false, the content contains the error. + * content: the server answer that contains the list of results. + * @param args (optional) if set, contains an object with query parameters: + * - page: (integer) Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. + * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). + * Attributes are separated with a comma (for example "name,address"). + * You can also use an array (for example ["name","address"]). + * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. + * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. + * Attributes are separated by a comma. You can also use an array (for example ["name","address"]). + * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. + * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. + * A matchLevel is returned for each highlighted attribute and can contain: + * - full: if all the query terms were found in the attribute, + * - partial: if only some of the query terms were found, + * - none: if none of the query terms were found. + * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). + * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). + * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). By default no snippet is computed. + * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. + * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. + * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). + * For example aroundLatLng=47.316669,5.016670). + * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision + * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). + * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. + * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. + * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. + * You can also use an array (for example numericFilters: ["price>100","price<1000"]). + * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. + * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). + * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). + * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). + * - facetFilters: filter the query by a list of facets. + * Facets are separated by commas and each facet is encoded as `attributeName:value`. + * For example: `facetFilters=category:Book,author:John%20Doe`. + * You can also use an array (for example `["category:Book","author:John%20Doe"]`). + * - facets: List of object attributes that you want to use for faceting. + * Comma separated list: `"category,author"` or array `['category','author']` + * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. + * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. + * - queryType: select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. + * Comma separated and array are accepted. + * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. + * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, + * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best + * one is kept and others are removed. + * - restrictSearchableAttributes: List of attributes you want to use for textual search (must be a subset of the attributesToIndex index setting) + * either comma separated or as an array + * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime. + */ + search: function(query, callback, args, delay) { + if (query === undefined || query === null) { + query = ''; + } + + // no query = getAllObjects + if (typeof query === 'function') { + callback = query; + query = ''; + } + + if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) { + args = callback; + callback = null; + } + + var indexObj = this; + var params = 'query=' + encodeURIComponent(query); + if (!this.as._isUndefined(args) && args !== null) { + params = this.as._getSearchParams(args, params); + } + window.clearTimeout(indexObj.onDelayTrigger); + if (!this.as._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + indexObj._search(params, callback); + }, delay); + indexObj.onDelayTrigger = onDelayTrigger; + } else { + return this._search(params, callback); + } + }, + + /* + * Browse all index content + * + * @param page Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. + */ + browse: function(page, callback, hitsPerPage) { + if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) { + hitsPerPage = callback; + callback = null; + } + var indexObj = this; + var params = '?page=' + page; + if (!this.as._isUndefined(hitsPerPage)) { + params += '&hitsPerPage=' + hitsPerPage; + } + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params, + callback: callback }); + }, + + /* + * Get a Typeahead.js adapter + * @param searchParams contains an object with query parameters (see search for details) + */ + ttAdapter: function(params) { + var self = this; + return function(query, cb) { + self.search(query, function(success, content) { + if (success) { + cb(content.hits); + } else { + cb(content && content.message); + } + }, params); + }; + }, + + /* + * Wait the publication of a task on the server. + * All server task are asynchronous and you can check with this method that the task is published. + * + * @param taskID the id of the task returned by server + * @param callback the result callback with with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains the list of results + */ + waitTask: function(taskID, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID, + callback: function(success, body) { + if (success) { + if (body.status === 'published') { + callback(true, body); + } else { + setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100); + } + } else { + callback(false, body); + } + }}); + }, + + /* + * This function deletes the index content. Settings and index specific API keys are kept untouched. + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + clearIndex: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + callback: callback }); + }, + /* + * Get settings of this index + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + getSettings: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + callback: callback }); + }, + + /* + * Set settings for this index + * + * @param settigns the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). + * By default no snippet is computed. If set to null, no snippet is computed. + * - attributesToIndex: (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. + * This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to + * retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of + * the list will be considered more important than matches in attributes further down the list. + * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable + * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled + * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreassing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by attributesToIndex, + * - exact: + * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. + * For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV + * show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. + * For example `"customRanking" => ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer or the error message if a failure occured + */ + setSettings: function(settings, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + body: settings, + callback: callback }); + }, + /* + * List all existing user keys associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + callback: callback }); + }, + /* + * Get ACL of a user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /// + /// Internal methods only after this line + /// + _search: function(params, callback) { + var pObj = {params: params}; + if (this.as.jsonp === null) { + var self = this; + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: function(success, content) { + var status = content && content.status; + if (success || status && Math.floor(status / 100) === 4 || Math.floor(status / 100) === 1) { + self.as.jsonp = false; + callback && callback(success, content); + } else { + self.as.jsonp = true; + self._search(params, callback); + } + } + }); + } else if (this.as.jsonp) { + return this.as._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: pObj, + callback: callback }); + } else { + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: callback}); + } + }, + + // internal attributes + as: null, + indexName: null, + typeAheadArgs: null, + typeAheadValueOption: null +}; + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + var extend = function(out) { + out = out || {}; + for (var i = 1; i < arguments.length; i++) { + if (!arguments[i]) { + continue; + } + for (var key in arguments[i]) { + if (arguments[i].hasOwnProperty(key)) { + out[key] = arguments[i][key]; + } + } + } + return out; + }; + + /** + * Algolia Search Helper providing faceting and disjunctive faceting + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets, the list of disjunctive facets and the default facet filters + */ + window.AlgoliaSearchHelper = function(client, index, options) { + /// Default options + var defaults = { + facets: [], // list of facets to compute + disjunctiveFacets: [], // list of disjunctive facets to compute + hitsPerPage: 20, // number of hits per page + defaultFacetFilters: [] // the default list of facetFilters + }; + + this.init(client, index, extend({}, defaults, options)); + }; + + AlgoliaSearchHelper.prototype = { + /** + * Initialize a new AlgoliaSearchHelper + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets + * @return {AlgoliaSearchHelper} + */ + init: function(client, index, options) { + this.client = client; + this.index = index; + this.options = options; + this.page = 0; + this.refinements = {}; + this.excludes = {}; + this.disjunctiveRefinements = {}; + this.extraQueries = []; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + */ + search: function(q, searchCallback, searchParams) { + this.q = q; + this.searchCallback = searchCallback; + this.searchParams = searchParams || {}; + this.page = this.page || 0; + this.refinements = this.refinements || {}; + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this._search(); + }, + + /** + * Remove all refinements (disjunctive + conjunctive) + */ + clearRefinements: function() { + this.disjunctiveRefinements = {}; + this.refinements = {}; + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + this.disjunctiveRefinements[facet][value] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + try { + delete this.disjunctiveRefinements[facet][value]; + } catch (e) { + this.disjunctiveRefinements[facet][value] = undefined; // IE compat + } + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = false; + }, + + /** + * Ensure a facet exclude exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = true; + }, + + /** + * Ensure a facet exclude does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = false; + }, + + /** + * Toggle refinement state of an exclude + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleExclude: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':-' + value; + this.excludes[refinement] = !this.excludes[refinement]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Toggle refinement state of a facet + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleRefine: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':' + value; + this.refinements[refinement] = !this.refinements[refinement]; + this.page = 0; + this._search(); + return true; + } + } + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) { + if (this.options.disjunctiveFacets[j] == facet) { + this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Check the refinement state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isRefined: function(facet, value) { + var refinement = facet + ':' + value; + if (this.refinements[refinement]) { + return true; + } + if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) { + return true; + } + return false; + }, + + /** + * Check the exclude state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isExcluded: function(facet, value) { + var refinement = facet + ':-' + value; + if (this.excludes[refinement]) { + return true; + } + return false; + }, + + /** + * Go to next page + */ + nextPage: function() { + this._gotoPage(this.page + 1); + }, + + /** + * Go to previous page + */ + previousPage: function() { + if (this.page > 0) { + this._gotoPage(this.page - 1); + } + }, + + /** + * Goto a page + * @param {integer} page The page number + */ + gotoPage: function(page) { + this._gotoPage(page); + }, + + /** + * Configure the page but do not trigger a reload + * @param {integer} page The page number + */ + setPage: function(page) { + this.page = page; + }, + + /** + * Configure the underlying index name + * @param {string} name the index name + */ + setIndex: function(name) { + this.index = name; + }, + + /** + * Get the underlying configured index name + */ + getIndex: function() { + return this.index; + }, + + /** + * Clear the extra queries added to the underlying batch of queries + */ + clearExtraQueries: function() { + this.extraQueries = []; + }, + + /** + * Add an extra query to the underlying batch of queries. Once you add queries + * to the batch, the 2nd parameter of the searchCallback will be an object with a `results` + * attribute listing all search results. + */ + addExtraQuery: function(index, query, params) { + this.extraQueries.push({ index: index, query: query, params: (params || {}) }); + }, + + ///////////// PRIVATE + + /** + * Goto a page + * @param {integer} page The page number + */ + _gotoPage: function(page) { + this.page = page; + this._search(); + }, + + /** + * Perform the underlying queries + */ + _search: function() { + this.client.startQueriesBatch(); + this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams()); + var disjunctiveFacets = []; + var unusedDisjunctiveFacets = {}; + var i = 0; + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (this._hasDisjunctiveRefinements(facet)) { + disjunctiveFacets.push(facet); + } else { + unusedDisjunctiveFacets[facet] = true; + } + } + for (i = 0; i < disjunctiveFacets.length; ++i) { + this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(disjunctiveFacets[i])); + } + for (i = 0; i < this.extraQueries.length; ++i) { + this.client.addQueryInBatch(this.extraQueries[i].index, this.extraQueries[i].query, this.extraQueries[i].params); + } + var self = this; + this.client.sendQueriesBatch(function(success, content) { + if (!success) { + self.searchCallback(false, content); + return; + } + var aggregatedAnswer = content.results[0]; + aggregatedAnswer.disjunctiveFacets = aggregatedAnswer.disjunctiveFacets || {}; + aggregatedAnswer.facets_stats = aggregatedAnswer.facets_stats || {}; + // create disjunctive facets from facets (disjunctive facets without refinements) + for (var facet in unusedDisjunctiveFacets) { + if (aggregatedAnswer.facets[facet] && !aggregatedAnswer.disjunctiveFacets[facet]) { + aggregatedAnswer.disjunctiveFacets[facet] = aggregatedAnswer.facets[facet]; + try { + delete aggregatedAnswer.facets[facet]; + } catch (e) { + aggregatedAnswer.facets[facet] = undefined; // IE compat + } + } + } + // aggregate the disjunctive facets + for (i = 0; i < disjunctiveFacets.length; ++i) { + for (var dfacet in content.results[i + 1].facets) { + aggregatedAnswer.disjunctiveFacets[dfacet] = content.results[i + 1].facets[dfacet]; + if (self.disjunctiveRefinements[dfacet]) { + for (var value in self.disjunctiveRefinements[dfacet]) { + // add the disjunctive reginements if it is no more retrieved + if (!aggregatedAnswer.disjunctiveFacets[dfacet][value] && self.disjunctiveRefinements[dfacet][value]) { + aggregatedAnswer.disjunctiveFacets[dfacet][value] = 0; + } + } + } + } + // aggregate the disjunctive facets stats + for (var stats in content.results[i + 1].facets_stats) { + aggregatedAnswer.facets_stats[stats] = content.results[i + 1].facets_stats[stats]; + } + } + + // Backward compatibility + aggregatedAnswer.facetStats = aggregatedAnswer.facets_stats; + + // add the excludes + for (var exclude in self.excludes) { + if (self.excludes[exclude]) { + var e = exclude.indexOf(':-'); + var facet = exclude.slice(0, e); + var value = exclude.slice(e + 2); + aggregatedAnswer.facets[facet] = aggregatedAnswer.facets[facet] || {}; + if (!aggregatedAnswer.facets[facet][value]) { + aggregatedAnswer.facets[facet][value] = 0; + } + } + } + // call the actual callback + if (self.extraQueries.length === 0) { + self.searchCallback(true, aggregatedAnswer); + } else { + // append the extra queries + var c = { results: [ aggregatedAnswer ] }; + for (i = 0; i < self.extraQueries.length; ++i) { + c.results.push(content.results[1 + disjunctiveFacets.length + i]); + } + self.searchCallback(true, c); + } + }); + }, + + /** + * Build search parameters used to fetch hits + * @return {hash} + */ + _getHitsSearchParams: function() { + var facets = []; + var i = 0; + for (i = 0; i < this.options.facets.length; ++i) { + facets.push(this.options.facets[i]); + } + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (!this._hasDisjunctiveRefinements(facet)) { + facets.push(facet); + } + } + return extend({}, { + hitsPerPage: this.options.hitsPerPage, + page: this.page, + facets: facets, + facetFilters: this._getFacetFilters() + }, this.searchParams); + }, + + /** + * Build search parameters used to fetch a disjunctive facet + * @param {string} facet the associated facet name + * @return {hash} + */ + _getDisjunctiveFacetSearchParams: function(facet) { + return extend({}, this.searchParams, { + hitsPerPage: 1, + page: 0, + attributesToRetrieve: [], + attributesToHighlight: [], + attributesToSnippet: [], + facets: facet, + facetFilters: this._getFacetFilters(facet), + analytics: false + }); + }, + + /** + * Test if there are some disjunctive refinements on the facet + */ + _hasDisjunctiveRefinements: function(facet) { + for (var value in this.disjunctiveRefinements[facet]) { + if (this.disjunctiveRefinements[facet][value]) { + return true; + } + } + return false; + }, + + /** + * Build facetFilters parameter based on current refinements + * @param {string} facet if set, the current disjunctive facet + * @return {hash} + */ + _getFacetFilters: function(facet) { + var facetFilters = []; + if (this.options.defaultFacetFilters) { + for (var i = 0; i < this.options.defaultFacetFilters.length; ++i) { + facetFilters.push(this.options.defaultFacetFilters[i]); + } + } + for (var refinement in this.refinements) { + if (this.refinements[refinement]) { + facetFilters.push(refinement); + } + } + for (var refinement in this.excludes) { + if (this.excludes[refinement]) { + facetFilters.push(refinement); + } + } + for (var disjunctiveRefinement in this.disjunctiveRefinements) { + if (disjunctiveRefinement != facet) { + var refinements = []; + for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) { + if (this.disjunctiveRefinements[disjunctiveRefinement][value]) { + refinements.push(disjunctiveRefinement + ':' + value); + } + } + if (refinements.length > 0) { + facetFilters.push(refinements); + } + } + } + return facetFilters; + } + }; +})(); + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + + /** + * Algolia Places API + * @param {string} Your application ID + * @param {string} Your API Key + */ + window.AlgoliaPlaces = function(applicationID, apiKey) { + this.init(applicationID, apiKey); + }; + + AlgoliaPlaces.prototype = { + /** + * @param {string} Your application ID + * @param {string} Your API Key + */ + init: function(applicationID, apiKey) { + this.client = new AlgoliaSearch(applicationID, apiKey, 'http', true, ['places-1.algolia.io', 'places-2.algolia.io', 'places-3.algolia.io']); + this.cache = {}; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + * @param {hash} the list of search parameters + */ + search: function(q, searchCallback, searchParams) { + var indexObj = this; + var params = 'query=' + encodeURIComponent(q); + if (!this.client._isUndefined(searchParams) && searchParams != null) { + params = this.client._getSearchParams(searchParams, params); + } + var pObj = {params: params, apiKey: this.client.apiKey, appID: this.client.applicationID}; + this.client._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/places/query', + body: pObj, + callback: searchCallback, + removeCustomHTTPHeaders: true }); + } + }; +})(); + +/* + json2.js + 2014-02-04 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx, + escapable, + gap, + indent, + meta, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); + +/* global jQuery */ +(function ($) { + + $.algolia = {}; + $.algolia.Client = function(applicationID, apiKey, options) { + options = options || {}; + options.jQuery = { + '$': $ + }; + options._ua = 'Algolia for jQuery ' + window.ALGOLIA_VERSION; + return new AlgoliaSearch(applicationID, apiKey, options); + }; + +}(jQuery)); diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.min.js b/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.min.js new file mode 100644 index 000000000..a5d823071 --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.jquery.min.js @@ -0,0 +1,7 @@ +/*! + * algoliasearch 2.9.7 + * https://github.com/algolia/algoliasearch-client-js + * Copyright 2014 Algolia SAS; Licensed MIT + */ + +function AlgoliaExplainResults(a,b,c){function d(a,b){var c=[];if("object"==typeof a&&"matchedWords"in a&&"value"in a){for(var e=!1,f=0;f0?h[0]:"",f.subtitles=[],"undefined"!=typeof c)for(var i=0;i0))return this._sendQueriesBatch(d,a);var f=window.setTimeout(function(){c._sendQueriesBatch(d,a)},b);c.onDelayTrigger=f},setRequestTimeout:function(a){a&&(this.requestTimeoutInMs=parseInt(a,10))},Index:function(a,b){this.indexName=b,this.as=a,this.typeAheadArgs=null,this.typeAheadValueOption=null,this.cache={}},setExtraHeader:function(a,b){this.extraHeaders.push({key:a,value:b})},_sendQueriesBatch:function(a,b){if(null===this.jsonp){var c=this;return this._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/*/queries",body:a,callback:function(d,e){d?(c.jsonp=!1,b&&b(d,e)):(c.jsonp=!0,c._sendQueriesBatch(a,b))}})}if(this.jsonp){for(var d="",e=0;e=b.hosts.length){var h={message:"Cannot connect the Algolia's Search API. Please send an email to support@algolia.com to report the issue."};return!b._isUndefined(c)&&c&&(a.successiveRetryCount=0,c(!1,h)),void(f&&f.reject(h))}a.callback=function(h,i,j){i&&!b._isUndefined(a.cache)&&(d[e]=j),!i&&h?(b.currentHostIndex=++b.currentHostIndex%b.hosts.length,a.successiveRetryCount+=1,g()):(a.successiveRetryCount=0,f&&(i?f.resolve(j):f.reject(j)),!b._isUndefined(c)&&c&&c(i,j))},a.hostname=b.hosts[b.currentHostIndex],b._jsonRequestByHost(a)};return g(),f&&f.promise},_jsonRequestByHost:function(a){var b=a.hostname+a.url;this.jsonp?this._makeJsonpRequestByHost(b,a):this.options.jQuery?this._makejQueryRequestByHost(b,a):this.options.angular?this._makeAngularRequestByHost(b,a):this._makeXmlHttpRequestByHost(b,a)},_makeAngularRequestByHost:function(a,b){var c=null;this._isUndefined(b.body)||(c=JSON.stringify(b.body)),a+=(-1===a.indexOf("?")?"?":"&")+"X-Algolia-API-Key="+this.apiKey,a+="&X-Algolia-Application-Id="+this.applicationID,this.userToken&&(a+="&X-Algolia-UserToken="+encodeURIComponent(this.userToken)),this.tagFilters&&(a+="&X-Algolia-TagFilters="+encodeURIComponent(this.tagFilters)),a+="&X-Algolia-Agent="+encodeURIComponent(this._ua);for(var d=0;d0))return this._search(f,b);var g=window.setTimeout(function(){e._search(f,b)},d);e.onDelayTrigger=g},browse:function(a,b,c){+b>0&&(this.as._isUndefined(c)||!c)&&(c=b,b=null);var d=this,e="?page="+a;return this.as._isUndefined(c)||(e+="&hitsPerPage="+c),this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/browse"+e,callback:b})},ttAdapter:function(a){var b=this;return function(c,d){b.search(c,function(a,b){d(a?b.hits:b&&b.message)},a)}},waitTask:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/task/"+a,callback:function(d,e){d?"published"===e.status?b(!0,e):setTimeout(function(){c.waitTask(a,b)},100):b(!1,e)}})},clearIndex:function(a){var b=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/clear",callback:a})},getSettings:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/settings",callback:a})},setSettings:function(a,b){var c=this;return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/settings",body:a,callback:b})},listUserKeys:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/keys",callback:a})},getUserKeyACL:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},deleteUserKey:function(a,b){var c=this;return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},addUserKey:function(a,b){var c=this,d={};return d.acl=a,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys",body:d,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};return g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(f.indexName)+"/keys",body:g,callback:e})},_search:function(a,b){var c={params:a};if(null===this.as.jsonp){var d=this;return this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:function(c,e){var f=e&&e.status;c||f&&4===Math.floor(f/100)||1===Math.floor(f/100)?(d.as.jsonp=!1,b&&b(c,e)):(d.as.jsonp=!0,d._search(a,b))}})}return this.as.jsonp?this.as._jsonRequest({cache:this.cache,method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName),body:c,callback:b}):this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:b})},as:null,indexName:null,typeAheadArgs:null,typeAheadValueOption:null},function(a){var b=function(a){a=a||{};for(var b=1;b0&&this._gotoPage(this.page-1)},gotoPage:function(a){this._gotoPage(a)},setPage:function(a){this.page=a},setIndex:function(a){this.index=a},getIndex:function(){return this.index},clearExtraQueries:function(){this.extraQueries=[]},addExtraQuery:function(a,b,c){this.extraQueries.push({index:a,query:b,params:c||{}})},_gotoPage:function(a){this.page=a,this._search()},_search:function(){this.client.startQueriesBatch(),this.client.addQueryInBatch(this.index,this.q,this._getHitsSearchParams());var a=[],b={},c=0;for(c=0;c0&&b.push(f)}return b}}}(),function(a){window.AlgoliaPlaces=function(a,b){this.init(a,b)},AlgoliaPlaces.prototype={init:function(a,b){this.client=new AlgoliaSearch(a,b,"http",!0,["places-1.algolia.io","places-2.algolia.io","places-3.algolia.io"]),this.cache={}},search:function(a,b,c){var d="query="+encodeURIComponent(a);this.client._isUndefined(c)||null==c||(d=this.client._getSearchParams(c,d));var e={params:d,apiKey:this.client.apiKey,appID:this.client.applicationID};this.client._jsonRequest({cache:this.cache,method:"POST",url:"/1/places/query",body:e,callback:b,removeCustomHTTPHeaders:!0})}}}(),"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return 10>a?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g,h=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,g=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;f>c;c+=1)g[c]=str(c,i)||"null";return e=0===g.length?"[]":gap?"[\n"+gap+g.join(",\n"+gap)+"\n"+h+"]":"["+g.join(",")+"]",gap=h,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;f>c;c+=1)"string"==typeof rep[c]&&(d=rep[c],e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));return e=0===g.length?"{}":gap?"{\n"+gap+g.join(",\n"+gap)+"\n"+h+"}":"{"+g.join(",")+"}",gap=h,e}}"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()});var cx,escapable,gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;c>d;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(a){a.algolia={},a.algolia.Client=function(b,c,d){return d=d||{},d.jQuery={$:a},d._ua="Algolia for jQuery "+window.ALGOLIA_VERSION,new AlgoliaSearch(b,c,d)}}(jQuery); \ No newline at end of file diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.js b/dist/bower_components/algoliasearch/dist/algoliasearch.js new file mode 100644 index 000000000..6294b352d --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.js @@ -0,0 +1,2663 @@ +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +var ALGOLIA_VERSION = '2.9.7'; + +/* + * Copyright (c) 2013 Algolia + * http://www.algolia.com/ + * + * 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. + */ + +/* + * Algolia Search library initialization + * @param applicationID the application ID you have in your admin interface + * @param apiKey a valid API key for the service + * @param methodOrOptions the hash of parameters for initialization. It can contains: + * - method (optional) specify if the protocol used is http or https (http by default to make the first search query faster). + * You need to use https is you are doing something else than just search queries. + * - hosts (optional) the list of hosts that you have received for the service + * - dsn (optional) set to true if your account has the Distributed Search Option + * - dsnHost (optional) override the automatic computation of dsn hostname + */ +var AlgoliaSearch = function(applicationID, apiKey, methodOrOptions, resolveDNS, hosts) { + var self = this; + this.applicationID = applicationID; + this.apiKey = apiKey; + this.dsn = true; + this.dsnHost = null; + this.hosts = []; + this.currentHostIndex = 0; + this.requestTimeoutInMs = 2000; + this.extraHeaders = []; + this.jsonp = null; + this.options = {}; + + // make sure every client instance has it's own cache + this.cache = {}; + + var method; + var tld = 'net'; + if (typeof methodOrOptions === 'string') { // Old initialization + method = methodOrOptions; + } else { + // Take all option from the hash + var options = methodOrOptions || {}; + this.options = options; + if (!this._isUndefined(options.method)) { + method = options.method; + } + if (!this._isUndefined(options.tld)) { + tld = options.tld; + } + if (!this._isUndefined(options.dsn)) { + this.dsn = options.dsn; + } + if (!this._isUndefined(options.hosts)) { + hosts = options.hosts; + } + if (!this._isUndefined(options.dsnHost)) { + this.dsnHost = options.dsnHost; + } + if (!this._isUndefined(options.requestTimeoutInMs)) { + this.requestTimeoutInMs = +options.requestTimeoutInMs; + } + if (!this._isUndefined(options.jsonp)) { + this.jsonp = options.jsonp; + } + } + // If hosts is undefined, initialize it with applicationID + if (this._isUndefined(hosts)) { + hosts = [ + this.applicationID + '-1.algolianet.com', + this.applicationID + '-2.algolianet.com', + this.applicationID + '-3.algolianet.com' + ]; + } + // detect is we use http or https + this.host_protocol = 'http://'; + if (this._isUndefined(method) || method === null) { + this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://'; + } else if (method === 'https' || method === 'HTTPS') { + this.host_protocol = 'https://'; + } + // Add protocol to hosts + for (var i = 0; i < hosts.length; ++i) { + this.hosts.push(this.host_protocol + hosts[i]); + } + // then add Distributed Search Network host if there is one + if (this.dsn || this.dsnHost != null) { + if (this.dsnHost) { + this.hosts.unshift(this.host_protocol + this.dsnHost); + } else { + this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld); + } + } + // angular dependencies injection + if (this.options.angular) { + this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) { + self.options.angular.$q = $q; + self.options.angular.$http = $http; + }]); + } + + this._ua = this.options._ua || 'Algolia for vanilla JavaScript ' + window.ALGOLIA_VERSION; +}; + +// This holds the number of JSONP requests done accross clients +// It's used as part of the ?callback=JSONP_$JSONPCounter when we do JSONP requests +AlgoliaSearch.JSONPCounter = 0; + +function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) { + + function _getHitExplanationForOneAttr_recurse(obj, foundWords) { + var res = []; + if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) { + var match = false; + for (var j = 0; j < obj.matchedWords.length; ++j) { + var word = obj.matchedWords[j]; + if (!(word in foundWords)) { + foundWords[word] = 1; + match = true; + } + } + if (match) { + res.push(obj.value); + } + } else if (Object.prototype.toString.call(obj) === '[object Array]') { + for (var i = 0; i < obj.length; ++i) { + var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords); + res = res.concat(array); + } + } else if (typeof obj === 'object') { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)){ + res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords)); + } + } + } + return res; + } + + function _getHitExplanationForOneAttr(hit, foundWords, attr) { + var base = hit._highlightResult || hit; + if (attr.indexOf('.') === -1) { + if (attr in base) { + return _getHitExplanationForOneAttr_recurse(base[attr], foundWords); + } + return []; + } + var array = attr.split('.'); + var obj = base; + for (var i = 0; i < array.length; ++i) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + var res = []; + for (var j = 0; j < obj.length; ++j) { + res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.'))); + } + return res; + } + if (array[i] in obj) { + obj = obj[array[i]]; + } else { + return []; + } + } + return _getHitExplanationForOneAttr_recurse(obj, foundWords); + } + + var res = {}; + var foundWords = {}; + var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute); + res.title = (title.length > 0) ? title[0] : ''; + res.subtitles = []; + + if (typeof otherAttributes !== 'undefined') { + for (var i = 0; i < otherAttributes.length; ++i) { + var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]); + for (var j = 0; j < attr.length; ++j) { + res.subtitles.push({ attr: otherAttributes[i], value: attr[j] }); + } + } + } + return res; +} + + +AlgoliaSearch.prototype = { + /* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + deleteIndex: function(indexName, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + callback: callback }); + }, + /** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + moveIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'move', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + + }, + /** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + copyIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = {operation: 'copy', destination: dstIndexName}; + return this._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + callback: callback }); + }, + /** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer that contains the task ID + */ + getLogs: function(callback, offset, length) { + if (this._isUndefined(offset)) { + offset = 0; + } + if (this._isUndefined(length)) { + length = 10; + } + + return this._jsonRequest({ method: 'GET', + url: '/1/logs?offset=' + offset + '&length=' + length, + callback: callback }); + }, + /* + * List all existing indexes (paginated) + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with index list or error description if success is false. + * @param page The page to retrieve, starting at 0. + */ + listIndexes: function(callback, page) { + var params = typeof page !== 'undefined' ? '?page=' + page : ''; + return this._jsonRequest({ method: 'GET', + url: '/1/indexes' + params, + callback: callback }); + }, + + /* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ + initIndex: function(indexName) { + return new this.Index(this, indexName); + }, + /* + * List all existing user keys with their associated ACLs + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys', + callback: callback }); + }, + /* + * Get ACL of a user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + return this._jsonRequest({ method: 'GET', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + return this._jsonRequest({ method: 'DELETE', + url: '/1/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + return this.addUserKeyWithValidity(acls, 0, 0, 0, callback); + }, + /* + * Add an existing user key + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this._jsonRequest({ method: 'POST', + url: '/1/keys', + body: aclsObject, + callback: callback }); + }, + + /** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ + setSecurityTags: function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + this.tagFilters = tags; + }, + + /** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ + setUserToken: function(userToken) { + this.userToken = userToken; + }, + + /* + * Initialize a new batch of search queries + */ + startQueriesBatch: function() { + this.batch = []; + }, + /* + * Add a search query in the batch + * + * @param query the full text query + * @param args (optional) if set, contains an object with query parameters: + * - attributes: an array of object attribute names to retrieve + * (if not set all attributes are retrieve) + * - attributesToHighlight: an array of object attribute names to highlight + * (if not set indexed attributes are highlighted) + * - minWordSizefor1Typo: the minimum number of characters to accept one typo. + * Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters to accept two typos. + * Defaults to 7. + * - getRankingInfo: if set, the result hits will contain ranking information in + * _rankingInfo attribute + * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0. + * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10. + */ + addQueryInBatch: function(indexName, query, args) { + var params = 'query=' + encodeURIComponent(query); + if (!this._isUndefined(args) && args !== null) { + params = this._getSearchParams(args, params); + } + this.batch.push({ indexName: indexName, params: params }); + }, + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Launch the batch of queries using XMLHttpRequest. + * (Optimized for browser using a POST query to minimize number of OPTIONS queries) + * + * @param callback the function that will receive results + * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime. + */ + sendQueriesBatch: function(callback, delay) { + var as = this; + var params = {requests: []}; + for (var i = 0; i < as.batch.length; ++i) { + params.requests.push(as.batch[i]); + } + window.clearTimeout(as.onDelayTrigger); + if (!this._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + as._sendQueriesBatch(params, callback); + }, delay); + as.onDelayTrigger = onDelayTrigger; + } else { + return this._sendQueriesBatch(params, callback); + } + }, + + /** + * Set the number of milliseconds a request can take before automatically being terminated. + * + * @param {Number} milliseconds + */ + setRequestTimeout: function(milliseconds) + { + if (milliseconds) { + this.requestTimeoutInMs = parseInt(milliseconds, 10); + } + }, + + /* + * Index class constructor. + * You should not use this method directly but use initIndex() function + */ + Index: function(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; + }, + /** + * Add an extra field to the HTTP request + * + * @param key the header field name + * @param value the header field value + */ + setExtraHeader: function(key, value) { + this.extraHeaders.push({ key: key, value: value}); + }, + + _sendQueriesBatch: function(params, callback) { + if (this.jsonp === null) { + var self = this; + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: function(success, content) { + if (!success) { + // retry first with JSONP + self.jsonp = true; + self._sendQueriesBatch(params, callback); + } else { + self.jsonp = false; + callback && callback(success, content); + } + } + }); + } else if (this.jsonp) { + var jsonpParams = ''; + for (var i = 0; i < params.requests.length; ++i) { + var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params; + jsonpParams += i + '=' + encodeURIComponent(q) + '&'; + } + var pObj = {params: jsonpParams}; + return this._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/*', + body: pObj, + callback: callback }); + } else { + return this._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: params, + callback: callback}); + } + }, + /* + * Wrapper that try all hosts to maximize the quality of service + */ + _jsonRequest: function(opts) { + var self = this; + var callback = opts.callback; + var cache = null; + var cacheID = opts.url; + var deferred = null; + if (this.options.jQuery) { + deferred = this.options.jQuery.$.Deferred(); + deferred.promise = deferred.promise(); // promise is a property in angular + } else if (this.options.angular) { + deferred = this.options.angular.$q.defer(); + } + + if (!this._isUndefined(opts.body)) { + cacheID = opts.url + '_body_' + JSON.stringify(opts.body); + } + if (!this._isUndefined(opts.cache)) { + cache = opts.cache; + if (!this._isUndefined(cache[cacheID])) { + if (!this._isUndefined(callback) && callback) { + setTimeout(function () { callback(true, cache[cacheID]); }, 1); + } + deferred && deferred.resolve(cache[cacheID]); + return deferred && deferred.promise; + } + } + + opts.successiveRetryCount = 0; + var impl = function() { + + if (opts.successiveRetryCount >= self.hosts.length) { + var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' }; + if (!self._isUndefined(callback) && callback) { + opts.successiveRetryCount = 0; + callback(false, error); + } + deferred && deferred.reject(error); + return; + } + opts.callback = function(retry, success, body) { + if (success && !self._isUndefined(opts.cache)) { + cache[cacheID] = body; + } + if (!success && retry) { + self.currentHostIndex = ++self.currentHostIndex % self.hosts.length; + opts.successiveRetryCount += 1; + impl(); + } else { + opts.successiveRetryCount = 0; + deferred && (success ? deferred.resolve(body) : deferred.reject(body)); + if (!self._isUndefined(callback) && callback) { + callback(success, body); + } + } + }; + opts.hostname = self.hosts[self.currentHostIndex]; + self._jsonRequestByHost(opts); + }; + impl(); + + return deferred && deferred.promise; + }, + + _jsonRequestByHost: function(opts) { + var self = this; + var url = opts.hostname + opts.url; + + if (this.jsonp) { + this._makeJsonpRequestByHost(url, opts); + } else if (this.options.jQuery) { + this._makejQueryRequestByHost(url, opts); + } else if (this.options.angular) { + this._makeAngularRequestByHost(url, opts); + } else { + this._makeXmlHttpRequestByHost(url, opts); + } + }, + + /** + * Make a $http + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeAngularRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.angular.$http({ + url: url, + method: opts.method, + data: body, + cache: false, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)) + }).then(function(response) { + opts.callback(false, true, response.data); + }, function(response) { + if (response.status === 0) { + // xhr.timeout is not handled by Angular.js right now + // let's retry + opts.callback(true, false, response.data); + } else if (response.status == 400 || response.status === 403 || response.status === 404) { + opts.callback(false, false, response.data); + } else { + opts.callback(true, false, response.data); + } + }); + }, + + /** + * Make a $.ajax + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makejQueryRequestByHost: function(url, opts) { + var self = this; + var body = null; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + this.options.jQuery.$.ajax(url, { + type: opts.method, + timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)), + dataType: 'json', + data: body, + error: function(xhr, textStatus, error) { + if (textStatus === 'timeout') { + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) { + opts.callback(false, false, xhr.responseJSON ); + } else { + opts.callback(true, false, { 'message': error } ); + } + }, + success: function(data, textStatus, xhr) { + opts.callback(false, true, data); + } + }); + }, + + /** + * Make a JSONP request + * + * @param url request url (includes endpoint and path) + * @param opts all request options + */ + _makeJsonpRequestByHost: function(url, opts) { + if (opts.method !== 'GET') { + opts.callback(true, false, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' }); + return; + } + + var cbCalled = false; + var timedOut = false; + + AlgoliaSearch.JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cb = 'algoliaJSONP_' + AlgoliaSearch.JSONPCounter; + var done = false; + var ontimeout; + var success; + var clean; + + window[cb] = function(data) { + try { delete window[cb]; } catch (e) { window[cb] = undefined; } + + if (timedOut) { + return; + } + + var status = + data && data.message && data.status || + data && 200; + + var ok = status === 200; + var retry = !ok && status !== 400 && status !== 403 && status !== 404; + cbCalled = true; + opts.callback(retry, ok, data); + }; + + script.type = 'text/javascript'; + url += '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey; + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + if (opts.body && opts.body.params) { + url += '&' + opts.body.params; + } + + ontimeout = setTimeout(function() { + timedOut = true; + clean(); + + opts.callback(true, false, { 'message': 'Timeout - Failed to load JSONP script.' }); + }, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + + success = function() { + if (done || timedOut) { + return; + } + + done = true; + clean(); + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + } + }; + + clean = function() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + + try { + delete window[cb]; + delete window[cb + '_loaded']; + } catch (e) { + window[cb] = null; + window[cb + '_loaded'] = null; + } + }; + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = function() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + }; + + script.onload = function() { + success(); + }; + + script.onerror = function() { + if (done || timedOut) { + return; + } + + clean(); + opts.callback(true, false, { 'message': 'Failed to load JSONP script.' }); + }; + + script.async = true; + script.defer = true; + script.src = url; + + head.appendChild(script); + }, + + /** + * Make a XmlHttpRequest + * + * @param url request url (includes endpoint and path) + * @param opts all request opts + */ + _makeXmlHttpRequestByHost: function(url, opts) { + // no cors or XDomainRequest, no request + if (!this._support.cors && !this._support.hasXDomainRequest) { + // very old browser, not supported + opts.callback(false, false, { 'message': 'CORS not supported' }); + return; + } + + var body = null; + var request = this._support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var ontimeout; + var self = this; + var timedOut; + var timeoutListener; + + if (!this._isUndefined(opts.body)) { + body = JSON.stringify(opts.body); + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey; + url += '&X-Algolia-Application-Id=' + this.applicationID; + + if (this.userToken) { + url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken); + } + + if (this.tagFilters) { + url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters); + } + + url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua); + + for (var i = 0; i < this.extraHeaders.length; ++i) { + url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value; + } + + timeoutListener = function() { + if (!self._support.timeout) { + timedOut = true; + request.abort(); + } + + opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } ); + }; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (request instanceof XMLHttpRequest) { + request.open(opts.method, url, true); + } else { + request.open(opts.method, url); + } + + if (this._support.cors && body !== null && opts.method !== 'GET') { + request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + request.onload = function(/*event*/) { + // When browser does not supports request.timeout, we can + // have both a load and timeout event + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + var response = null; + + try { + response = JSON.parse(request.responseText); + } catch(e) {} + + var status = + // XHR provides a `status` property + request.status || + + // XDR does not have a `status` property, + // we rely on our own API response `status`, only + // provided when an error occurs, so we expect a .message + response && response.message && response.status || + + // XDR default to success when no response.status + response && 200; + + var success = status === 200 || status === 201; + var retry = !success && status !== 400 && status !== 403 && status !== 404; + + opts.callback(retry, success, response); + }; + + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + request.onprogress = function noop() {}; + + if (this._support.timeout) { + // .timeout supported by both XHR and XDR, + // we do receive timeout event, tested + request.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1); + + request.ontimeout = timeoutListener; + } else { + ontimeout = setTimeout(timeoutListener, this.requestTimeoutInMs * (opts.successiveRetryCount + 1)); + } + + request.onerror = function(event) { + if (timedOut) { + return; + } + + if (!self._support.timeout) { + clearTimeout(ontimeout); + } + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + opts.callback(true, false, { 'message': 'Could not connect to host', 'error': event } ); + }; + + request.send(body); + }, + + /* + * Transform search param object in query string + */ + _getSearchParams: function(args, params) { + if (this._isUndefined(args) || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args.hasOwnProperty(key)) { + params += (params.length === 0) ? '?' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]); + } + } + return params; + }, + _isUndefined: function(obj) { + return obj === void 0; + }, + + _support: { + hasXMLHttpRequest: 'XMLHttpRequest' in window, + hasXDomainRequest: 'XDomainRequest' in window, + cors: 'withCredentials' in new XMLHttpRequest(), + timeout: 'timeout' in new XMLHttpRequest() + } +}; + +/* + * Contains all the functions related to one index + * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object + */ +AlgoliaSearch.prototype.Index.prototype = { + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Add an object in this index + * + * @param content contains the javascript object to add inside the index + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + * @param objectID (optional) an objectID you want to attribute to this object + * (if the attribute already exist the old object will be overwrite) + */ + addObject: function(content, callback, objectID) { + var indexObj = this; + if (this.as._isUndefined(objectID)) { + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName), + body: content, + callback: callback }); + } else { + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + body: content, + callback: callback }); + } + + }, + /* + * Add several objects + * + * @param objects contains an array of objects to add + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + addObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'addObject', + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Get an object from this index + * + * @param objectID the unique identifier of the object to retrieve + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the object to retrieve or the error message if a failure occured + * @param attributes (optional) if set, contains the array of attribute names to retrieve + */ + getObject: function(objectID, callback, attributes) { + if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) { + attributes = callback; + callback = null; + } + var indexObj = this; + var params = ''; + if (!this.as._isUndefined(attributes)) { + params = '?attributes='; + for (var i = 0; i < attributes.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attributes[i]; + } + } + + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + callback: callback }); + }, + + /* + * Update partially an object (only update attributes passed in argument) + * + * @param partialObject contains the javascript attributes to override, the + * object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + partialUpdateObject: function(partialObject, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial', + body: partialObject, + callback: callback }); + }, + /* + * Partially Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + partialUpdateObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'partialUpdateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Override the content of object + * + * @param object contains the javascript object to save, the object must contains an objectID attribute + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObject: function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + callback: callback }); + }, + /* + * Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that updateAt and taskID + */ + saveObjects: function(objects, callback) { + var indexObj = this; + var postObj = {requests:[]}; + for (var i = 0; i < objects.length; ++i) { + var request = { action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + callback: callback }); + }, + /* + * Delete an object from the index + * + * @param objectID the unique identifier of object to delete + * @param callback (optional) the result callback with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + deleteObject: function(objectID, callback) { + if (objectID === null || objectID.length === 0) { + callback(false, { message: 'empty objectID'}); + return; + } + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + callback: callback }); + }, + /* + * Search inside the index using XMLHttpRequest request (Using a POST query to + * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). + * + * @param query the full text query + * @param callback the result callback with two arguments: + * success: boolean set to true if the request was successfull. If false, the content contains the error. + * content: the server answer that contains the list of results. + * @param args (optional) if set, contains an object with query parameters: + * - page: (integer) Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. + * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). + * Attributes are separated with a comma (for example "name,address"). + * You can also use an array (for example ["name","address"]). + * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. + * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. + * Attributes are separated by a comma. You can also use an array (for example ["name","address"]). + * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. + * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. + * A matchLevel is returned for each highlighted attribute and can contain: + * - full: if all the query terms were found in the attribute, + * - partial: if only some of the query terms were found, + * - none: if none of the query terms were found. + * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). + * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). + * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). By default no snippet is computed. + * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. + * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. + * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). + * For example aroundLatLng=47.316669,5.016670). + * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision + * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). + * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). + * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. + * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. + * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. + * You can also use an array (for example numericFilters: ["price>100","price<1000"]). + * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. + * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). + * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). + * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). + * - facetFilters: filter the query by a list of facets. + * Facets are separated by commas and each facet is encoded as `attributeName:value`. + * For example: `facetFilters=category:Book,author:John%20Doe`. + * You can also use an array (for example `["category:Book","author:John%20Doe"]`). + * - facets: List of object attributes that you want to use for faceting. + * Comma separated list: `"category,author"` or array `['category','author']` + * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. + * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. + * - queryType: select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. + * Comma separated and array are accepted. + * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. + * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, + * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best + * one is kept and others are removed. + * - restrictSearchableAttributes: List of attributes you want to use for textual search (must be a subset of the attributesToIndex index setting) + * either comma separated or as an array + * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime. + */ + search: function(query, callback, args, delay) { + if (query === undefined || query === null) { + query = ''; + } + + // no query = getAllObjects + if (typeof query === 'function') { + callback = query; + query = ''; + } + + if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) { + args = callback; + callback = null; + } + + var indexObj = this; + var params = 'query=' + encodeURIComponent(query); + if (!this.as._isUndefined(args) && args !== null) { + params = this.as._getSearchParams(args, params); + } + window.clearTimeout(indexObj.onDelayTrigger); + if (!this.as._isUndefined(delay) && delay !== null && delay > 0) { + var onDelayTrigger = window.setTimeout( function() { + indexObj._search(params, callback); + }, delay); + indexObj.onDelayTrigger = onDelayTrigger; + } else { + return this._search(params, callback); + } + }, + + /* + * Browse all index content + * + * @param page Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 + * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. + */ + browse: function(page, callback, hitsPerPage) { + if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) { + hitsPerPage = callback; + callback = null; + } + var indexObj = this; + var params = '?page=' + page; + if (!this.as._isUndefined(hitsPerPage)) { + params += '&hitsPerPage=' + hitsPerPage; + } + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params, + callback: callback }); + }, + + /* + * Get a Typeahead.js adapter + * @param searchParams contains an object with query parameters (see search for details) + */ + ttAdapter: function(params) { + var self = this; + return function(query, cb) { + self.search(query, function(success, content) { + if (success) { + cb(content.hits); + } else { + cb(content && content.message); + } + }, params); + }; + }, + + /* + * Wait the publication of a task on the server. + * All server task are asynchronous and you can check with this method that the task is published. + * + * @param taskID the id of the task returned by server + * @param callback the result callback with with two arguments: + * success: boolean set to true if the request was successfull + * content: the server answer that contains the list of results + */ + waitTask: function(taskID, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID, + callback: function(success, body) { + if (success) { + if (body.status === 'published') { + callback(true, body); + } else { + setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100); + } + } else { + callback(false, body); + } + }}); + }, + + /* + * This function deletes the index content. Settings and index specific API keys are kept untouched. + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + clearIndex: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + callback: callback }); + }, + /* + * Get settings of this index + * + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the settings object or the error message if a failure occured + */ + getSettings: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + callback: callback }); + }, + + /* + * Set settings for this index + * + * @param settigns the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). + * By default no snippet is computed. If set to null, no snippet is computed. + * - attributesToIndex: (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. + * This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to + * retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of + * the list will be considered more important than matches in attributes further down the list. + * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable + * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled + * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreassing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by attributesToIndex, + * - exact: + * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. + * For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV + * show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. + * For example `"customRanking" => ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. + * @param callback (optional) the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer or the error message if a failure occured + */ + setSettings: function(settings, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + body: settings, + callback: callback }); + }, + /* + * List all existing user keys associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + listUserKeys: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + callback: callback }); + }, + /* + * Get ACL of a user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + getUserKeyACL: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Delete an existing user key associated to this index + * + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + deleteUserKey: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKey: function(acls, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /* + * Add an existing user key associated to this index + * + * @param acls the list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. + * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. + * @param callback the result callback with two arguments + * success: boolean set to true if the request was successfull + * content: the server answer with user keys list or error description if success is false. + */ + addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { + var indexObj = this; + var aclsObject = {}; + aclsObject.acl = acls; + aclsObject.validity = validity; + aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; + aclsObject.maxHitsPerQuery = maxHitsPerQuery; + return this.as._jsonRequest({ method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + body: aclsObject, + callback: callback }); + }, + /// + /// Internal methods only after this line + /// + _search: function(params, callback) { + var pObj = {params: params}; + if (this.as.jsonp === null) { + var self = this; + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: function(success, content) { + var status = content && content.status; + if (success || status && Math.floor(status / 100) === 4 || Math.floor(status / 100) === 1) { + self.as.jsonp = false; + callback && callback(success, content); + } else { + self.as.jsonp = true; + self._search(params, callback); + } + } + }); + } else if (this.as.jsonp) { + return this.as._jsonRequest({ cache: this.cache, + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: pObj, + callback: callback }); + } else { + return this.as._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: pObj, + callback: callback}); + } + }, + + // internal attributes + as: null, + indexName: null, + typeAheadArgs: null, + typeAheadValueOption: null +}; + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + var extend = function(out) { + out = out || {}; + for (var i = 1; i < arguments.length; i++) { + if (!arguments[i]) { + continue; + } + for (var key in arguments[i]) { + if (arguments[i].hasOwnProperty(key)) { + out[key] = arguments[i][key]; + } + } + } + return out; + }; + + /** + * Algolia Search Helper providing faceting and disjunctive faceting + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets, the list of disjunctive facets and the default facet filters + */ + window.AlgoliaSearchHelper = function(client, index, options) { + /// Default options + var defaults = { + facets: [], // list of facets to compute + disjunctiveFacets: [], // list of disjunctive facets to compute + hitsPerPage: 20, // number of hits per page + defaultFacetFilters: [] // the default list of facetFilters + }; + + this.init(client, index, extend({}, defaults, options)); + }; + + AlgoliaSearchHelper.prototype = { + /** + * Initialize a new AlgoliaSearchHelper + * @param {AlgoliaSearch} client an AlgoliaSearch client + * @param {string} index the index name to query + * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets + * @return {AlgoliaSearchHelper} + */ + init: function(client, index, options) { + this.client = client; + this.index = index; + this.options = options; + this.page = 0; + this.refinements = {}; + this.excludes = {}; + this.disjunctiveRefinements = {}; + this.extraQueries = []; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + */ + search: function(q, searchCallback, searchParams) { + this.q = q; + this.searchCallback = searchCallback; + this.searchParams = searchParams || {}; + this.page = this.page || 0; + this.refinements = this.refinements || {}; + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this._search(); + }, + + /** + * Remove all refinements (disjunctive + conjunctive) + */ + clearRefinements: function() { + this.disjunctiveRefinements = {}; + this.refinements = {}; + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + this.disjunctiveRefinements[facet][value] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeDisjunctiveRefine: function(facet, value) { + this.disjunctiveRefinements = this.disjunctiveRefinements || {}; + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + try { + delete this.disjunctiveRefinements[facet][value]; + } catch (e) { + this.disjunctiveRefinements[facet][value] = undefined; // IE compat + } + }, + + /** + * Ensure a facet refinement exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = true; + }, + + /** + * Ensure a facet refinement does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeRefine: function(facet, value) { + var refinement = facet + ':' + value; + this.refinements = this.refinements || {}; + this.refinements[refinement] = false; + }, + + /** + * Ensure a facet exclude exists + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + addExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = true; + }, + + /** + * Ensure a facet exclude does not exist + * @param {string} facet the facet to refine + * @param {string} value the associated value + */ + removeExclude: function(facet, value) { + var refinement = facet + ':-' + value; + this.excludes = this.excludes || {}; + this.excludes[refinement] = false; + }, + + /** + * Toggle refinement state of an exclude + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleExclude: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':-' + value; + this.excludes[refinement] = !this.excludes[refinement]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Toggle refinement state of a facet + * @param {string} facet the facet to refine + * @param {string} value the associated value + * @return {boolean} true if the facet has been found + */ + toggleRefine: function(facet, value) { + for (var i = 0; i < this.options.facets.length; ++i) { + if (this.options.facets[i] == facet) { + var refinement = facet + ':' + value; + this.refinements[refinement] = !this.refinements[refinement]; + this.page = 0; + this._search(); + return true; + } + } + this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; + for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) { + if (this.options.disjunctiveFacets[j] == facet) { + this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value]; + this.page = 0; + this._search(); + return true; + } + } + return false; + }, + + /** + * Check the refinement state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isRefined: function(facet, value) { + var refinement = facet + ':' + value; + if (this.refinements[refinement]) { + return true; + } + if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) { + return true; + } + return false; + }, + + /** + * Check the exclude state of a facet + * @param {string} facet the facet + * @param {string} value the associated value + * @return {boolean} true if refined + */ + isExcluded: function(facet, value) { + var refinement = facet + ':-' + value; + if (this.excludes[refinement]) { + return true; + } + return false; + }, + + /** + * Go to next page + */ + nextPage: function() { + this._gotoPage(this.page + 1); + }, + + /** + * Go to previous page + */ + previousPage: function() { + if (this.page > 0) { + this._gotoPage(this.page - 1); + } + }, + + /** + * Goto a page + * @param {integer} page The page number + */ + gotoPage: function(page) { + this._gotoPage(page); + }, + + /** + * Configure the page but do not trigger a reload + * @param {integer} page The page number + */ + setPage: function(page) { + this.page = page; + }, + + /** + * Configure the underlying index name + * @param {string} name the index name + */ + setIndex: function(name) { + this.index = name; + }, + + /** + * Get the underlying configured index name + */ + getIndex: function() { + return this.index; + }, + + /** + * Clear the extra queries added to the underlying batch of queries + */ + clearExtraQueries: function() { + this.extraQueries = []; + }, + + /** + * Add an extra query to the underlying batch of queries. Once you add queries + * to the batch, the 2nd parameter of the searchCallback will be an object with a `results` + * attribute listing all search results. + */ + addExtraQuery: function(index, query, params) { + this.extraQueries.push({ index: index, query: query, params: (params || {}) }); + }, + + ///////////// PRIVATE + + /** + * Goto a page + * @param {integer} page The page number + */ + _gotoPage: function(page) { + this.page = page; + this._search(); + }, + + /** + * Perform the underlying queries + */ + _search: function() { + this.client.startQueriesBatch(); + this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams()); + var disjunctiveFacets = []; + var unusedDisjunctiveFacets = {}; + var i = 0; + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (this._hasDisjunctiveRefinements(facet)) { + disjunctiveFacets.push(facet); + } else { + unusedDisjunctiveFacets[facet] = true; + } + } + for (i = 0; i < disjunctiveFacets.length; ++i) { + this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(disjunctiveFacets[i])); + } + for (i = 0; i < this.extraQueries.length; ++i) { + this.client.addQueryInBatch(this.extraQueries[i].index, this.extraQueries[i].query, this.extraQueries[i].params); + } + var self = this; + this.client.sendQueriesBatch(function(success, content) { + if (!success) { + self.searchCallback(false, content); + return; + } + var aggregatedAnswer = content.results[0]; + aggregatedAnswer.disjunctiveFacets = aggregatedAnswer.disjunctiveFacets || {}; + aggregatedAnswer.facets_stats = aggregatedAnswer.facets_stats || {}; + // create disjunctive facets from facets (disjunctive facets without refinements) + for (var facet in unusedDisjunctiveFacets) { + if (aggregatedAnswer.facets[facet] && !aggregatedAnswer.disjunctiveFacets[facet]) { + aggregatedAnswer.disjunctiveFacets[facet] = aggregatedAnswer.facets[facet]; + try { + delete aggregatedAnswer.facets[facet]; + } catch (e) { + aggregatedAnswer.facets[facet] = undefined; // IE compat + } + } + } + // aggregate the disjunctive facets + for (i = 0; i < disjunctiveFacets.length; ++i) { + for (var dfacet in content.results[i + 1].facets) { + aggregatedAnswer.disjunctiveFacets[dfacet] = content.results[i + 1].facets[dfacet]; + if (self.disjunctiveRefinements[dfacet]) { + for (var value in self.disjunctiveRefinements[dfacet]) { + // add the disjunctive reginements if it is no more retrieved + if (!aggregatedAnswer.disjunctiveFacets[dfacet][value] && self.disjunctiveRefinements[dfacet][value]) { + aggregatedAnswer.disjunctiveFacets[dfacet][value] = 0; + } + } + } + } + // aggregate the disjunctive facets stats + for (var stats in content.results[i + 1].facets_stats) { + aggregatedAnswer.facets_stats[stats] = content.results[i + 1].facets_stats[stats]; + } + } + + // Backward compatibility + aggregatedAnswer.facetStats = aggregatedAnswer.facets_stats; + + // add the excludes + for (var exclude in self.excludes) { + if (self.excludes[exclude]) { + var e = exclude.indexOf(':-'); + var facet = exclude.slice(0, e); + var value = exclude.slice(e + 2); + aggregatedAnswer.facets[facet] = aggregatedAnswer.facets[facet] || {}; + if (!aggregatedAnswer.facets[facet][value]) { + aggregatedAnswer.facets[facet][value] = 0; + } + } + } + // call the actual callback + if (self.extraQueries.length === 0) { + self.searchCallback(true, aggregatedAnswer); + } else { + // append the extra queries + var c = { results: [ aggregatedAnswer ] }; + for (i = 0; i < self.extraQueries.length; ++i) { + c.results.push(content.results[1 + disjunctiveFacets.length + i]); + } + self.searchCallback(true, c); + } + }); + }, + + /** + * Build search parameters used to fetch hits + * @return {hash} + */ + _getHitsSearchParams: function() { + var facets = []; + var i = 0; + for (i = 0; i < this.options.facets.length; ++i) { + facets.push(this.options.facets[i]); + } + for (i = 0; i < this.options.disjunctiveFacets.length; ++i) { + var facet = this.options.disjunctiveFacets[i]; + if (!this._hasDisjunctiveRefinements(facet)) { + facets.push(facet); + } + } + return extend({}, { + hitsPerPage: this.options.hitsPerPage, + page: this.page, + facets: facets, + facetFilters: this._getFacetFilters() + }, this.searchParams); + }, + + /** + * Build search parameters used to fetch a disjunctive facet + * @param {string} facet the associated facet name + * @return {hash} + */ + _getDisjunctiveFacetSearchParams: function(facet) { + return extend({}, this.searchParams, { + hitsPerPage: 1, + page: 0, + attributesToRetrieve: [], + attributesToHighlight: [], + attributesToSnippet: [], + facets: facet, + facetFilters: this._getFacetFilters(facet), + analytics: false + }); + }, + + /** + * Test if there are some disjunctive refinements on the facet + */ + _hasDisjunctiveRefinements: function(facet) { + for (var value in this.disjunctiveRefinements[facet]) { + if (this.disjunctiveRefinements[facet][value]) { + return true; + } + } + return false; + }, + + /** + * Build facetFilters parameter based on current refinements + * @param {string} facet if set, the current disjunctive facet + * @return {hash} + */ + _getFacetFilters: function(facet) { + var facetFilters = []; + if (this.options.defaultFacetFilters) { + for (var i = 0; i < this.options.defaultFacetFilters.length; ++i) { + facetFilters.push(this.options.defaultFacetFilters[i]); + } + } + for (var refinement in this.refinements) { + if (this.refinements[refinement]) { + facetFilters.push(refinement); + } + } + for (var refinement in this.excludes) { + if (this.excludes[refinement]) { + facetFilters.push(refinement); + } + } + for (var disjunctiveRefinement in this.disjunctiveRefinements) { + if (disjunctiveRefinement != facet) { + var refinements = []; + for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) { + if (this.disjunctiveRefinements[disjunctiveRefinement][value]) { + refinements.push(disjunctiveRefinement + ':' + value); + } + } + if (refinements.length > 0) { + facetFilters.push(refinements); + } + } + } + return facetFilters; + } + }; +})(); + +/* + * Copyright (c) 2014 Algolia + * http://www.algolia.com/ + * + * 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($) { + + /** + * Algolia Places API + * @param {string} Your application ID + * @param {string} Your API Key + */ + window.AlgoliaPlaces = function(applicationID, apiKey) { + this.init(applicationID, apiKey); + }; + + AlgoliaPlaces.prototype = { + /** + * @param {string} Your application ID + * @param {string} Your API Key + */ + init: function(applicationID, apiKey) { + this.client = new AlgoliaSearch(applicationID, apiKey, 'http', true, ['places-1.algolia.io', 'places-2.algolia.io', 'places-3.algolia.io']); + this.cache = {}; + }, + + /** + * Perform a query + * @param {string} q the user query + * @param {function} searchCallback the result callback called with two arguments: + * success: boolean set to true if the request was successfull + * content: the query answer with an extra 'disjunctiveFacets' attribute + * @param {hash} the list of search parameters + */ + search: function(q, searchCallback, searchParams) { + var indexObj = this; + var params = 'query=' + encodeURIComponent(q); + if (!this.client._isUndefined(searchParams) && searchParams != null) { + params = this.client._getSearchParams(searchParams, params); + } + var pObj = {params: params, apiKey: this.client.apiKey, appID: this.client.applicationID}; + this.client._jsonRequest({ cache: this.cache, + method: 'POST', + url: '/1/places/query', + body: pObj, + callback: searchCallback, + removeCustomHTTPHeaders: true }); + } + }; +})(); + +/* + json2.js + 2014-02-04 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx, + escapable, + gap, + indent, + meta, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.min.js b/dist/bower_components/algoliasearch/dist/algoliasearch.min.js new file mode 100644 index 000000000..51e2a5c94 --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.min.js @@ -0,0 +1,7 @@ +/*! + * algoliasearch 2.9.7 + * https://github.com/algolia/algoliasearch-client-js + * Copyright 2014 Algolia SAS; Licensed MIT + */ + +function AlgoliaExplainResults(a,b,c){function d(a,b){var c=[];if("object"==typeof a&&"matchedWords"in a&&"value"in a){for(var e=!1,f=0;f0?h[0]:"",f.subtitles=[],"undefined"!=typeof c)for(var i=0;i0))return this._sendQueriesBatch(d,a);var f=window.setTimeout(function(){c._sendQueriesBatch(d,a)},b);c.onDelayTrigger=f},setRequestTimeout:function(a){a&&(this.requestTimeoutInMs=parseInt(a,10))},Index:function(a,b){this.indexName=b,this.as=a,this.typeAheadArgs=null,this.typeAheadValueOption=null,this.cache={}},setExtraHeader:function(a,b){this.extraHeaders.push({key:a,value:b})},_sendQueriesBatch:function(a,b){if(null===this.jsonp){var c=this;return this._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/*/queries",body:a,callback:function(d,e){d?(c.jsonp=!1,b&&b(d,e)):(c.jsonp=!0,c._sendQueriesBatch(a,b))}})}if(this.jsonp){for(var d="",e=0;e=b.hosts.length){var h={message:"Cannot connect the Algolia's Search API. Please send an email to support@algolia.com to report the issue."};return!b._isUndefined(c)&&c&&(a.successiveRetryCount=0,c(!1,h)),void(f&&f.reject(h))}a.callback=function(h,i,j){i&&!b._isUndefined(a.cache)&&(d[e]=j),!i&&h?(b.currentHostIndex=++b.currentHostIndex%b.hosts.length,a.successiveRetryCount+=1,g()):(a.successiveRetryCount=0,f&&(i?f.resolve(j):f.reject(j)),!b._isUndefined(c)&&c&&c(i,j))},a.hostname=b.hosts[b.currentHostIndex],b._jsonRequestByHost(a)};return g(),f&&f.promise},_jsonRequestByHost:function(a){var b=a.hostname+a.url;this.jsonp?this._makeJsonpRequestByHost(b,a):this.options.jQuery?this._makejQueryRequestByHost(b,a):this.options.angular?this._makeAngularRequestByHost(b,a):this._makeXmlHttpRequestByHost(b,a)},_makeAngularRequestByHost:function(a,b){var c=null;this._isUndefined(b.body)||(c=JSON.stringify(b.body)),a+=(-1===a.indexOf("?")?"?":"&")+"X-Algolia-API-Key="+this.apiKey,a+="&X-Algolia-Application-Id="+this.applicationID,this.userToken&&(a+="&X-Algolia-UserToken="+encodeURIComponent(this.userToken)),this.tagFilters&&(a+="&X-Algolia-TagFilters="+encodeURIComponent(this.tagFilters)),a+="&X-Algolia-Agent="+encodeURIComponent(this._ua);for(var d=0;d0))return this._search(f,b);var g=window.setTimeout(function(){e._search(f,b)},d);e.onDelayTrigger=g},browse:function(a,b,c){+b>0&&(this.as._isUndefined(c)||!c)&&(c=b,b=null);var d=this,e="?page="+a;return this.as._isUndefined(c)||(e+="&hitsPerPage="+c),this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/browse"+e,callback:b})},ttAdapter:function(a){var b=this;return function(c,d){b.search(c,function(a,b){d(a?b.hits:b&&b.message)},a)}},waitTask:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/task/"+a,callback:function(d,e){d?"published"===e.status?b(!0,e):setTimeout(function(){c.waitTask(a,b)},100):b(!1,e)}})},clearIndex:function(a){var b=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/clear",callback:a})},getSettings:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/settings",callback:a})},setSettings:function(a,b){var c=this;return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/settings",body:a,callback:b})},listUserKeys:function(a){var b=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/keys",callback:a})},getUserKeyACL:function(a,b){var c=this;return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},deleteUserKey:function(a,b){var c=this;return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},addUserKey:function(a,b){var c=this,d={};return d.acl=a,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys",body:d,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};return g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(f.indexName)+"/keys",body:g,callback:e})},_search:function(a,b){var c={params:a};if(null===this.as.jsonp){var d=this;return this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:function(c,e){var f=e&&e.status;c||f&&4===Math.floor(f/100)||1===Math.floor(f/100)?(d.as.jsonp=!1,b&&b(c,e)):(d.as.jsonp=!0,d._search(a,b))}})}return this.as.jsonp?this.as._jsonRequest({cache:this.cache,method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName),body:c,callback:b}):this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:c,callback:b})},as:null,indexName:null,typeAheadArgs:null,typeAheadValueOption:null},function(a){var b=function(a){a=a||{};for(var b=1;b0&&this._gotoPage(this.page-1)},gotoPage:function(a){this._gotoPage(a)},setPage:function(a){this.page=a},setIndex:function(a){this.index=a},getIndex:function(){return this.index},clearExtraQueries:function(){this.extraQueries=[]},addExtraQuery:function(a,b,c){this.extraQueries.push({index:a,query:b,params:c||{}})},_gotoPage:function(a){this.page=a,this._search()},_search:function(){this.client.startQueriesBatch(),this.client.addQueryInBatch(this.index,this.q,this._getHitsSearchParams());var a=[],b={},c=0;for(c=0;c0&&b.push(f)}return b}}}(),function(a){window.AlgoliaPlaces=function(a,b){this.init(a,b)},AlgoliaPlaces.prototype={init:function(a,b){this.client=new AlgoliaSearch(a,b,"http",!0,["places-1.algolia.io","places-2.algolia.io","places-3.algolia.io"]),this.cache={}},search:function(a,b,c){var d="query="+encodeURIComponent(a);this.client._isUndefined(c)||null==c||(d=this.client._getSearchParams(c,d));var e={params:d,apiKey:this.client.apiKey,appID:this.client.applicationID};this.client._jsonRequest({cache:this.cache,method:"POST",url:"/1/places/query",body:e,callback:b,removeCustomHTTPHeaders:!0})}}}(),"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return 10>a?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g,h=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,g=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;f>c;c+=1)g[c]=str(c,i)||"null";return e=0===g.length?"[]":gap?"[\n"+gap+g.join(",\n"+gap)+"\n"+h+"]":"["+g.join(",")+"]",gap=h,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;f>c;c+=1)"string"==typeof rep[c]&&(d=rep[c],e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&g.push(quote(d)+(gap?": ":":")+e));return e=0===g.length?"{}":gap?"{\n"+gap+g.join(",\n"+gap)+"\n"+h+"}":"{"+g.join(",")+"}",gap=h,e}}"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()});var cx,escapable,gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;c>d;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(); \ No newline at end of file diff --git a/dist/bower_components/algoliasearch/dist/algoliasearch.parse.js b/dist/bower_components/algoliasearch/dist/algoliasearch.parse.js new file mode 100644 index 000000000..e97995f90 --- /dev/null +++ b/dist/bower_components/algoliasearch/dist/algoliasearch.parse.js @@ -0,0 +1,6085 @@ +module.exports = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + + + // This is the Parse entry point + // See https://www.parse.com/docs/cloud_code_guide#cloud_code + module.exports = algoliasearch; + + // parse has no process.env, force it down for npm modules compatibility + process.env = {}; + + // a lot of node modules are expecting to find a `global` object, + // this has triggered some bugs + /* global global: true */ + global = {}; + + var debug = __webpack_require__(1)('algoliasearch:parse'); + + var inherits = __webpack_require__(4); + + var AlgoliaSearchServer = __webpack_require__(5); + + debug('loaded the Parse client'); + + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = __webpack_require__(79); + opts = cloneDeep(opts || {}); + + if (opts.protocol === undefined) { + opts.protocol = 'https:'; + } + + if (opts.timeout === undefined) { + opts.timeout = 7500; + } + + opts._setTimeout = _setTimeout; + + opts._ua = opts._ua || algoliasearch.ua; + opts._useCache = false; + + return new AlgoliaSearchParse(applicationID, apiKey, opts); + } + + algoliasearch.version = __webpack_require__(80); + algoliasearch.ua = 'Algolia for Parse ' + algoliasearch.version; + + function AlgoliaSearchParse() { + // call AlgoliaSearchServer constructor + AlgoliaSearchServer.apply(this, arguments); + } + + inherits(AlgoliaSearchParse, AlgoliaSearchServer); + + AlgoliaSearchParse.prototype._request = function(rawUrl, opts) { + /* global Parse */ + var clone = __webpack_require__(34); + var promise = new Parse.Promise(); + + debug('url: %s, opts: %j', rawUrl, opts); + + var parseReqOpts = { + url: rawUrl, + headers: clone(opts.headers), + method: opts.method, + success: success, + error: error + }; + + if (opts.body) { + // parse is proxing our requests and requires us to set a charset. while json is always utf-8 + parseReqOpts.headers['content-type'] = 'application/json;charset=utf-8'; + parseReqOpts.body = opts.body; + } + + Parse.Cloud.httpRequest(parseReqOpts); + + function error(res) { + debug('error: %j - %s %j', res, rawUrl, opts); + + // we still resolve, bc Parse does not distinguish network errors + // from 400/500 statuses + promise.resolve({ + statusCode: res.status, + body: res.data, + headers: res.headers + }); + } + + function success(res) { + debug('success: %j - %s %j', res, rawUrl, opts); + + promise.resolve({ + statusCode: res.status, + body: res.data, + headers: res.headers + }); + } + + return promise; + }; + + AlgoliaSearchParse.prototype._promise = { + reject: function(val) { + return Parse.Promise.error(val); + }, + resolve: function(val) { + return Parse.Promise.as(val); + }, + delay: function(ms) { + var promise = new Parse.Promise(); + + _setTimeout(promise.resolve.bind(promise), ms); + + return promise; + } + }; + + // There's no setTimeout in Parse cloud, but we have nextTick + function _setTimeout(fn, ms) { + var start = Date.now(); + + process.nextTick(fakeSetTimeout); + + function fakeSetTimeout() { + if (Date.now() < start + ms) { + process.nextTick(fakeSetTimeout); + return; + } + + fn(); + } + } + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + + /** + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + + exports = module.exports = __webpack_require__(2); + exports.log = log; + exports.formatArgs = formatArgs; + exports.save = save; + exports.load = load; + exports.useColors = useColors; + exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); + + /** + * Colors. + */ + + exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' + ]; + + /** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + + function useColors() { + // is webkit? http://stackoverflow.com/a/16459606/376773 + return ('WebkitAppearance' in document.documentElement.style) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (window.console && (console.firebug || (console.exception && console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31); + } + + /** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + + exports.formatters.j = function(v) { + return JSON.stringify(v); + }; + + + /** + * Colorize log arguments if enabled. + * + * @api public + */ + + function formatArgs() { + var args = arguments; + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return args; + + var c = 'color: ' + this.color; + args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1)); + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); + return args; + } + + /** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + + function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); + } + + /** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + + function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} + } + + /** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + + function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + return r; + } + + /** + * Enable namespaces listed in `localStorage.debug` initially. + */ + + exports.enable(load()); + + /** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + + function localstorage(){ + try { + return window.localStorage; + } catch (e) {} + } + + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + + /** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + + exports = module.exports = debug; + exports.coerce = coerce; + exports.disable = disable; + exports.enable = enable; + exports.enabled = enabled; + exports.humanize = __webpack_require__(3); + + /** + * The currently active debug mode names, and names to skip. + */ + + exports.names = []; + exports.skips = []; + + /** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lowercased letter, i.e. "n". + */ + + exports.formatters = {}; + + /** + * Previously assigned color. + */ + + var prevColor = 0; + + /** + * Previous log timestamp. + */ + + var prevTime; + + /** + * Select a color. + * + * @return {Number} + * @api private + */ + + function selectColor() { + return exports.colors[prevColor++ % exports.colors.length]; + } + + /** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + + function debug(namespace) { + + // define the `disabled` version + function disabled() { + } + disabled.enabled = false; + + // define the `enabled` version + function enabled() { + + var self = enabled; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // add the `color` if not set + if (null == self.useColors) self.useColors = exports.useColors(); + if (null == self.color && self.useColors) self.color = selectColor(); + + var args = Array.prototype.slice.call(arguments); + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %o + args = ['%o'].concat(args); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + if ('function' === typeof exports.formatArgs) { + args = exports.formatArgs.apply(self, args); + } + var logFn = enabled.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + enabled.enabled = true; + + var fn = exports.enabled(namespace) ? enabled : disabled; + + fn.namespace = namespace; + + return fn; + } + + /** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + + function enable(namespaces) { + exports.save(namespaces); + + var split = (namespaces || '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } + } + + /** + * Disable debug output. + * + * @api public + */ + + function disable() { + exports.enable(''); + } + + /** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + + function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; + } + + /** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + + function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; + } + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + /** + * Helpers. + */ + + var s = 1000; + var m = s * 60; + var h = m * 60; + var d = h * 24; + var y = d * 365.25; + + /** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} options + * @return {String|Number} + * @api public + */ + + module.exports = function(val, options){ + options = options || {}; + if ('string' == typeof val) return parse(val); + return options.long + ? long(val) + : short(val); + }; + + /** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + + function parse(str) { + str = '' + str; + if (str.length > 10000) return; + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + } + } + + /** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function short(ms) { + if (ms >= d) return Math.round(ms / d) + 'd'; + if (ms >= h) return Math.round(ms / h) + 'h'; + if (ms >= m) return Math.round(ms / m) + 'm'; + if (ms >= s) return Math.round(ms / s) + 's'; + return ms + 'ms'; + } + + /** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function long(ms) { + return plural(ms, d, 'day') + || plural(ms, h, 'hour') + || plural(ms, m, 'minute') + || plural(ms, s, 'second') + || ms + ' ms'; + } + + /** + * Pluralization helper. + */ + + function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; + return Math.ceil(ms / n) + ' ' + name + 's'; + } + + +/***/ }, +/* 4 */ +/***/ function(module, exports) { + + if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; + } else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } + } + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + + + // Some methods only accessible server side + + module.exports = AlgoliaSearchServer; + + var inherits = __webpack_require__(4); + + var AlgoliaSearch = __webpack_require__(6); + + function AlgoliaSearchServer(applicationID, apiKey, opts) { + // Default protocol is https: on the server, to avoid leaking admin keys + if (opts.protocol === undefined) { + opts.protocol = 'https:'; + } + + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchServer, AlgoliaSearch); + + /* + * Allow to use IP rate limit when you have a proxy between end-user and Algolia. + * This option will set the X-Forwarded-For HTTP header with the client IP and the X-Forwarded-API-Key with the API Key having rate limits. + * @param adminAPIKey the admin API Key you can find in your dashboard + * @param endUserIP the end user IP (you can use both IPV4 or IPV6 syntax) + * @param rateLimitAPIKey the API key on which you have a rate limit + */ + AlgoliaSearchServer.prototype.enableRateLimitForward = function(adminAPIKey, endUserIP, rateLimitAPIKey) { + this._forward = { + adminAPIKey: adminAPIKey, + endUserIP: endUserIP, + rateLimitAPIKey: rateLimitAPIKey + }; + }; + + /* + * Disable IP rate limit enabled with enableRateLimitForward() function + */ + AlgoliaSearchServer.prototype.disableRateLimitForward = function() { + this._forward = null; + }; + + /* + * Specify the securedAPIKey to use with associated information + */ + AlgoliaSearchServer.prototype.useSecuredAPIKey = function(securedAPIKey, securityTags, userToken) { + this._secure = { + apiKey: securedAPIKey, + securityTags: securityTags, + userToken: userToken + }; + }; + + /* + * If a secured API was used, disable it + */ + AlgoliaSearchServer.prototype.disableSecuredAPIKey = function() { + this._secure = null; + }; + + AlgoliaSearchServer.prototype._computeRequestHeaders = function() { + var headers = AlgoliaSearchServer.super_.prototype._computeRequestHeaders.call(this); + + if (this._forward) { + headers['x-algolia-api-key'] = this._forward.adminAPIKey; + headers['x-forwarded-for'] = this._forward.endUserIP; + headers['x-forwarded-api-key'] = this._forward.rateLimitAPIKey; + } + + if (this._secure) { + headers['x-algolia-api-key'] = this._secure.apiKey; + headers['x-algolia-tagfilters'] = this._secure.securityTags; + headers['x-algolia-usertoken'] = this._secure.userToken; + } + + return headers; + }; + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + + + module.exports = AlgoliaSearch; + + var errors = __webpack_require__(7); + + /* + * Algolia Search library initialization + * https://www.algolia.com/ + * + * @param {string} applicationID - Your applicationID, found in your dashboard + * @param {string} apiKey - Your API key, found in your dashboard + * @param {Object} [opts] + * @param {number} [opts.timeout=2000] - The request timeout set in milliseconds, + * another request will be issued after this timeout + * @param {string} [opts.protocol='http:'] - The protocol used to query Algolia Search API. + * Set to 'https:' to force using https. + * Default to document.location.protocol in browsers + * @param {Object|Array} [opts.hosts={ + * read: [this.applicationID + '-dsn.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]), + * write: [this.applicationID + '.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]) - The hosts to use for Algolia Search API. + * If you provide them, you will less benefit from our HA implementation + */ + function AlgoliaSearch(applicationID, apiKey, opts) { + var debug = __webpack_require__(1)('algoliasearch'); + + var clone = __webpack_require__(34); + var isArray = __webpack_require__(27); + var map = __webpack_require__(44); + + var usage = 'Usage: algoliasearch(applicationID, apiKey, opts)'; + + if (!applicationID) { + throw new errors.AlgoliaSearchError('Please provide an application ID. ' + usage); + } + + if (!apiKey) { + throw new errors.AlgoliaSearchError('Please provide an API key. ' + usage); + } + + this.applicationID = applicationID; + this.apiKey = apiKey; + + var defaultHosts = [ + this.applicationID + '-1.algolianet.com', + this.applicationID + '-2.algolianet.com', + this.applicationID + '-3.algolianet.com' + ]; + this.hosts = { + read: [], + write: [] + }; + + this.hostIndex = { + read: 0, + write: 0 + }; + + opts = opts || {}; + + var protocol = opts.protocol || 'https:'; + var timeout = opts.timeout === undefined ? 2000 : opts.timeout; + + // while we advocate for colon-at-the-end values: 'http:' for `opts.protocol` + // we also accept `http` and `https`. It's a common error. + if (!/:$/.test(protocol)) { + protocol = protocol + ':'; + } + + if (opts.protocol !== 'http:' && opts.protocol !== 'https:') { + throw new errors.AlgoliaSearchError('protocol must be `http:` or `https:` (was `' + opts.protocol + '`)'); + } + + // no hosts given, add defaults + if (!opts.hosts) { + this.hosts.read = [this.applicationID + '-dsn.algolia.net'].concat(defaultHosts); + this.hosts.write = [this.applicationID + '.algolia.net'].concat(defaultHosts); + } else if (isArray(opts.hosts)) { + this.hosts.read = clone(opts.hosts); + this.hosts.write = clone(opts.hosts); + } else { + this.hosts.read = clone(opts.hosts.read); + this.hosts.write = clone(opts.hosts.write); + } + + // add protocol and lowercase hosts + this.hosts.read = map(this.hosts.read, prepareHost(protocol)); + this.hosts.write = map(this.hosts.write, prepareHost(protocol)); + this.requestTimeout = timeout; + + this.extraHeaders = []; + this.cache = {}; + + this._ua = opts._ua; + this._useCache = opts._useCache === undefined ? true : opts._useCache; + + this._setTimeout = opts._setTimeout; + + debug('init done, %j', this); + } + + AlgoliaSearch.prototype = { + /* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ + deleteIndex: function(indexName, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + hostType: 'write', + callback: callback + }); + }, + /** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of + * srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ + moveIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = { + operation: 'move', destination: dstIndexName + }; + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy + * of srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ + copyIndex: function(srcIndexName, dstIndexName, callback) { + var postObj = { + operation: 'copy', destination: dstIndexName + }; + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ + getLogs: function(offset, length, callback) { + if (arguments.length === 0 || typeof offset === 'function') { + // getLogs([cb]) + callback = offset; + offset = 0; + length = 10; + } else if (arguments.length === 1 || typeof length === 'function') { + // getLogs(1, [cb)] + callback = length; + length = 10; + } + + return this._jsonRequest({ + method: 'GET', + url: '/1/logs?offset=' + offset + '&length=' + length, + hostType: 'read', + callback: callback + }); + }, + /* + * List all existing indexes (paginated) + * + * @param page The page to retrieve, starting at 0. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with index list + */ + listIndexes: function(page, callback) { + var params = ''; + + if (page === undefined || typeof page === 'function') { + callback = page; + } else { + params = '?page=' + page; + } + + return this._jsonRequest({ + method: 'GET', + url: '/1/indexes' + params, + hostType: 'read', + callback: callback + }); + }, + + /* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ + initIndex: function(indexName) { + return new this.Index(this, indexName); + }, + /* + * List all existing user keys with their associated ACLs + * + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + listUserKeys: function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys', + hostType: 'read', + callback: callback + }); + }, + /* + * Get ACL of a user key + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + getUserKeyACL: function(key, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys/' + key, + hostType: 'read', + callback: callback + }); + }, + /* + * Delete an existing user key + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + deleteUserKey: function(key, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/keys/' + key, + hostType: 'write', + callback: callback + }); + }, + /* + * Add a new global API key + * + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.addUserKey(['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#AddKey|Algolia REST API Documentation} + */ + addUserKey: function(acls, params, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: client.addUserKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.indexes = params.indexes; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/keys', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /** + * Add a new global API key + * @deprecated Please use client.addUserKey() + */ + addUserKeyWithValidity: deprecate(function(acls, params, callback) { + return this.addUserKey(acls, params, callback); + }, deprecatedMessage('client.addUserKeyWithValidity()', 'client.addUserKey()')), + + /** + * Update an existing API key + * @param {string} key - The key to update + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.updateUserKey('APIKEY', ['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} + */ + updateUserKey: function(key, acls, params, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: client.updateUserKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.indexes = params.indexes; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'PUT', + url: '/1/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); + }, + + /** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ + setSecurityTags: function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + + this.securityTags = tags; + }, + + /** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ + setUserToken: function(userToken) { + this.userToken = userToken; + }, + + /** + * Initialize a new batch of search queries + * @deprecated use client.search() + */ + startQueriesBatch: deprecate(function startQueriesBatchDeprecated() { + this._batch = []; + }, deprecatedMessage('client.startQueriesBatch()', 'client.search()')), + + /** + * Add a search query in the batch + * @deprecated use client.search() + */ + addQueryInBatch: deprecate(function addQueryInBatchDeprecated(indexName, query, args) { + this._batch.push({ + indexName: indexName, + query: query, + params: args + }); + }, deprecatedMessage('client.addQueryInBatch()', 'client.search()')), + + /** + * Clear all queries in client's cache + * @return undefined + */ + clearCache: function() { + this.cache = {}; + }, + + /** + * Launch the batch of queries using XMLHttpRequest. + * @deprecated use client.search() + */ + sendQueriesBatch: deprecate(function sendQueriesBatchDeprecated(callback) { + return this.search(this._batch, callback); + }, deprecatedMessage('client.sendQueriesBatch()', 'client.search()')), + + /** + * Set the number of milliseconds a request can take before automatically being terminated. + * + * @param {Number} milliseconds + */ + setRequestTimeout: function(milliseconds) { + if (milliseconds) { + this.requestTimeout = parseInt(milliseconds, 10); + } + }, + + /** + * Search through multiple indices at the same time + * @param {Object[]} queries An array of queries you want to run. + * @param {string} queries[].indexName The index name you want to target + * @param {string} [queries[].query] The query to issue on this index. Can also be passed into `params` + * @param {Object} queries[].params Any search param like hitsPerPage, .. + * @param {Function} callback Callback to be called + * @return {Promise|undefined} Returns a promise if no callback given + */ + search: function(queries, callback) { + var isArray = __webpack_require__(27); + var map = __webpack_require__(44); + + var usage = 'Usage: client.search(arrayOfQueries[, callback])'; + + if (!isArray(queries)) { + throw new Error(usage); + } + + var client = this; + + var postObj = { + requests: map(queries, function prepareRequest(query) { + var params = ''; + + // allow query.query + // so we are mimicing the index.search(query, params) method + // {indexName:, query:, params:} + if (query.query !== undefined) { + params += 'query=' + encodeURIComponent(query.query); + } + + return { + indexName: query.indexName, + params: client._getSearchParams(query.params, params) + }; + }) + }; + + var JSONPParams = map(postObj.requests, function prepareJSONPParams(request, requestId) { + return requestId + '=' + + encodeURIComponent( + '/1/indexes/' + encodeURIComponent(request.indexName) + '?' + + request.params + ); + }).join('&'); + + return this._jsonRequest({ + cache: this.cache, + method: 'POST', + url: '/1/indexes/*/queries', + body: postObj, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/*', + body: { + params: JSONPParams + } + }, + callback: callback + }); + }, + + /** + * Perform write operations accross multiple indexes. + * + * To reduce the amount of time spent on network round trips, + * you can create, update, or delete several objects in one call, + * using the batch endpoint (all operations are done in the given order). + * + * Available actions: + * - addObject + * - updateObject + * - partialUpdateObject + * - partialUpdateObjectNoCreate + * - deleteObject + * + * https://www.algolia.com/doc/rest_api#Indexes + * @param {Object[]} operations An array of operations to perform + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.batch([{ + * action: 'addObject', + * indexName: 'clients', + * body: { + * name: 'Bill' + * } + * }, { + * action: 'udpateObject', + * indexName: 'fruits', + * body: { + * objectID: '29138', + * name: 'banana' + * } + * }], cb) + */ + batch: function(operations, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: client.batch(operations[, callback])'; + + if (!isArray(operations)) { + throw new Error(usage); + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/batch', + body: { + requests: operations + }, + hostType: 'write', + callback: callback + }); + }, + + // environment specific methods + destroy: notImplemented, + enableRateLimitForward: notImplemented, + disableRateLimitForward: notImplemented, + useSecuredAPIKey: notImplemented, + disableSecuredAPIKey: notImplemented, + generateSecuredApiKey: notImplemented, + /* + * Index class constructor. + * You should not use this method directly but use initIndex() function + */ + Index: function(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; + }, + /** + * Add an extra field to the HTTP request + * + * @param name the header field name + * @param value the header field value + */ + setExtraHeader: function(name, value) { + this.extraHeaders.push({ + name: name.toLowerCase(), value: value + }); + }, + + /** + * Augment sent x-algolia-agent with more data, each agent part + * is automatically separated from the others by a semicolon; + * + * @param algoliaAgent the agent to add + */ + addAlgoliaAgent: function(algoliaAgent) { + this._ua += ';' + algoliaAgent; + }, + + /* + * Wrapper that try all hosts to maximize the quality of service + */ + _jsonRequest: function(initialOpts) { + var requestDebug = __webpack_require__(1)('algoliasearch:' + initialOpts.url); + + var body; + var cache = initialOpts.cache; + var client = this; + var tries = 0; + var usingFallback = false; + var hasFallback = client._request.fallback && initialOpts.fallback; + + if (initialOpts.body !== undefined) { + body = safeJSONStringify(initialOpts.body); + } + + requestDebug('request start'); + + function doRequest(requester, reqOpts) { + var cacheID; + + if (client._useCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && body) { + cacheID += '_body_' + reqOpts.body; + } + + // handle cache existence + if (client._useCache && cache && cache[cacheID] !== undefined) { + requestDebug('serving response from cache'); + return client._promise.resolve(JSON.parse(cache[cacheID])); + } + + // if we reached max tries + if (tries >= client.hosts[initialOpts.hostType].length) { + if (!hasFallback || usingFallback) { + requestDebug('could not get any response'); + // then stop + return client._promise.reject(new errors.AlgoliaSearchError( + 'Cannot connect to the AlgoliaSearch API.' + + ' Send an email to support@algolia.com to report and resolve the issue.' + + ' Application id was: ' + client.applicationID + )); + } + + requestDebug('switching to fallback'); + + // let's try the fallback starting from here + tries = 0; + + // method, url and body are fallback dependent + reqOpts.method = initialOpts.fallback.method; + reqOpts.url = initialOpts.fallback.url; + reqOpts.jsonBody = initialOpts.fallback.body; + if (reqOpts.jsonBody) { + reqOpts.body = safeJSONStringify(reqOpts.jsonBody); + } + + reqOpts.timeout = client.requestTimeout * (tries + 1); + client.hostIndex[initialOpts.hostType] = 0; + usingFallback = true; // the current request is now using fallback + return doRequest(client._request.fallback, reqOpts); + } + + var url = client.hosts[initialOpts.hostType][client.hostIndex[initialOpts.hostType]] + reqOpts.url; + var options = { + body: reqOpts.body, + jsonBody: reqOpts.jsonBody, + method: reqOpts.method, + headers: client._computeRequestHeaders(), + timeout: reqOpts.timeout, + debug: requestDebug + }; + + requestDebug('method: %s, url: %s, headers: %j, timeout: %d', + options.method, url, options.headers, options.timeout); + + if (requester === client._request.fallback) { + requestDebug('using fallback'); + } + + // `requester` is any of this._request or this._request.fallback + // thus it needs to be called using the client as context + return requester.call(client, url, options).then(success, tryFallback); + + function success(httpResponse) { + // compute the status of the response, + // + // When in browser mode, using XDR or JSONP, we have no statusCode available + // So we rely on our API response `status` property. + // But `waitTask` can set a `status` property which is not the statusCode (it's the task status) + // So we check if there's a `message` along `status` and it means it's an error + // + // That's the only case where we have a response.status that's not the http statusCode + var status = httpResponse && httpResponse.body && httpResponse.body.message && httpResponse.body.status || + + // this is important to check the request statusCode AFTER the body eventual + // statusCode because some implementations (jQuery XDomainRequest transport) may + // send statusCode 200 while we had an error + httpResponse.statusCode || + + // When in browser mode, using XDR or JSONP + // we default to success when no error (no response.status && response.message) + // If there was a JSON.parse() error then body is null and it fails + httpResponse && httpResponse.body && 200; + + requestDebug('received response: statusCode: %s, computed statusCode: %d, headers: %j', + httpResponse.statusCode, status, httpResponse.headers); + + var ok = status === 200 || status === 201; + var retry = !ok && Math.floor(status / 100) !== 4 && Math.floor(status / 100) !== 1; + + if (client._useCache && ok && cache) { + cache[cacheID] = httpResponse.responseText; + } + + if (ok) { + return httpResponse.body; + } + + if (retry) { + tries += 1; + return retryRequest(); + } + + var unrecoverableError = new errors.AlgoliaSearchError( + httpResponse.body && httpResponse.body.message + ); + + return client._promise.reject(unrecoverableError); + } + + function tryFallback(err) { + // error cases: + // While not in fallback mode: + // - CORS not supported + // - network error + // While in fallback mode: + // - timeout + // - network error + // - badly formatted JSONP (script loaded, did not call our callback) + // In both cases: + // - uncaught exception occurs (TypeError) + requestDebug('error: %s, stack: %s', err.message, err.stack); + + if (!(err instanceof errors.AlgoliaSearchError)) { + err = new errors.Unknown(err && err.message, err); + } + + tries += 1; + + // stop the request implementation when: + if ( + // we did not generate this error, + // it comes from a throw in some other piece of code + err instanceof errors.Unknown || + + // server sent unparsable JSON + err instanceof errors.UnparsableJSON || + + // max tries and already using fallback or no fallback + tries >= client.hosts[initialOpts.hostType].length && + (usingFallback || !hasFallback)) { + // stop request implementation for this command + return client._promise.reject(err); + } + + client.hostIndex[initialOpts.hostType] = ++client.hostIndex[initialOpts.hostType] % client.hosts[initialOpts.hostType].length; + + if (err instanceof errors.RequestTimeout) { + return retryRequest(); + } else if (!usingFallback) { + // next request loop, force using fallback for this request + tries = Infinity; + } + + return doRequest(requester, reqOpts); + } + + function retryRequest() { + client.hostIndex[initialOpts.hostType] = ++client.hostIndex[initialOpts.hostType] % client.hosts[initialOpts.hostType].length; + reqOpts.timeout = client.requestTimeout * (tries + 1); + return doRequest(requester, reqOpts); + } + } + + var promise = doRequest( + client._request, { + url: initialOpts.url, + method: initialOpts.method, + body: body, + jsonBody: initialOpts.body, + timeout: client.requestTimeout * (tries + 1) + } + ); + + // either we have a callback + // either we are using promises + if (initialOpts.callback) { + promise.then(function okCb(content) { + exitPromise(function() { + initialOpts.callback(null, content); + }, client._setTimeout || setTimeout); + }, function nookCb(err) { + exitPromise(function() { + initialOpts.callback(err); + }, client._setTimeout || setTimeout); + }); + } else { + return promise; + } + }, + + /* + * Transform search param object in query string + */ + _getSearchParams: function(args, params) { + if (args === undefined || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args[key] !== undefined && args.hasOwnProperty(key)) { + params += params === '' ? '' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? safeJSONStringify(args[key]) : args[key]); + } + } + return params; + }, + + _computeRequestHeaders: function() { + var forEach = __webpack_require__(8); + + var requestHeaders = { + 'x-algolia-api-key': this.apiKey, + 'x-algolia-application-id': this.applicationID, + 'x-algolia-agent': this._ua + }; + + if (this.userToken) { + requestHeaders['x-algolia-usertoken'] = this.userToken; + } + + if (this.securityTags) { + requestHeaders['x-algolia-tagfilters'] = this.securityTags; + } + + if (this.extraHeaders) { + forEach(this.extraHeaders, function addToRequestHeaders(header) { + requestHeaders[header.name] = header.value; + }); + } + + return requestHeaders; + } + }; + + /* + * Contains all the functions related to one index + * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object + */ + AlgoliaSearch.prototype.Index.prototype = { + /* + * Clear all queries in cache + */ + clearCache: function() { + this.cache = {}; + }, + /* + * Add an object in this index + * + * @param content contains the javascript object to add inside the index + * @param objectID (optional) an objectID you want to attribute to this object + * (if the attribute already exist the old object will be overwrite) + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + addObject: function(content, objectID, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof objectID === 'function') { + callback = objectID; + objectID = undefined; + } + + return this.as._jsonRequest({ + method: objectID !== undefined ? + 'PUT' : // update or create + 'POST', // create (API generates an objectID) + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + // create + (objectID !== undefined ? '/' + encodeURIComponent(objectID) : ''), // update or create + body: content, + hostType: 'write', + callback: callback + }); + }, + /* + * Add several objects + * + * @param objects contains an array of objects to add + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that updateAt and taskID + */ + addObjects: function(objects, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: index.addObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'addObject', + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /* + * Get an object from this index + * + * @param objectID the unique identifier of the object to retrieve + * @param attrs (optional) if set, contains the array of attribute names to retrieve + * @param callback (optional) the result callback called with two arguments + * error: null or Error('message') + * content: the object to retrieve or the error message if a failure occured + */ + getObject: function(objectID, attrs, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof attrs === 'function') { + callback = attrs; + attrs = undefined; + } + + var params = ''; + if (attrs !== undefined) { + params = '?attributes='; + for (var i = 0; i < attrs.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attrs[i]; + } + } + + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + hostType: 'read', + callback: callback + }); + }, + + /* + * Get several objects from this index + * + * @param objectIDs the array of unique identifier of objects to retrieve + */ + getObjects: function(objectIDs, attributesToRetrieve, callback) { + var isArray = __webpack_require__(27); + var map = __webpack_require__(44); + + var usage = 'Usage: index.getObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + + if (arguments.length === 1 || typeof attributesToRetrieve === 'function') { + callback = attributesToRetrieve; + attributesToRetrieve = undefined; + } + + var body = { + requests: map(objectIDs, function prepareRequest(objectID) { + var request = { + indexName: indexObj.indexName, + objectID: objectID + }; + + if (attributesToRetrieve) { + request.attributesToRetrieve = attributesToRetrieve.join(','); + } + + return request; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/objects', + hostType: 'read', + body: body, + callback: callback + }); + }, + + /* + * Update partially an object (only update attributes passed in argument) + * + * @param partialObject contains the javascript attributes to override, the + * object must contains an objectID attribute + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + partialUpdateObject: function(partialObject, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial', + body: partialObject, + hostType: 'write', + callback: callback + }); + }, + /* + * Partially Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that updateAt and taskID + */ + partialUpdateObjects: function(objects, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: index.partialUpdateObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'partialUpdateObject', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /* + * Override the content of object + * + * @param object contains the javascript object to save, the object must contains an objectID attribute + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that updateAt and taskID + */ + saveObject: function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + hostType: 'write', + callback: callback + }); + }, + /* + * Override the content of several objects + * + * @param objects contains an array of objects to update (each object must contains a objectID attribute) + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that updateAt and taskID + */ + saveObjects: function(objects, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: index.saveObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /* + * Delete an object from the index + * + * @param objectID the unique identifier of object to delete + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + deleteObject: function(objectID, callback) { + if (typeof objectID === 'function' || typeof objectID !== 'string' && typeof objectID !== 'number') { + var err = new errors.AlgoliaSearchError('Cannot delete an object without an objectID'); + callback = objectID; + if (typeof callback === 'function') { + return callback(err); + } + + return this.as._promise.reject(err); + } + + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + hostType: 'write', + callback: callback + }); + }, + /* + * Delete several objects from an index + * + * @param objectIDs contains an array of objectID to delete + * @param callback (optional) the result callback called with two arguments: + * error: null or Error('message') + * content: the server answer that contains 3 elements: createAt, taskId and objectID + */ + deleteObjects: function(objectIDs, callback) { + var isArray = __webpack_require__(27); + var map = __webpack_require__(44); + + var usage = 'Usage: index.deleteObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: map(objectIDs, function prepareRequest(objectID) { + return { + action: 'deleteObject', + objectID: objectID, + body: { + objectID: objectID + } + }; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + /* + * Delete all objects matching a query + * + * @param query the query string + * @param params the optional query parameters + * @param callback (optional) the result callback called with one argument + * error: null or Error('message') + */ + deleteByQuery: function(query, params, callback) { + var clone = __webpack_require__(34); + var map = __webpack_require__(44); + + var indexObj = this; + var client = indexObj.as; + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = {}; + } else { + params = clone(params); + } + + params.attributesToRetrieve = 'objectID'; + params.hitsPerPage = 1000; + params.distinct = false; + + // when deleting, we should never use cache to get the + // search results + this.clearCache(); + + // there's a problem in how we use the promise chain, + // see how waitTask is done + var promise = this + .search(query, params) + .then(stopOrDelete); + + function stopOrDelete(searchContent) { + // stop here + if (searchContent.nbHits === 0) { + // return indexObj.as._request.resolve(); + return searchContent; + } + + // continue and do a recursive call + var objectIDs = map(searchContent.hits, function getObjectID(object) { + return object.objectID; + }); + + return indexObj + .deleteObjects(objectIDs) + .then(waitTask) + .then(doDeleteByQuery); + } + + function waitTask(deleteObjectsContent) { + return indexObj.waitTask(deleteObjectsContent.taskID); + } + + function doDeleteByQuery() { + return indexObj.deleteByQuery(query, params); + } + + if (!callback) { + return promise; + } + + promise.then(success, failure); + + function success() { + exitPromise(function exit() { + callback(null); + }, client._setTimeout || setTimeout); + } + + function failure(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } + }, + + /* + * Search inside the index using XMLHttpRequest request (Using a POST query to + * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). + * + * @param query the full text query + * @param args (optional) if set, contains an object with query parameters: + * - page: (integer) Pagination parameter used to select the page to retrieve. + * Page is zero-based and defaults to 0. Thus, + * to retrieve the 10th page you need to set page=9 + * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. + * - attributesToRetrieve: a string that contains the list of object attributes + * you want to retrieve (let you minimize the answer size). + * Attributes are separated with a comma (for example "name,address"). + * You can also use an array (for example ["name","address"]). + * By default, all attributes are retrieved. You can also use '*' to retrieve all + * values when an attributesToRetrieve setting is specified for your index. + * - attributesToHighlight: a string that contains the list of attributes you + * want to highlight according to the query. + * Attributes are separated by a comma. You can also use an array (for example ["name","address"]). + * If an attribute has no match for the query, the raw value is returned. + * By default all indexed text attributes are highlighted. + * You can use `*` if you want to highlight all textual attributes. + * Numerical attributes are not highlighted. + * A matchLevel is returned for each highlighted attribute and can contain: + * - full: if all the query terms were found in the attribute, + * - partial: if only some of the query terms were found, + * - none: if none of the query terms were found. + * - attributesToSnippet: a string that contains the list of attributes to snippet alongside + * the number of words to return (syntax is `attributeName:nbWords`). + * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). + * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). + * By default no snippet is computed. + * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. + * Defaults to 3. + * - minWordSizefor2Typos: the minimum number of characters in a query word + * to accept two typos in this word. Defaults to 7. + * - getRankingInfo: if set to 1, the result hits will contain ranking + * information in _rankingInfo attribute. + * - aroundLatLng: search for entries around a given + * latitude/longitude (specified as two floats separated by a comma). + * For example aroundLatLng=47.316669,5.016670). + * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) + * and the precision for ranking with aroundPrecision + * (for example if you set aroundPrecision=100, two objects that are distant of + * less than 100m will be considered as identical for "geo" ranking parameter). + * At indexing, you should specify geoloc of an object with the _geoloc attribute + * (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - insideBoundingBox: search entries inside a given area defined by the two extreme points + * of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). + * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). + * At indexing, you should specify geoloc of an object with the _geoloc attribute + * (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) + * - numericFilters: a string that contains the list of numeric filters you want to + * apply separated by a comma. + * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. + * Supported operands are `<`, `<=`, `=`, `>` and `>=`. + * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. + * You can also use an array (for example numericFilters: ["price>100","price<1000"]). + * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. + * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). + * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] + * means tag1 AND (tag2 OR tag3). + * At indexing, tags should be added in the _tags** attribute + * of objects (for example {"_tags":["tag1","tag2"]}). + * - facetFilters: filter the query by a list of facets. + * Facets are separated by commas and each facet is encoded as `attributeName:value`. + * For example: `facetFilters=category:Book,author:John%20Doe`. + * You can also use an array (for example `["category:Book","author:John%20Doe"]`). + * - facets: List of object attributes that you want to use for faceting. + * Comma separated list: `"category,author"` or array `['category','author']` + * Only attributes that have been added in **attributesForFaceting** index setting + * can be used in this parameter. + * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. + * - queryType: select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - optionalWords: a string that contains the list of words that should + * be considered as optional when found in the query. + * Comma separated and array are accepted. + * - distinct: If set to 1, enable the distinct feature (disabled by default) + * if the attributeForDistinct index setting is set. + * This feature is similar to the SQL "distinct" keyword: when enabled + * in a query with the distinct=1 parameter, + * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have + * the same value for show_name, then only the best + * one is kept and others are removed. + * - restrictSearchableAttributes: List of attributes you want to use for + * textual search (must be a subset of the attributesToIndex index setting) + * either comma separated or as an array + * @param callback the result callback called with two arguments: + * error: null or Error('message'). If false, the content contains the error. + * content: the server answer that contains the list of results. + */ + search: buildSearchMethod('query'), + + /* + * -- BETA -- + * Search a record similar to the query inside the index using XMLHttpRequest request (Using a POST query to + * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). + * + * @param query the similar query + * @param args (optional) if set, contains an object with query parameters. + * All search parameters are supported (see search function), restrictSearchableAttributes and facetFilters + * are the two most useful to restrict the similar results and get more relevant content + */ + similarSearch: buildSearchMethod('similarQuery'), + + /* + * Browse index content. The response content will have a `cursor` property that you can use + * to browse subsequent pages for this query. Use `index.browseFrom(cursor)` when you want. + * + * @param {string} query - The full text query + * @param {Object} [queryParameters] - Any search query parameter + * @param {Function} [callback] - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the browse result + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * index.browse('cool songs', { + * tagFilters: 'public,comments', + * hitsPerPage: 500 + * }, callback); + * @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} + */ + // pre 3.5.0 usage, backward compatible + // browse: function(page, hitsPerPage, callback) { + browse: function(query, queryParameters, callback) { + var merge = __webpack_require__(69); + + var indexObj = this; + + var page; + var hitsPerPage; + + // we check variadic calls that are not the one defined + // .browse()/.browse(fn) + // => page = 0 + if (arguments.length === 0 || arguments.length === 1 && typeof arguments[0] === 'function') { + page = 0; + callback = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'number') { + // .browse(2)/.browse(2, 10)/.browse(2, fn)/.browse(2, 10, fn) + page = arguments[0]; + if (typeof arguments[1] === 'number') { + hitsPerPage = arguments[1]; + } else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + hitsPerPage = undefined; + } + query = undefined; + queryParameters = undefined; + } else if (typeof arguments[0] === 'object') { + // .browse(queryParameters)/.browse(queryParameters, cb) + if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + queryParameters = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { + // .browse(query, cb) + callback = arguments[1]; + queryParameters = undefined; + } + + // otherwise it's a .browse(query)/.browse(query, queryParameters)/.browse(query, queryParameters, cb) + + // get search query parameters combining various possible calls + // to .browse(); + queryParameters = merge({}, queryParameters || {}, { + page: page, + hitsPerPage: hitsPerPage, + query: query + }); + + var params = this.as._getSearchParams(queryParameters, ''); + + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse?' + params, + hostType: 'read', + callback: callback + }); + }, + + /* + * Continue browsing from a previous position (cursor), obtained via a call to `.browse()`. + * + * @param {string} query - The full text query + * @param {Object} [queryParameters] - Any search query parameter + * @param {Function} [callback] - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the browse result + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * index.browseFrom('14lkfsakl32', callback); + * @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} + */ + browseFrom: function(cursor, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/browse?cursor=' + encodeURIComponent(cursor), + hostType: 'read', + callback: callback + }); + }, + + /* + * Browse all content from an index using events. Basically this will do + * .browse() -> .browseFrom -> .browseFrom -> .. until all the results are returned + * + * @param {string} query - The full text query + * @param {Object} [queryParameters] - Any search query parameter + * @return {EventEmitter} + * @example + * var browser = index.browseAll('cool songs', { + * tagFilters: 'public,comments', + * hitsPerPage: 500 + * }); + * + * browser.on('result', function resultCallback(content) { + * console.log(content.hits); + * }); + * + * // if any error occurs, you get it + * browser.on('error', function(err) { + * throw err; + * }); + * + * // when you have browsed the whole index, you get this event + * browser.on('end', function() { + * console.log('finished'); + * }); + * + * // at any point if you want to stop the browsing process, you can stop it manually + * // otherwise it will go on and on + * browser.stop(); + * + * @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} + */ + browseAll: function(query, queryParameters) { + if (typeof query === 'object') { + queryParameters = query; + query = undefined; + } + + var merge = __webpack_require__(69); + + var IndexBrowser = __webpack_require__(77); + + var browser = new IndexBrowser(); + var client = this.as; + var index = this; + var params = client._getSearchParams( + merge({}, queryParameters || {}, { + query: query + }), '' + ); + + // start browsing + browseLoop(); + + function browseLoop(cursor) { + if (browser._stopped) { + return; + } + + var queryString; + + if (cursor !== undefined) { + queryString = 'cursor=' + encodeURIComponent(cursor); + } else { + queryString = params; + } + + client._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(index.indexName) + '/browse?' + queryString, + hostType: 'read', + callback: browseCallback + }); + } + + function browseCallback(err, content) { + if (browser._stopped) { + return; + } + + if (err) { + browser._error(err); + return; + } + + browser._result(content); + + // no cursor means we are finished browsing + if (content.cursor === undefined) { + browser._end(); + return; + } + + browseLoop(content.cursor); + } + + return browser; + }, + + /* + * Get a Typeahead.js adapter + * @param searchParams contains an object with query parameters (see search for details) + */ + ttAdapter: function(params) { + var self = this; + return function ttAdapter(query, syncCb, asyncCb) { + var cb; + + if (typeof asyncCb === 'function') { + // typeahead 0.11 + cb = asyncCb; + } else { + // pre typeahead 0.11 + cb = syncCb; + } + + self.search(query, params, function searchDone(err, content) { + if (err) { + cb(err); + return; + } + + cb(content.hits); + }); + }; + }, + + /* + * Wait the publication of a task on the server. + * All server task are asynchronous and you can check with this method that the task is published. + * + * @param taskID the id of the task returned by server + * @param callback the result callback with with two arguments: + * error: null or Error('message') + * content: the server answer that contains the list of results + */ + waitTask: function(taskID, callback) { + // wait minimum 100ms before retrying + var baseDelay = 100; + // wait maximum 5s before retrying + var maxDelay = 5000; + var loop = 0; + + // waitTask() must be handled differently from other methods, + // it's a recursive method using a timeout + var indexObj = this; + var client = indexObj.as; + + var promise = retryLoop(); + + function retryLoop() { + return client._jsonRequest({ + method: 'GET', + hostType: 'read', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID + }).then(function success(content) { + loop++; + var delay = baseDelay * loop * loop; + if (delay > maxDelay) { + delay = maxDelay; + } + + if (content.status !== 'published') { + return client._promise.delay(delay).then(retryLoop); + } + + return content; + }); + } + + if (!callback) { + return promise; + } + + promise.then(successCb, failureCb); + + function successCb(content) { + exitPromise(function exit() { + callback(null, content); + }, client._setTimeout || setTimeout); + } + + function failureCb(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } + }, + + /* + * This function deletes the index content. Settings and index specific API keys are kept untouched. + * + * @param callback (optional) the result callback called with two arguments + * error: null or Error('message') + * content: the settings object or the error message if a failure occured + */ + clearIndex: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + hostType: 'write', + callback: callback + }); + }, + /* + * Get settings of this index + * + * @param callback (optional) the result callback called with two arguments + * error: null or Error('message') + * content: the settings object or the error message if a failure occured + */ + getSettings: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + hostType: 'read', + callback: callback + }); + }, + + /* + * Set settings for this index + * + * @param settigns the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number + * of words to return (syntax is attributeName:nbWords). + * By default no snippet is computed. If set to null, no snippet is computed. + * - attributesToIndex: (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, + * but you should update it to get optimal results. + * This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, + * you want to store it and be able to + * retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) + * Matches in attributes at the beginning of + * the list will be considered more important than matches in attributes further down the list. + * In one attribute, matching text at the beginning of the attribute will be + * considered more important than text after, you can disable + * this behavior if you add your attribute inside `unordered(AttributeName)`, + * for example attributesToIndex: ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. + * If set to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. + * This feature is similar to the SQL "distinct" keyword: when enabled + * in query with the distinct=1 parameter, all hits containing a duplicate + * value for this attribute are removed from results. + * For example, if the chosen attribute is show_name and several hits have + * the same value for show_name, then only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreassing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by attributesToIndex, + * - exact: + * - if the user query contains one word: sort objects having an attribute + * that is exactly the query word before others. + * For example if you search for the "V" TV show, you want to find it + * with the "V" query and avoid to have all popular TV + * show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the + * number of words that matched exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes + * prefixed by asc (ascending order) or desc (descending order) operator. + * For example `"customRanking" => ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before + * the highlighted parts in the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after + * the highlighted parts in the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should + * be considered as optional when found in the query. + * @param callback (optional) the result callback called with two arguments + * error: null or Error('message') + * content: the server answer or the error message if a failure occured + */ + setSettings: function(settings, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', + hostType: 'write', + body: settings, + callback: callback + }); + }, + /* + * List all existing user keys associated to this index + * + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + listUserKeys: function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + hostType: 'read', + callback: callback + }); + }, + /* + * Get ACL of a user key associated to this index + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + getUserKeyACL: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'read', + callback: callback + }); + }, + /* + * Delete an existing user key associated to this index + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + */ + deleteUserKey: function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'write', + callback: callback + }); + }, + /* + * Add a new API key to this index + * + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will + * be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * index.addUserKey(['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#AddIndexKey|Algolia REST API Documentation} + */ + addUserKey: function(acls, params, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: index.addUserKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys', + body: postObj, + hostType: 'write', + callback: callback + }); + }, + + /** + * Add an existing user key associated to this index + * @deprecated use index.addUserKey() + */ + addUserKeyWithValidity: deprecate(function deprecatedAddUserKeyWithValidity(acls, params, callback) { + return this.addUserKey(acls, params, callback); + }, deprecatedMessage('index.addUserKeyWithValidity()', 'index.addUserKey()')), + + /** + * Update an existing API key of this index + * @param {string} key - The key to update + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will + * be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with user keys list + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * index.updateUserKey('APIKEY', ['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} + */ + updateUserKey: function(key, acls, params, callback) { + var isArray = __webpack_require__(27); + var usage = 'Usage: index.updateUserKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); + }, + + _search: function(params, callback) { + return this.as._jsonRequest({ + cache: this.cache, + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: {params: params}, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: {params: params} + }, + callback: callback + }); + }, + + as: null, + indexName: null, + typeAheadArgs: null, + typeAheadValueOption: null + }; + + function prepareHost(protocol) { + return function prepare(host) { + return protocol + '//' + host.toLowerCase(); + }; + } + + function notImplemented() { + var message = 'Not implemented in this environment.\n' + + 'If you feel this is a mistake, write to support@algolia.com'; + + throw new errors.AlgoliaSearchError(message); + } + + function deprecatedMessage(previousUsage, newUsage) { + var githubAnchorLink = previousUsage.toLowerCase() + .replace('.', '') + .replace('()', ''); + + return 'algoliasearch: `' + previousUsage + '` was replaced by `' + newUsage + + '`. Please see https://github.com/algolia/algoliasearch-client-js/wiki/Deprecated#' + githubAnchorLink; + } + + // Parse cloud does not supports setTimeout + // We do not store a setTimeout reference in the client everytime + // We only fallback to a fake setTimeout when not available + // setTimeout cannot be override globally sadly + function exitPromise(fn, _setTimeout) { + _setTimeout(fn, 0); + } + + function deprecate(fn, message) { + var warned = false; + + function deprecated() { + if (!warned) { + /* eslint no-console:0 */ + console.log(message); + warned = true; + } + + return fn.apply(this, arguments); + } + + return deprecated; + } + + // Prototype.js < 1.7, a widely used library, defines a weird + // Array.prototype.toJSON function that will fail to stringify our content + // appropriately + // refs: + // - https://groups.google.com/forum/#!topic/prototype-core/E-SAVvV_V9Q + // - https://github.com/sstephenson/prototype/commit/038a2985a70593c1a86c230fadbdfe2e4898a48c + // - http://stackoverflow.com/a/3148441/147079 + function safeJSONStringify(obj) { + /* eslint no-extend-native:0 */ + + if (Array.prototype.toJSON === undefined) { + return JSON.stringify(obj); + } + + var toJSON = Array.prototype.toJSON; + delete Array.prototype.toJSON; + var out = JSON.stringify(obj); + Array.prototype.toJSON = toJSON; + + return out; + } + + function buildSearchMethod(queryParam) { + return function search(query, args, callback) { + // warn V2 users on how to search + if (typeof query === 'function' && typeof args === 'object' || + typeof callback === 'object') { + // .search(query, params, cb) + // .search(cb, params) + throw new errors.AlgoliaSearchError('index.search usage is index.search(query, params, cb)'); + } + + if (arguments.length === 0 || typeof query === 'function') { + // .search(), .search(cb) + callback = query; + query = ''; + } else if (arguments.length === 1 || typeof args === 'function') { + // .search(query/args), .search(query, cb) + callback = args; + args = undefined; + } + + // .search(args), careful: typeof null === 'object' + if (typeof query === 'object' && query !== null) { + args = query; + query = undefined; + } else if (query === undefined || query === null) { // .search(undefined/null) + query = ''; + } + + var params = ''; + + if (query !== undefined) { + params += queryParam + '=' + encodeURIComponent(query); + } + + if (args !== undefined) { + // `_getSearchParams` will augment params, do not be fooled by the = versus += from previous if + params = this.as._getSearchParams(args, params); + } + + return this._search(params, callback); + }; + } + + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + + + // This file hosts our error definitions + // We use custom error "types" so that we can act on them when we need it + // e.g.: if error instanceof errors.UnparsableJSON then.. + + var inherits = __webpack_require__(4); + + function AlgoliaSearchError(message, extraProperties) { + var forEach = __webpack_require__(8); + + var error = this; + + // try to get a stacktrace + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + error.stack = (new Error()).stack || 'Cannot get a stacktrace, browser is too old'; + } + + this.name = this.constructor.name; + this.message = message || 'Unknown error'; + + if (extraProperties) { + forEach(extraProperties, function addToErrorObject(value, key) { + error[key] = value; + }); + } + } + + inherits(AlgoliaSearchError, Error); + + function createCustomError(name, message) { + function AlgoliaSearchCustomError() { + var args = Array.prototype.slice.call(arguments, 0); + + // custom message not set, use default + if (typeof args[0] !== 'string') { + args.unshift(message); + } + + AlgoliaSearchError.apply(this, args); + this.name = 'AlgoliaSearch' + name + 'Error'; + } + + inherits(AlgoliaSearchCustomError, AlgoliaSearchError); + + return AlgoliaSearchCustomError; + } + + // late exports to let various fn defs and inherits take place + module.exports = { + AlgoliaSearchError: AlgoliaSearchError, + UnparsableJSON: createCustomError( + 'UnparsableJSON', + 'Could not parse the incoming response as JSON, see err.more for details' + ), + RequestTimeout: createCustomError( + 'RequestTimeout', + 'Request timedout before getting a response' + ), + Network: createCustomError( + 'Network', + 'Network issue, see err.more for details' + ), + JSONPScriptFail: createCustomError( + 'JSONPScriptFail', + ' - - - - - + + + + + +