yc-algoliasearch-rails 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.MD +566 -0
  5. data/Gemfile +38 -0
  6. data/Gemfile.lock +213 -0
  7. data/LICENSE +21 -0
  8. data/README.md +1171 -0
  9. data/Rakefile +17 -0
  10. data/algoliasearch-rails.gemspec +95 -0
  11. data/lib/algoliasearch/algolia_job.rb +9 -0
  12. data/lib/algoliasearch/configuration.rb +30 -0
  13. data/lib/algoliasearch/pagination/kaminari.rb +40 -0
  14. data/lib/algoliasearch/pagination/will_paginate.rb +15 -0
  15. data/lib/algoliasearch/pagination.rb +19 -0
  16. data/lib/algoliasearch/railtie.rb +11 -0
  17. data/lib/algoliasearch/tasks/algoliasearch.rake +19 -0
  18. data/lib/algoliasearch/utilities.rb +48 -0
  19. data/lib/algoliasearch/version.rb +3 -0
  20. data/lib/algoliasearch-rails.rb +1083 -0
  21. data/spec/spec_helper.rb +52 -0
  22. data/spec/utilities_spec.rb +30 -0
  23. data/vendor/assets/javascripts/algolia/algoliasearch.angular.js +2678 -0
  24. data/vendor/assets/javascripts/algolia/algoliasearch.angular.min.js +7 -0
  25. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.js +2678 -0
  26. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js +7 -0
  27. data/vendor/assets/javascripts/algolia/algoliasearch.js +2663 -0
  28. data/vendor/assets/javascripts/algolia/algoliasearch.min.js +7 -0
  29. data/vendor/assets/javascripts/algolia/bloodhound.js +727 -0
  30. data/vendor/assets/javascripts/algolia/bloodhound.min.js +7 -0
  31. data/vendor/assets/javascripts/algolia/typeahead.bundle.js +1782 -0
  32. data/vendor/assets/javascripts/algolia/typeahead.bundle.min.js +7 -0
  33. data/vendor/assets/javascripts/algolia/typeahead.jquery.js +1184 -0
  34. data/vendor/assets/javascripts/algolia/typeahead.jquery.min.js +7 -0
  35. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js +2678 -0
  36. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js +7 -0
  37. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js +2678 -0
  38. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js +7 -0
  39. data/vendor/assets/javascripts/algolia/v2/algoliasearch.js +2663 -0
  40. data/vendor/assets/javascripts/algolia/v2/algoliasearch.min.js +7 -0
  41. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js +6277 -0
  42. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js +3 -0
  43. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js +6223 -0
  44. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js +3 -0
  45. data/vendor/assets/javascripts/algolia/v3/algoliasearch.js +6070 -0
  46. data/vendor/assets/javascripts/algolia/v3/algoliasearch.min.js +3 -0
  47. metadata +174 -0
@@ -0,0 +1,2678 @@
1
+ /*
2
+ * Copyright (c) 2013 Algolia
3
+ * http://www.algolia.com/
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in
13
+ * all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ * THE SOFTWARE.
22
+ */
23
+
24
+ var ALGOLIA_VERSION = '2.9.7';
25
+
26
+ /*
27
+ * Copyright (c) 2013 Algolia
28
+ * http://www.algolia.com/
29
+ *
30
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ * of this software and associated documentation files (the "Software"), to deal
32
+ * in the Software without restriction, including without limitation the rights
33
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ * copies of the Software, and to permit persons to whom the Software is
35
+ * furnished to do so, subject to the following conditions:
36
+ *
37
+ * The above copyright notice and this permission notice shall be included in
38
+ * all copies or substantial portions of the Software.
39
+ *
40
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46
+ * THE SOFTWARE.
47
+ */
48
+
49
+ /*
50
+ * Algolia Search library initialization
51
+ * @param applicationID the application ID you have in your admin interface
52
+ * @param apiKey a valid API key for the service
53
+ * @param methodOrOptions the hash of parameters for initialization. It can contains:
54
+ * - method (optional) specify if the protocol used is http or https (http by default to make the first search query faster).
55
+ * You need to use https is you are doing something else than just search queries.
56
+ * - hosts (optional) the list of hosts that you have received for the service
57
+ * - dsn (optional) set to true if your account has the Distributed Search Option
58
+ * - dsnHost (optional) override the automatic computation of dsn hostname
59
+ */
60
+ var AlgoliaSearch = function(applicationID, apiKey, methodOrOptions, resolveDNS, hosts) {
61
+ var self = this;
62
+ this.applicationID = applicationID;
63
+ this.apiKey = apiKey;
64
+ this.dsn = true;
65
+ this.dsnHost = null;
66
+ this.hosts = [];
67
+ this.currentHostIndex = 0;
68
+ this.requestTimeoutInMs = 2000;
69
+ this.extraHeaders = [];
70
+ this.jsonp = null;
71
+ this.options = {};
72
+
73
+ // make sure every client instance has it's own cache
74
+ this.cache = {};
75
+
76
+ var method;
77
+ var tld = 'net';
78
+ if (typeof methodOrOptions === 'string') { // Old initialization
79
+ method = methodOrOptions;
80
+ } else {
81
+ // Take all option from the hash
82
+ var options = methodOrOptions || {};
83
+ this.options = options;
84
+ if (!this._isUndefined(options.method)) {
85
+ method = options.method;
86
+ }
87
+ if (!this._isUndefined(options.tld)) {
88
+ tld = options.tld;
89
+ }
90
+ if (!this._isUndefined(options.dsn)) {
91
+ this.dsn = options.dsn;
92
+ }
93
+ if (!this._isUndefined(options.hosts)) {
94
+ hosts = options.hosts;
95
+ }
96
+ if (!this._isUndefined(options.dsnHost)) {
97
+ this.dsnHost = options.dsnHost;
98
+ }
99
+ if (!this._isUndefined(options.requestTimeoutInMs)) {
100
+ this.requestTimeoutInMs = +options.requestTimeoutInMs;
101
+ }
102
+ if (!this._isUndefined(options.jsonp)) {
103
+ this.jsonp = options.jsonp;
104
+ }
105
+ }
106
+ // If hosts is undefined, initialize it with applicationID
107
+ if (this._isUndefined(hosts)) {
108
+ hosts = [
109
+ this.applicationID + '-1.algolianet.com',
110
+ this.applicationID + '-2.algolianet.com',
111
+ this.applicationID + '-3.algolianet.com'
112
+ ];
113
+ }
114
+ // detect is we use http or https
115
+ this.host_protocol = 'http://';
116
+ if (this._isUndefined(method) || method === null) {
117
+ this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://';
118
+ } else if (method === 'https' || method === 'HTTPS') {
119
+ this.host_protocol = 'https://';
120
+ }
121
+ // Add protocol to hosts
122
+ for (var i = 0; i < hosts.length; ++i) {
123
+ this.hosts.push(this.host_protocol + hosts[i]);
124
+ }
125
+ // then add Distributed Search Network host if there is one
126
+ if (this.dsn || this.dsnHost != null) {
127
+ if (this.dsnHost) {
128
+ this.hosts.unshift(this.host_protocol + this.dsnHost);
129
+ } else {
130
+ this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld);
131
+ }
132
+ }
133
+ // angular dependencies injection
134
+ if (this.options.angular) {
135
+ this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) {
136
+ self.options.angular.$q = $q;
137
+ self.options.angular.$http = $http;
138
+ }]);
139
+ }
140
+
141
+ this._ua = this.options._ua || 'Algolia for vanilla JavaScript ' + window.ALGOLIA_VERSION;
142
+ };
143
+
144
+ // This holds the number of JSONP requests done accross clients
145
+ // It's used as part of the ?callback=JSONP_$JSONPCounter when we do JSONP requests
146
+ AlgoliaSearch.JSONPCounter = 0;
147
+
148
+ function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) {
149
+
150
+ function _getHitExplanationForOneAttr_recurse(obj, foundWords) {
151
+ var res = [];
152
+ if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) {
153
+ var match = false;
154
+ for (var j = 0; j < obj.matchedWords.length; ++j) {
155
+ var word = obj.matchedWords[j];
156
+ if (!(word in foundWords)) {
157
+ foundWords[word] = 1;
158
+ match = true;
159
+ }
160
+ }
161
+ if (match) {
162
+ res.push(obj.value);
163
+ }
164
+ } else if (Object.prototype.toString.call(obj) === '[object Array]') {
165
+ for (var i = 0; i < obj.length; ++i) {
166
+ var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords);
167
+ res = res.concat(array);
168
+ }
169
+ } else if (typeof obj === 'object') {
170
+ for (var prop in obj) {
171
+ if (obj.hasOwnProperty(prop)){
172
+ res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords));
173
+ }
174
+ }
175
+ }
176
+ return res;
177
+ }
178
+
179
+ function _getHitExplanationForOneAttr(hit, foundWords, attr) {
180
+ var base = hit._highlightResult || hit;
181
+ if (attr.indexOf('.') === -1) {
182
+ if (attr in base) {
183
+ return _getHitExplanationForOneAttr_recurse(base[attr], foundWords);
184
+ }
185
+ return [];
186
+ }
187
+ var array = attr.split('.');
188
+ var obj = base;
189
+ for (var i = 0; i < array.length; ++i) {
190
+ if (Object.prototype.toString.call(obj) === '[object Array]') {
191
+ var res = [];
192
+ for (var j = 0; j < obj.length; ++j) {
193
+ res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.')));
194
+ }
195
+ return res;
196
+ }
197
+ if (array[i] in obj) {
198
+ obj = obj[array[i]];
199
+ } else {
200
+ return [];
201
+ }
202
+ }
203
+ return _getHitExplanationForOneAttr_recurse(obj, foundWords);
204
+ }
205
+
206
+ var res = {};
207
+ var foundWords = {};
208
+ var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute);
209
+ res.title = (title.length > 0) ? title[0] : '';
210
+ res.subtitles = [];
211
+
212
+ if (typeof otherAttributes !== 'undefined') {
213
+ for (var i = 0; i < otherAttributes.length; ++i) {
214
+ var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]);
215
+ for (var j = 0; j < attr.length; ++j) {
216
+ res.subtitles.push({ attr: otherAttributes[i], value: attr[j] });
217
+ }
218
+ }
219
+ }
220
+ return res;
221
+ }
222
+
223
+
224
+ AlgoliaSearch.prototype = {
225
+ /*
226
+ * Delete an index
227
+ *
228
+ * @param indexName the name of index to delete
229
+ * @param callback the result callback with two arguments
230
+ * success: boolean set to true if the request was successfull
231
+ * content: the server answer that contains the task ID
232
+ */
233
+ deleteIndex: function(indexName, callback) {
234
+ return this._jsonRequest({ method: 'DELETE',
235
+ url: '/1/indexes/' + encodeURIComponent(indexName),
236
+ callback: callback });
237
+ },
238
+ /**
239
+ * Move an existing index.
240
+ * @param srcIndexName the name of index to copy.
241
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
242
+ * @param callback the result callback with two arguments
243
+ * success: boolean set to true if the request was successfull
244
+ * content: the server answer that contains the task ID
245
+ */
246
+ moveIndex: function(srcIndexName, dstIndexName, callback) {
247
+ var postObj = {operation: 'move', destination: dstIndexName};
248
+ return this._jsonRequest({ method: 'POST',
249
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
250
+ body: postObj,
251
+ callback: callback });
252
+
253
+ },
254
+ /**
255
+ * Copy an existing index.
256
+ * @param srcIndexName the name of index to copy.
257
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
258
+ * @param callback the result callback with two arguments
259
+ * success: boolean set to true if the request was successfull
260
+ * content: the server answer that contains the task ID
261
+ */
262
+ copyIndex: function(srcIndexName, dstIndexName, callback) {
263
+ var postObj = {operation: 'copy', destination: dstIndexName};
264
+ return this._jsonRequest({ method: 'POST',
265
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
266
+ body: postObj,
267
+ callback: callback });
268
+ },
269
+ /**
270
+ * Return last log entries.
271
+ * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
272
+ * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
273
+ * @param callback the result callback with two arguments
274
+ * success: boolean set to true if the request was successfull
275
+ * content: the server answer that contains the task ID
276
+ */
277
+ getLogs: function(callback, offset, length) {
278
+ if (this._isUndefined(offset)) {
279
+ offset = 0;
280
+ }
281
+ if (this._isUndefined(length)) {
282
+ length = 10;
283
+ }
284
+
285
+ return this._jsonRequest({ method: 'GET',
286
+ url: '/1/logs?offset=' + offset + '&length=' + length,
287
+ callback: callback });
288
+ },
289
+ /*
290
+ * List all existing indexes (paginated)
291
+ *
292
+ * @param callback the result callback with two arguments
293
+ * success: boolean set to true if the request was successfull
294
+ * content: the server answer with index list or error description if success is false.
295
+ * @param page The page to retrieve, starting at 0.
296
+ */
297
+ listIndexes: function(callback, page) {
298
+ var params = typeof page !== 'undefined' ? '?page=' + page : '';
299
+ return this._jsonRequest({ method: 'GET',
300
+ url: '/1/indexes' + params,
301
+ callback: callback });
302
+ },
303
+
304
+ /*
305
+ * Get the index object initialized
306
+ *
307
+ * @param indexName the name of index
308
+ * @param callback the result callback with one argument (the Index instance)
309
+ */
310
+ initIndex: function(indexName) {
311
+ return new this.Index(this, indexName);
312
+ },
313
+ /*
314
+ * List all existing user keys with their associated ACLs
315
+ *
316
+ * @param callback the result callback with two arguments
317
+ * success: boolean set to true if the request was successfull
318
+ * content: the server answer with user keys list or error description if success is false.
319
+ */
320
+ listUserKeys: function(callback) {
321
+ return this._jsonRequest({ method: 'GET',
322
+ url: '/1/keys',
323
+ callback: callback });
324
+ },
325
+ /*
326
+ * Get ACL of a user key
327
+ *
328
+ * @param callback the result callback with two arguments
329
+ * success: boolean set to true if the request was successfull
330
+ * content: the server answer with user keys list or error description if success is false.
331
+ */
332
+ getUserKeyACL: function(key, callback) {
333
+ return this._jsonRequest({ method: 'GET',
334
+ url: '/1/keys/' + key,
335
+ callback: callback });
336
+ },
337
+ /*
338
+ * Delete an existing user key
339
+ *
340
+ * @param callback the result callback with two arguments
341
+ * success: boolean set to true if the request was successfull
342
+ * content: the server answer with user keys list or error description if success is false.
343
+ */
344
+ deleteUserKey: function(key, callback) {
345
+ return this._jsonRequest({ method: 'DELETE',
346
+ url: '/1/keys/' + key,
347
+ callback: callback });
348
+ },
349
+ /*
350
+ * Add an existing user key
351
+ *
352
+ * @param acls the list of ACL for this key. Defined by an array of strings that
353
+ * can contains the following values:
354
+ * - search: allow to search (https and http)
355
+ * - addObject: allows to add/update an object in the index (https only)
356
+ * - deleteObject : allows to delete an existing object (https only)
357
+ * - deleteIndex : allows to delete index content (https only)
358
+ * - settings : allows to get index settings (https only)
359
+ * - editSettings : allows to change index settings (https only)
360
+ * @param callback the result callback with two arguments
361
+ * success: boolean set to true if the request was successfull
362
+ * content: the server answer with user keys list or error description if success is false.
363
+ */
364
+ addUserKey: function(acls, callback) {
365
+ return this.addUserKeyWithValidity(acls, 0, 0, 0, callback);
366
+ },
367
+ /*
368
+ * Add an existing user key
369
+ *
370
+ * @param acls the list of ACL for this key. Defined by an array of strings that
371
+ * can contains the following values:
372
+ * - search: allow to search (https and http)
373
+ * - addObject: allows to add/update an object in the index (https only)
374
+ * - deleteObject : allows to delete an existing object (https only)
375
+ * - deleteIndex : allows to delete index content (https only)
376
+ * - settings : allows to get index settings (https only)
377
+ * - editSettings : allows to change index settings (https only)
378
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
379
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
380
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
381
+ * @param callback the result callback with two arguments
382
+ * success: boolean set to true if the request was successfull
383
+ * content: the server answer with user keys list or error description if success is false.
384
+ */
385
+ addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
386
+ var aclsObject = {};
387
+ aclsObject.acl = acls;
388
+ aclsObject.validity = validity;
389
+ aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
390
+ aclsObject.maxHitsPerQuery = maxHitsPerQuery;
391
+ return this._jsonRequest({ method: 'POST',
392
+ url: '/1/keys',
393
+ body: aclsObject,
394
+ callback: callback });
395
+ },
396
+
397
+ /**
398
+ * Set the extra security tagFilters header
399
+ * @param {string|array} tags The list of tags defining the current security filters
400
+ */
401
+ setSecurityTags: function(tags) {
402
+ if (Object.prototype.toString.call(tags) === '[object Array]') {
403
+ var strTags = [];
404
+ for (var i = 0; i < tags.length; ++i) {
405
+ if (Object.prototype.toString.call(tags[i]) === '[object Array]') {
406
+ var oredTags = [];
407
+ for (var j = 0; j < tags[i].length; ++j) {
408
+ oredTags.push(tags[i][j]);
409
+ }
410
+ strTags.push('(' + oredTags.join(',') + ')');
411
+ } else {
412
+ strTags.push(tags[i]);
413
+ }
414
+ }
415
+ tags = strTags.join(',');
416
+ }
417
+ this.tagFilters = tags;
418
+ },
419
+
420
+ /**
421
+ * Set the extra user token header
422
+ * @param {string} userToken The token identifying a uniq user (used to apply rate limits)
423
+ */
424
+ setUserToken: function(userToken) {
425
+ this.userToken = userToken;
426
+ },
427
+
428
+ /*
429
+ * Initialize a new batch of search queries
430
+ */
431
+ startQueriesBatch: function() {
432
+ this.batch = [];
433
+ },
434
+ /*
435
+ * Add a search query in the batch
436
+ *
437
+ * @param query the full text query
438
+ * @param args (optional) if set, contains an object with query parameters:
439
+ * - attributes: an array of object attribute names to retrieve
440
+ * (if not set all attributes are retrieve)
441
+ * - attributesToHighlight: an array of object attribute names to highlight
442
+ * (if not set indexed attributes are highlighted)
443
+ * - minWordSizefor1Typo: the minimum number of characters to accept one typo.
444
+ * Defaults to 3.
445
+ * - minWordSizefor2Typos: the minimum number of characters to accept two typos.
446
+ * Defaults to 7.
447
+ * - getRankingInfo: if set, the result hits will contain ranking information in
448
+ * _rankingInfo attribute
449
+ * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
450
+ * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
451
+ */
452
+ addQueryInBatch: function(indexName, query, args) {
453
+ var params = 'query=' + encodeURIComponent(query);
454
+ if (!this._isUndefined(args) && args !== null) {
455
+ params = this._getSearchParams(args, params);
456
+ }
457
+ this.batch.push({ indexName: indexName, params: params });
458
+ },
459
+ /*
460
+ * Clear all queries in cache
461
+ */
462
+ clearCache: function() {
463
+ this.cache = {};
464
+ },
465
+ /*
466
+ * Launch the batch of queries using XMLHttpRequest.
467
+ * (Optimized for browser using a POST query to minimize number of OPTIONS queries)
468
+ *
469
+ * @param callback the function that will receive results
470
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime.
471
+ */
472
+ sendQueriesBatch: function(callback, delay) {
473
+ var as = this;
474
+ var params = {requests: []};
475
+ for (var i = 0; i < as.batch.length; ++i) {
476
+ params.requests.push(as.batch[i]);
477
+ }
478
+ window.clearTimeout(as.onDelayTrigger);
479
+ if (!this._isUndefined(delay) && delay !== null && delay > 0) {
480
+ var onDelayTrigger = window.setTimeout( function() {
481
+ as._sendQueriesBatch(params, callback);
482
+ }, delay);
483
+ as.onDelayTrigger = onDelayTrigger;
484
+ } else {
485
+ return this._sendQueriesBatch(params, callback);
486
+ }
487
+ },
488
+
489
+ /**
490
+ * Set the number of milliseconds a request can take before automatically being terminated.
491
+ *
492
+ * @param {Number} milliseconds
493
+ */
494
+ setRequestTimeout: function(milliseconds)
495
+ {
496
+ if (milliseconds) {
497
+ this.requestTimeoutInMs = parseInt(milliseconds, 10);
498
+ }
499
+ },
500
+
501
+ /*
502
+ * Index class constructor.
503
+ * You should not use this method directly but use initIndex() function
504
+ */
505
+ Index: function(algoliasearch, indexName) {
506
+ this.indexName = indexName;
507
+ this.as = algoliasearch;
508
+ this.typeAheadArgs = null;
509
+ this.typeAheadValueOption = null;
510
+
511
+ // make sure every index instance has it's own cache
512
+ this.cache = {};
513
+ },
514
+ /**
515
+ * Add an extra field to the HTTP request
516
+ *
517
+ * @param key the header field name
518
+ * @param value the header field value
519
+ */
520
+ setExtraHeader: function(key, value) {
521
+ this.extraHeaders.push({ key: key, value: value});
522
+ },
523
+
524
+ _sendQueriesBatch: function(params, callback) {
525
+ if (this.jsonp === null) {
526
+ var self = this;
527
+ return this._jsonRequest({ cache: this.cache,
528
+ method: 'POST',
529
+ url: '/1/indexes/*/queries',
530
+ body: params,
531
+ callback: function(success, content) {
532
+ if (!success) {
533
+ // retry first with JSONP
534
+ self.jsonp = true;
535
+ self._sendQueriesBatch(params, callback);
536
+ } else {
537
+ self.jsonp = false;
538
+ callback && callback(success, content);
539
+ }
540
+ }
541
+ });
542
+ } else if (this.jsonp) {
543
+ var jsonpParams = '';
544
+ for (var i = 0; i < params.requests.length; ++i) {
545
+ var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params;
546
+ jsonpParams += i + '=' + encodeURIComponent(q) + '&';
547
+ }
548
+ var pObj = {params: jsonpParams};
549
+ return this._jsonRequest({ cache: this.cache,
550
+ method: 'GET',
551
+ url: '/1/indexes/*',
552
+ body: pObj,
553
+ callback: callback });
554
+ } else {
555
+ return this._jsonRequest({ cache: this.cache,
556
+ method: 'POST',
557
+ url: '/1/indexes/*/queries',
558
+ body: params,
559
+ callback: callback});
560
+ }
561
+ },
562
+ /*
563
+ * Wrapper that try all hosts to maximize the quality of service
564
+ */
565
+ _jsonRequest: function(opts) {
566
+ var self = this;
567
+ var callback = opts.callback;
568
+ var cache = null;
569
+ var cacheID = opts.url;
570
+ var deferred = null;
571
+ if (this.options.jQuery) {
572
+ deferred = this.options.jQuery.$.Deferred();
573
+ deferred.promise = deferred.promise(); // promise is a property in angular
574
+ } else if (this.options.angular) {
575
+ deferred = this.options.angular.$q.defer();
576
+ }
577
+
578
+ if (!this._isUndefined(opts.body)) {
579
+ cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
580
+ }
581
+ if (!this._isUndefined(opts.cache)) {
582
+ cache = opts.cache;
583
+ if (!this._isUndefined(cache[cacheID])) {
584
+ if (!this._isUndefined(callback) && callback) {
585
+ setTimeout(function () { callback(true, cache[cacheID]); }, 1);
586
+ }
587
+ deferred && deferred.resolve(cache[cacheID]);
588
+ return deferred && deferred.promise;
589
+ }
590
+ }
591
+
592
+ opts.successiveRetryCount = 0;
593
+ var impl = function() {
594
+
595
+ if (opts.successiveRetryCount >= self.hosts.length) {
596
+ var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' };
597
+ if (!self._isUndefined(callback) && callback) {
598
+ opts.successiveRetryCount = 0;
599
+ callback(false, error);
600
+ }
601
+ deferred && deferred.reject(error);
602
+ return;
603
+ }
604
+ opts.callback = function(retry, success, body) {
605
+ if (success && !self._isUndefined(opts.cache)) {
606
+ cache[cacheID] = body;
607
+ }
608
+ if (!success && retry) {
609
+ self.currentHostIndex = ++self.currentHostIndex % self.hosts.length;
610
+ opts.successiveRetryCount += 1;
611
+ impl();
612
+ } else {
613
+ opts.successiveRetryCount = 0;
614
+ deferred && (success ? deferred.resolve(body) : deferred.reject(body));
615
+ if (!self._isUndefined(callback) && callback) {
616
+ callback(success, body);
617
+ }
618
+ }
619
+ };
620
+ opts.hostname = self.hosts[self.currentHostIndex];
621
+ self._jsonRequestByHost(opts);
622
+ };
623
+ impl();
624
+
625
+ return deferred && deferred.promise;
626
+ },
627
+
628
+ _jsonRequestByHost: function(opts) {
629
+ var self = this;
630
+ var url = opts.hostname + opts.url;
631
+
632
+ if (this.jsonp) {
633
+ this._makeJsonpRequestByHost(url, opts);
634
+ } else if (this.options.jQuery) {
635
+ this._makejQueryRequestByHost(url, opts);
636
+ } else if (this.options.angular) {
637
+ this._makeAngularRequestByHost(url, opts);
638
+ } else {
639
+ this._makeXmlHttpRequestByHost(url, opts);
640
+ }
641
+ },
642
+
643
+ /**
644
+ * Make a $http
645
+ *
646
+ * @param url request url (includes endpoint and path)
647
+ * @param opts all request opts
648
+ */
649
+ _makeAngularRequestByHost: function(url, opts) {
650
+ var self = this;
651
+ var body = null;
652
+
653
+ if (!this._isUndefined(opts.body)) {
654
+ body = JSON.stringify(opts.body);
655
+ }
656
+
657
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
658
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
659
+ if (this.userToken) {
660
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
661
+ }
662
+ if (this.tagFilters) {
663
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
664
+ }
665
+ url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua);
666
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
667
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
668
+ }
669
+ this.options.angular.$http({
670
+ url: url,
671
+ method: opts.method,
672
+ data: body,
673
+ cache: false,
674
+ timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1))
675
+ }).then(function(response) {
676
+ opts.callback(false, true, response.data);
677
+ }, function(response) {
678
+ if (response.status === 0) {
679
+ // xhr.timeout is not handled by Angular.js right now
680
+ // let's retry
681
+ opts.callback(true, false, response.data);
682
+ } else if (response.status == 400 || response.status === 403 || response.status === 404) {
683
+ opts.callback(false, false, response.data);
684
+ } else {
685
+ opts.callback(true, false, response.data);
686
+ }
687
+ });
688
+ },
689
+
690
+ /**
691
+ * Make a $.ajax
692
+ *
693
+ * @param url request url (includes endpoint and path)
694
+ * @param opts all request opts
695
+ */
696
+ _makejQueryRequestByHost: function(url, opts) {
697
+ var self = this;
698
+ var body = null;
699
+
700
+ if (!this._isUndefined(opts.body)) {
701
+ body = JSON.stringify(opts.body);
702
+ }
703
+
704
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
705
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
706
+ if (this.userToken) {
707
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
708
+ }
709
+ if (this.tagFilters) {
710
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
711
+ }
712
+ url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua);
713
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
714
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
715
+ }
716
+ this.options.jQuery.$.ajax(url, {
717
+ type: opts.method,
718
+ timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)),
719
+ dataType: 'json',
720
+ data: body,
721
+ error: function(xhr, textStatus, error) {
722
+ if (textStatus === 'timeout') {
723
+ opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
724
+ } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) {
725
+ opts.callback(false, false, xhr.responseJSON );
726
+ } else {
727
+ opts.callback(true, false, { 'message': error } );
728
+ }
729
+ },
730
+ success: function(data, textStatus, xhr) {
731
+ opts.callback(false, true, data);
732
+ }
733
+ });
734
+ },
735
+
736
+ /**
737
+ * Make a JSONP request
738
+ *
739
+ * @param url request url (includes endpoint and path)
740
+ * @param opts all request options
741
+ */
742
+ _makeJsonpRequestByHost: function(url, opts) {
743
+ if (opts.method !== 'GET') {
744
+ opts.callback(true, false, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' });
745
+ return;
746
+ }
747
+
748
+ var cbCalled = false;
749
+ var timedOut = false;
750
+
751
+ AlgoliaSearch.JSONPCounter += 1;
752
+ var head = document.getElementsByTagName('head')[0];
753
+ var script = document.createElement('script');
754
+ var cb = 'algoliaJSONP_' + AlgoliaSearch.JSONPCounter;
755
+ var done = false;
756
+ var ontimeout;
757
+ var success;
758
+ var clean;
759
+
760
+ window[cb] = function(data) {
761
+ try { delete window[cb]; } catch (e) { window[cb] = undefined; }
762
+
763
+ if (timedOut) {
764
+ return;
765
+ }
766
+
767
+ var status =
768
+ data && data.message && data.status ||
769
+ data && 200;
770
+
771
+ var ok = status === 200;
772
+ var retry = !ok && status !== 400 && status !== 403 && status !== 404;
773
+ cbCalled = true;
774
+ opts.callback(retry, ok, data);
775
+ };
776
+
777
+ script.type = 'text/javascript';
778
+ url += '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey;
779
+
780
+ if (this.tagFilters) {
781
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
782
+ }
783
+
784
+ if (this.userToken) {
785
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
786
+ }
787
+
788
+ url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua);
789
+
790
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
791
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
792
+ }
793
+
794
+ if (opts.body && opts.body.params) {
795
+ url += '&' + opts.body.params;
796
+ }
797
+
798
+ ontimeout = setTimeout(function() {
799
+ timedOut = true;
800
+ clean();
801
+
802
+ opts.callback(true, false, { 'message': 'Timeout - Failed to load JSONP script.' });
803
+ }, this.requestTimeoutInMs * (opts.successiveRetryCount + 1));
804
+
805
+ success = function() {
806
+ if (done || timedOut) {
807
+ return;
808
+ }
809
+
810
+ done = true;
811
+ clean();
812
+
813
+ // script loaded but did not call the fn => script loading error
814
+ if (!cbCalled) {
815
+ opts.callback(true, false, { 'message': 'Failed to load JSONP script.' });
816
+ }
817
+ };
818
+
819
+ clean = function() {
820
+ clearTimeout(ontimeout);
821
+ script.onload = null;
822
+ script.onreadystatechange = null;
823
+ script.onerror = null;
824
+ head.removeChild(script);
825
+
826
+ try {
827
+ delete window[cb];
828
+ delete window[cb + '_loaded'];
829
+ } catch (e) {
830
+ window[cb] = null;
831
+ window[cb + '_loaded'] = null;
832
+ }
833
+ };
834
+
835
+ // script onreadystatechange needed only for
836
+ // <= IE8
837
+ // https://github.com/angular/angular.js/issues/4523
838
+ script.onreadystatechange = function() {
839
+ if (this.readyState === 'loaded' || this.readyState === 'complete') {
840
+ success();
841
+ }
842
+ };
843
+
844
+ script.onload = function() {
845
+ success();
846
+ };
847
+
848
+ script.onerror = function() {
849
+ if (done || timedOut) {
850
+ return;
851
+ }
852
+
853
+ clean();
854
+ opts.callback(true, false, { 'message': 'Failed to load JSONP script.' });
855
+ };
856
+
857
+ script.async = true;
858
+ script.defer = true;
859
+ script.src = url;
860
+
861
+ head.appendChild(script);
862
+ },
863
+
864
+ /**
865
+ * Make a XmlHttpRequest
866
+ *
867
+ * @param url request url (includes endpoint and path)
868
+ * @param opts all request opts
869
+ */
870
+ _makeXmlHttpRequestByHost: function(url, opts) {
871
+ // no cors or XDomainRequest, no request
872
+ if (!this._support.cors && !this._support.hasXDomainRequest) {
873
+ // very old browser, not supported
874
+ opts.callback(false, false, { 'message': 'CORS not supported' });
875
+ return;
876
+ }
877
+
878
+ var body = null;
879
+ var request = this._support.cors ? new XMLHttpRequest() : new XDomainRequest();
880
+ var ontimeout;
881
+ var self = this;
882
+ var timedOut;
883
+ var timeoutListener;
884
+
885
+ if (!this._isUndefined(opts.body)) {
886
+ body = JSON.stringify(opts.body);
887
+ }
888
+
889
+ url += (url.indexOf('?') === -1 ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
890
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
891
+
892
+ if (this.userToken) {
893
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
894
+ }
895
+
896
+ if (this.tagFilters) {
897
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
898
+ }
899
+
900
+ url += '&X-Algolia-Agent=' + encodeURIComponent(this._ua);
901
+
902
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
903
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
904
+ }
905
+
906
+ timeoutListener = function() {
907
+ if (!self._support.timeout) {
908
+ timedOut = true;
909
+ request.abort();
910
+ }
911
+
912
+ opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
913
+ };
914
+
915
+ // do not rely on default XHR async flag, as some analytics code like hotjar
916
+ // breaks it and set it to false by default
917
+ if (request instanceof XMLHttpRequest) {
918
+ request.open(opts.method, url, true);
919
+ } else {
920
+ request.open(opts.method, url);
921
+ }
922
+
923
+ if (this._support.cors && body !== null && opts.method !== 'GET') {
924
+ request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
925
+ }
926
+
927
+ // event object not received in IE8, at least
928
+ // but we do not use it, still important to note
929
+ request.onload = function(/*event*/) {
930
+ // When browser does not supports request.timeout, we can
931
+ // have both a load and timeout event
932
+ if (timedOut) {
933
+ return;
934
+ }
935
+
936
+ if (!self._support.timeout) {
937
+ clearTimeout(ontimeout);
938
+ }
939
+
940
+ var response = null;
941
+
942
+ try {
943
+ response = JSON.parse(request.responseText);
944
+ } catch(e) {}
945
+
946
+ var status =
947
+ // XHR provides a `status` property
948
+ request.status ||
949
+
950
+ // XDR does not have a `status` property,
951
+ // we rely on our own API response `status`, only
952
+ // provided when an error occurs, so we expect a .message
953
+ response && response.message && response.status ||
954
+
955
+ // XDR default to success when no response.status
956
+ response && 200;
957
+
958
+ var success = status === 200 || status === 201;
959
+ var retry = !success && status !== 400 && status !== 403 && status !== 404;
960
+
961
+ opts.callback(retry, success, response);
962
+ };
963
+
964
+ // we set an empty onprogress listener
965
+ // so that XDomainRequest on IE9 is not aborted
966
+ // refs:
967
+ // - https://github.com/algolia/algoliasearch-client-js/issues/76
968
+ // - 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
969
+ request.onprogress = function noop() {};
970
+
971
+ if (this._support.timeout) {
972
+ // .timeout supported by both XHR and XDR,
973
+ // we do receive timeout event, tested
974
+ request.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1);
975
+
976
+ request.ontimeout = timeoutListener;
977
+ } else {
978
+ ontimeout = setTimeout(timeoutListener, this.requestTimeoutInMs * (opts.successiveRetryCount + 1));
979
+ }
980
+
981
+ request.onerror = function(event) {
982
+ if (timedOut) {
983
+ return;
984
+ }
985
+
986
+ if (!self._support.timeout) {
987
+ clearTimeout(ontimeout);
988
+ }
989
+
990
+ // error event is trigerred both with XDR/XHR on:
991
+ // - DNS error
992
+ // - unallowed cross domain request
993
+ opts.callback(true, false, { 'message': 'Could not connect to host', 'error': event } );
994
+ };
995
+
996
+ request.send(body);
997
+ },
998
+
999
+ /*
1000
+ * Transform search param object in query string
1001
+ */
1002
+ _getSearchParams: function(args, params) {
1003
+ if (this._isUndefined(args) || args === null) {
1004
+ return params;
1005
+ }
1006
+ for (var key in args) {
1007
+ if (key !== null && args.hasOwnProperty(key)) {
1008
+ params += (params.length === 0) ? '?' : '&';
1009
+ params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]);
1010
+ }
1011
+ }
1012
+ return params;
1013
+ },
1014
+ _isUndefined: function(obj) {
1015
+ return obj === void 0;
1016
+ },
1017
+
1018
+ _support: {
1019
+ hasXMLHttpRequest: 'XMLHttpRequest' in window,
1020
+ hasXDomainRequest: 'XDomainRequest' in window,
1021
+ cors: 'withCredentials' in new XMLHttpRequest(),
1022
+ timeout: 'timeout' in new XMLHttpRequest()
1023
+ }
1024
+ };
1025
+
1026
+ /*
1027
+ * Contains all the functions related to one index
1028
+ * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object
1029
+ */
1030
+ AlgoliaSearch.prototype.Index.prototype = {
1031
+ /*
1032
+ * Clear all queries in cache
1033
+ */
1034
+ clearCache: function() {
1035
+ this.cache = {};
1036
+ },
1037
+ /*
1038
+ * Add an object in this index
1039
+ *
1040
+ * @param content contains the javascript object to add inside the index
1041
+ * @param callback (optional) the result callback with two arguments:
1042
+ * success: boolean set to true if the request was successfull
1043
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1044
+ * @param objectID (optional) an objectID you want to attribute to this object
1045
+ * (if the attribute already exist the old object will be overwrite)
1046
+ */
1047
+ addObject: function(content, callback, objectID) {
1048
+ var indexObj = this;
1049
+ if (this.as._isUndefined(objectID)) {
1050
+ return this.as._jsonRequest({ method: 'POST',
1051
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName),
1052
+ body: content,
1053
+ callback: callback });
1054
+ } else {
1055
+ return this.as._jsonRequest({ method: 'PUT',
1056
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1057
+ body: content,
1058
+ callback: callback });
1059
+ }
1060
+
1061
+ },
1062
+ /*
1063
+ * Add several objects
1064
+ *
1065
+ * @param objects contains an array of objects to add
1066
+ * @param callback (optional) the result callback with two arguments:
1067
+ * success: boolean set to true if the request was successfull
1068
+ * content: the server answer that updateAt and taskID
1069
+ */
1070
+ addObjects: function(objects, callback) {
1071
+ var indexObj = this;
1072
+ var postObj = {requests:[]};
1073
+ for (var i = 0; i < objects.length; ++i) {
1074
+ var request = { action: 'addObject',
1075
+ body: objects[i] };
1076
+ postObj.requests.push(request);
1077
+ }
1078
+ return this.as._jsonRequest({ method: 'POST',
1079
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1080
+ body: postObj,
1081
+ callback: callback });
1082
+ },
1083
+ /*
1084
+ * Get an object from this index
1085
+ *
1086
+ * @param objectID the unique identifier of the object to retrieve
1087
+ * @param callback (optional) the result callback with two arguments
1088
+ * success: boolean set to true if the request was successfull
1089
+ * content: the object to retrieve or the error message if a failure occured
1090
+ * @param attributes (optional) if set, contains the array of attribute names to retrieve
1091
+ */
1092
+ getObject: function(objectID, callback, attributes) {
1093
+ if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) {
1094
+ attributes = callback;
1095
+ callback = null;
1096
+ }
1097
+ var indexObj = this;
1098
+ var params = '';
1099
+ if (!this.as._isUndefined(attributes)) {
1100
+ params = '?attributes=';
1101
+ for (var i = 0; i < attributes.length; ++i) {
1102
+ if (i !== 0) {
1103
+ params += ',';
1104
+ }
1105
+ params += attributes[i];
1106
+ }
1107
+ }
1108
+
1109
+ return this.as._jsonRequest({ method: 'GET',
1110
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params,
1111
+ callback: callback });
1112
+ },
1113
+
1114
+ /*
1115
+ * Update partially an object (only update attributes passed in argument)
1116
+ *
1117
+ * @param partialObject contains the javascript attributes to override, the
1118
+ * object must contains an objectID attribute
1119
+ * @param callback (optional) the result callback with two arguments:
1120
+ * success: boolean set to true if the request was successfull
1121
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1122
+ */
1123
+ partialUpdateObject: function(partialObject, callback) {
1124
+ var indexObj = this;
1125
+ return this.as._jsonRequest({ method: 'POST',
1126
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial',
1127
+ body: partialObject,
1128
+ callback: callback });
1129
+ },
1130
+ /*
1131
+ * Partially Override the content of several objects
1132
+ *
1133
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1134
+ * @param callback (optional) the result callback with two arguments:
1135
+ * success: boolean set to true if the request was successfull
1136
+ * content: the server answer that updateAt and taskID
1137
+ */
1138
+ partialUpdateObjects: function(objects, callback) {
1139
+ var indexObj = this;
1140
+ var postObj = {requests:[]};
1141
+ for (var i = 0; i < objects.length; ++i) {
1142
+ var request = { action: 'partialUpdateObject',
1143
+ objectID: objects[i].objectID,
1144
+ body: objects[i] };
1145
+ postObj.requests.push(request);
1146
+ }
1147
+ return this.as._jsonRequest({ method: 'POST',
1148
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1149
+ body: postObj,
1150
+ callback: callback });
1151
+ },
1152
+ /*
1153
+ * Override the content of object
1154
+ *
1155
+ * @param object contains the javascript object to save, the object must contains an objectID attribute
1156
+ * @param callback (optional) the result callback with two arguments:
1157
+ * success: boolean set to true if the request was successfull
1158
+ * content: the server answer that updateAt and taskID
1159
+ */
1160
+ saveObject: function(object, callback) {
1161
+ var indexObj = this;
1162
+ return this.as._jsonRequest({ method: 'PUT',
1163
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID),
1164
+ body: object,
1165
+ callback: callback });
1166
+ },
1167
+ /*
1168
+ * Override the content of several objects
1169
+ *
1170
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1171
+ * @param callback (optional) the result callback with two arguments:
1172
+ * success: boolean set to true if the request was successfull
1173
+ * content: the server answer that updateAt and taskID
1174
+ */
1175
+ saveObjects: function(objects, callback) {
1176
+ var indexObj = this;
1177
+ var postObj = {requests:[]};
1178
+ for (var i = 0; i < objects.length; ++i) {
1179
+ var request = { action: 'updateObject',
1180
+ objectID: objects[i].objectID,
1181
+ body: objects[i] };
1182
+ postObj.requests.push(request);
1183
+ }
1184
+ return this.as._jsonRequest({ method: 'POST',
1185
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1186
+ body: postObj,
1187
+ callback: callback });
1188
+ },
1189
+ /*
1190
+ * Delete an object from the index
1191
+ *
1192
+ * @param objectID the unique identifier of object to delete
1193
+ * @param callback (optional) the result callback with two arguments:
1194
+ * success: boolean set to true if the request was successfull
1195
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1196
+ */
1197
+ deleteObject: function(objectID, callback) {
1198
+ if (objectID === null || objectID.length === 0) {
1199
+ callback(false, { message: 'empty objectID'});
1200
+ return;
1201
+ }
1202
+ var indexObj = this;
1203
+ return this.as._jsonRequest({ method: 'DELETE',
1204
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1205
+ callback: callback });
1206
+ },
1207
+ /*
1208
+ * Search inside the index using XMLHttpRequest request (Using a POST query to
1209
+ * minimize number of OPTIONS queries: Cross-Origin Resource Sharing).
1210
+ *
1211
+ * @param query the full text query
1212
+ * @param callback the result callback with two arguments:
1213
+ * success: boolean set to true if the request was successfull. If false, the content contains the error.
1214
+ * content: the server answer that contains the list of results.
1215
+ * @param args (optional) if set, contains an object with query parameters:
1216
+ * - page: (integer) Pagination parameter used to select the page to retrieve.
1217
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1218
+ * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
1219
+ * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
1220
+ * Attributes are separated with a comma (for example "name,address").
1221
+ * You can also use an array (for example ["name","address"]).
1222
+ * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
1223
+ * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
1224
+ * Attributes are separated by a comma. You can also use an array (for example ["name","address"]).
1225
+ * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
1226
+ * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
1227
+ * A matchLevel is returned for each highlighted attribute and can contain:
1228
+ * - full: if all the query terms were found in the attribute,
1229
+ * - partial: if only some of the query terms were found,
1230
+ * - none: if none of the query terms were found.
1231
+ * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
1232
+ * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
1233
+ * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). By default no snippet is computed.
1234
+ * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
1235
+ * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
1236
+ * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
1237
+ * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
1238
+ * For example aroundLatLng=47.316669,5.016670).
1239
+ * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
1240
+ * (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).
1241
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1242
+ * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
1243
+ * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
1244
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1245
+ * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
1246
+ * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
1247
+ * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
1248
+ * You can also use an array (for example numericFilters: ["price>100","price<1000"]).
1249
+ * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
1250
+ * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
1251
+ * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
1252
+ * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
1253
+ * - facetFilters: filter the query by a list of facets.
1254
+ * Facets are separated by commas and each facet is encoded as `attributeName:value`.
1255
+ * For example: `facetFilters=category:Book,author:John%20Doe`.
1256
+ * You can also use an array (for example `["category:Book","author:John%20Doe"]`).
1257
+ * - facets: List of object attributes that you want to use for faceting.
1258
+ * Comma separated list: `"category,author"` or array `['category','author']`
1259
+ * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
1260
+ * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
1261
+ * - queryType: select how the query words are interpreted, it can be one of the following value:
1262
+ * - prefixAll: all query words are interpreted as prefixes,
1263
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1264
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1265
+ * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
1266
+ * Comma separated and array are accepted.
1267
+ * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
1268
+ * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
1269
+ * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
1270
+ * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
1271
+ * one is kept and others are removed.
1272
+ * - restrictSearchableAttributes: List of attributes you want to use for textual search (must be a subset of the attributesToIndex index setting)
1273
+ * either comma separated or as an array
1274
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime.
1275
+ */
1276
+ search: function(query, callback, args, delay) {
1277
+ if (query === undefined || query === null) {
1278
+ query = '';
1279
+ }
1280
+
1281
+ // no query = getAllObjects
1282
+ if (typeof query === 'function') {
1283
+ callback = query;
1284
+ query = '';
1285
+ }
1286
+
1287
+ if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) {
1288
+ args = callback;
1289
+ callback = null;
1290
+ }
1291
+
1292
+ var indexObj = this;
1293
+ var params = 'query=' + encodeURIComponent(query);
1294
+ if (!this.as._isUndefined(args) && args !== null) {
1295
+ params = this.as._getSearchParams(args, params);
1296
+ }
1297
+ window.clearTimeout(indexObj.onDelayTrigger);
1298
+ if (!this.as._isUndefined(delay) && delay !== null && delay > 0) {
1299
+ var onDelayTrigger = window.setTimeout( function() {
1300
+ indexObj._search(params, callback);
1301
+ }, delay);
1302
+ indexObj.onDelayTrigger = onDelayTrigger;
1303
+ } else {
1304
+ return this._search(params, callback);
1305
+ }
1306
+ },
1307
+
1308
+ /*
1309
+ * Browse all index content
1310
+ *
1311
+ * @param page Pagination parameter used to select the page to retrieve.
1312
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1313
+ * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
1314
+ */
1315
+ browse: function(page, callback, hitsPerPage) {
1316
+ if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) {
1317
+ hitsPerPage = callback;
1318
+ callback = null;
1319
+ }
1320
+ var indexObj = this;
1321
+ var params = '?page=' + page;
1322
+ if (!this.as._isUndefined(hitsPerPage)) {
1323
+ params += '&hitsPerPage=' + hitsPerPage;
1324
+ }
1325
+ return this.as._jsonRequest({ method: 'GET',
1326
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params,
1327
+ callback: callback });
1328
+ },
1329
+
1330
+ /*
1331
+ * Get a Typeahead.js adapter
1332
+ * @param searchParams contains an object with query parameters (see search for details)
1333
+ */
1334
+ ttAdapter: function(params) {
1335
+ var self = this;
1336
+ return function(query, cb) {
1337
+ self.search(query, function(success, content) {
1338
+ if (success) {
1339
+ cb(content.hits);
1340
+ } else {
1341
+ cb(content && content.message);
1342
+ }
1343
+ }, params);
1344
+ };
1345
+ },
1346
+
1347
+ /*
1348
+ * Wait the publication of a task on the server.
1349
+ * All server task are asynchronous and you can check with this method that the task is published.
1350
+ *
1351
+ * @param taskID the id of the task returned by server
1352
+ * @param callback the result callback with with two arguments:
1353
+ * success: boolean set to true if the request was successfull
1354
+ * content: the server answer that contains the list of results
1355
+ */
1356
+ waitTask: function(taskID, callback) {
1357
+ var indexObj = this;
1358
+ return this.as._jsonRequest({ method: 'GET',
1359
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID,
1360
+ callback: function(success, body) {
1361
+ if (success) {
1362
+ if (body.status === 'published') {
1363
+ callback(true, body);
1364
+ } else {
1365
+ setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100);
1366
+ }
1367
+ } else {
1368
+ callback(false, body);
1369
+ }
1370
+ }});
1371
+ },
1372
+
1373
+ /*
1374
+ * This function deletes the index content. Settings and index specific API keys are kept untouched.
1375
+ *
1376
+ * @param callback (optional) the result callback with two arguments
1377
+ * success: boolean set to true if the request was successfull
1378
+ * content: the settings object or the error message if a failure occured
1379
+ */
1380
+ clearIndex: function(callback) {
1381
+ var indexObj = this;
1382
+ return this.as._jsonRequest({ method: 'POST',
1383
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear',
1384
+ callback: callback });
1385
+ },
1386
+ /*
1387
+ * Get settings of this index
1388
+ *
1389
+ * @param callback (optional) the result callback with two arguments
1390
+ * success: boolean set to true if the request was successfull
1391
+ * content: the settings object or the error message if a failure occured
1392
+ */
1393
+ getSettings: function(callback) {
1394
+ var indexObj = this;
1395
+ return this.as._jsonRequest({ method: 'GET',
1396
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1397
+ callback: callback });
1398
+ },
1399
+
1400
+ /*
1401
+ * Set settings for this index
1402
+ *
1403
+ * @param settigns the settings object that can contains :
1404
+ * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
1405
+ * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
1406
+ * - hitsPerPage: (integer) the number of hits per page (default = 10).
1407
+ * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
1408
+ * If set to null, all attributes are retrieved.
1409
+ * - attributesToHighlight: (array of strings) default list of attributes to highlight.
1410
+ * If set to null, all indexed attributes are highlighted.
1411
+ * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
1412
+ * By default no snippet is computed. If set to null, no snippet is computed.
1413
+ * - attributesToIndex: (array of strings) the list of fields you want to index.
1414
+ * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
1415
+ * This parameter has two important uses:
1416
+ * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
1417
+ * retrieve it but you don't want to search in the base64 string.
1418
+ * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
1419
+ * the list will be considered more important than matches in attributes further down the list.
1420
+ * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
1421
+ * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
1422
+ * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
1423
+ * 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.
1424
+ * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
1425
+ * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
1426
+ * 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.
1427
+ * - ranking: (array of strings) controls the way results are sorted.
1428
+ * We have six available criteria:
1429
+ * - typo: sort according to number of typos,
1430
+ * - geo: sort according to decreassing distance when performing a geo-location based search,
1431
+ * - proximity: sort according to the proximity of query words in hits,
1432
+ * - attribute: sort according to the order of attributes defined by attributesToIndex,
1433
+ * - exact:
1434
+ * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
1435
+ * 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
1436
+ * show starting by the v letter before it.
1437
+ * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
1438
+ * - custom: sort according to a user defined formula set in **customRanking** attribute.
1439
+ * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
1440
+ * - customRanking: (array of strings) lets you specify part of the ranking.
1441
+ * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
1442
+ * For example `"customRanking" => ["desc(population)", "asc(name)"]`
1443
+ * - queryType: Select how the query words are interpreted, it can be one of the following value:
1444
+ * - prefixAll: all query words are interpreted as prefixes,
1445
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1446
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1447
+ * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
1448
+ * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
1449
+ * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
1450
+ * @param callback (optional) the result callback with two arguments
1451
+ * success: boolean set to true if the request was successfull
1452
+ * content: the server answer or the error message if a failure occured
1453
+ */
1454
+ setSettings: function(settings, callback) {
1455
+ var indexObj = this;
1456
+ return this.as._jsonRequest({ method: 'PUT',
1457
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1458
+ body: settings,
1459
+ callback: callback });
1460
+ },
1461
+ /*
1462
+ * List all existing user keys associated to this index
1463
+ *
1464
+ * @param callback the result callback with two arguments
1465
+ * success: boolean set to true if the request was successfull
1466
+ * content: the server answer with user keys list or error description if success is false.
1467
+ */
1468
+ listUserKeys: function(callback) {
1469
+ var indexObj = this;
1470
+ return this.as._jsonRequest({ method: 'GET',
1471
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1472
+ callback: callback });
1473
+ },
1474
+ /*
1475
+ * Get ACL of a user key associated to this index
1476
+ *
1477
+ * @param callback the result callback with two arguments
1478
+ * success: boolean set to true if the request was successfull
1479
+ * content: the server answer with user keys list or error description if success is false.
1480
+ */
1481
+ getUserKeyACL: function(key, callback) {
1482
+ var indexObj = this;
1483
+ return this.as._jsonRequest({ method: 'GET',
1484
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1485
+ callback: callback });
1486
+ },
1487
+ /*
1488
+ * Delete an existing user key associated to this index
1489
+ *
1490
+ * @param callback the result callback with two arguments
1491
+ * success: boolean set to true if the request was successfull
1492
+ * content: the server answer with user keys list or error description if success is false.
1493
+ */
1494
+ deleteUserKey: function(key, callback) {
1495
+ var indexObj = this;
1496
+ return this.as._jsonRequest({ method: 'DELETE',
1497
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1498
+ callback: callback });
1499
+ },
1500
+ /*
1501
+ * Add an existing user key associated to this index
1502
+ *
1503
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1504
+ * can contains the following values:
1505
+ * - search: allow to search (https and http)
1506
+ * - addObject: allows to add/update an object in the index (https only)
1507
+ * - deleteObject : allows to delete an existing object (https only)
1508
+ * - deleteIndex : allows to delete index content (https only)
1509
+ * - settings : allows to get index settings (https only)
1510
+ * - editSettings : allows to change index settings (https only)
1511
+ * @param callback the result callback with two arguments
1512
+ * success: boolean set to true if the request was successfull
1513
+ * content: the server answer with user keys list or error description if success is false.
1514
+ */
1515
+ addUserKey: function(acls, callback) {
1516
+ var indexObj = this;
1517
+ var aclsObject = {};
1518
+ aclsObject.acl = acls;
1519
+ return this.as._jsonRequest({ method: 'POST',
1520
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1521
+ body: aclsObject,
1522
+ callback: callback });
1523
+ },
1524
+ /*
1525
+ * Add an existing user key associated to this index
1526
+ *
1527
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1528
+ * can contains the following values:
1529
+ * - search: allow to search (https and http)
1530
+ * - addObject: allows to add/update an object in the index (https only)
1531
+ * - deleteObject : allows to delete an existing object (https only)
1532
+ * - deleteIndex : allows to delete index content (https only)
1533
+ * - settings : allows to get index settings (https only)
1534
+ * - editSettings : allows to change index settings (https only)
1535
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
1536
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
1537
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
1538
+ * @param callback the result callback with two arguments
1539
+ * success: boolean set to true if the request was successfull
1540
+ * content: the server answer with user keys list or error description if success is false.
1541
+ */
1542
+ addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
1543
+ var indexObj = this;
1544
+ var aclsObject = {};
1545
+ aclsObject.acl = acls;
1546
+ aclsObject.validity = validity;
1547
+ aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
1548
+ aclsObject.maxHitsPerQuery = maxHitsPerQuery;
1549
+ return this.as._jsonRequest({ method: 'POST',
1550
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1551
+ body: aclsObject,
1552
+ callback: callback });
1553
+ },
1554
+ ///
1555
+ /// Internal methods only after this line
1556
+ ///
1557
+ _search: function(params, callback) {
1558
+ var pObj = {params: params};
1559
+ if (this.as.jsonp === null) {
1560
+ var self = this;
1561
+ return this.as._jsonRequest({ cache: this.cache,
1562
+ method: 'POST',
1563
+ url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1564
+ body: pObj,
1565
+ callback: function(success, content) {
1566
+ var status = content && content.status;
1567
+ if (success || status && Math.floor(status / 100) === 4 || Math.floor(status / 100) === 1) {
1568
+ self.as.jsonp = false;
1569
+ callback && callback(success, content);
1570
+ } else {
1571
+ self.as.jsonp = true;
1572
+ self._search(params, callback);
1573
+ }
1574
+ }
1575
+ });
1576
+ } else if (this.as.jsonp) {
1577
+ return this.as._jsonRequest({ cache: this.cache,
1578
+ method: 'GET',
1579
+ url: '/1/indexes/' + encodeURIComponent(this.indexName),
1580
+ body: pObj,
1581
+ callback: callback });
1582
+ } else {
1583
+ return this.as._jsonRequest({ cache: this.cache,
1584
+ method: 'POST',
1585
+ url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1586
+ body: pObj,
1587
+ callback: callback});
1588
+ }
1589
+ },
1590
+
1591
+ // internal attributes
1592
+ as: null,
1593
+ indexName: null,
1594
+ typeAheadArgs: null,
1595
+ typeAheadValueOption: null
1596
+ };
1597
+
1598
+ /*
1599
+ * Copyright (c) 2014 Algolia
1600
+ * http://www.algolia.com/
1601
+ *
1602
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
1603
+ * of this software and associated documentation files (the "Software"), to deal
1604
+ * in the Software without restriction, including without limitation the rights
1605
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1606
+ * copies of the Software, and to permit persons to whom the Software is
1607
+ * furnished to do so, subject to the following conditions:
1608
+ *
1609
+ * The above copyright notice and this permission notice shall be included in
1610
+ * all copies or substantial portions of the Software.
1611
+ *
1612
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1613
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1614
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1615
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1616
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1617
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1618
+ * THE SOFTWARE.
1619
+ */
1620
+
1621
+ (function($) {
1622
+ var extend = function(out) {
1623
+ out = out || {};
1624
+ for (var i = 1; i < arguments.length; i++) {
1625
+ if (!arguments[i]) {
1626
+ continue;
1627
+ }
1628
+ for (var key in arguments[i]) {
1629
+ if (arguments[i].hasOwnProperty(key)) {
1630
+ out[key] = arguments[i][key];
1631
+ }
1632
+ }
1633
+ }
1634
+ return out;
1635
+ };
1636
+
1637
+ /**
1638
+ * Algolia Search Helper providing faceting and disjunctive faceting
1639
+ * @param {AlgoliaSearch} client an AlgoliaSearch client
1640
+ * @param {string} index the index name to query
1641
+ * @param {hash} options an associative array defining the hitsPerPage, list of facets, the list of disjunctive facets and the default facet filters
1642
+ */
1643
+ window.AlgoliaSearchHelper = function(client, index, options) {
1644
+ /// Default options
1645
+ var defaults = {
1646
+ facets: [], // list of facets to compute
1647
+ disjunctiveFacets: [], // list of disjunctive facets to compute
1648
+ hitsPerPage: 20, // number of hits per page
1649
+ defaultFacetFilters: [] // the default list of facetFilters
1650
+ };
1651
+
1652
+ this.init(client, index, extend({}, defaults, options));
1653
+ };
1654
+
1655
+ AlgoliaSearchHelper.prototype = {
1656
+ /**
1657
+ * Initialize a new AlgoliaSearchHelper
1658
+ * @param {AlgoliaSearch} client an AlgoliaSearch client
1659
+ * @param {string} index the index name to query
1660
+ * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets
1661
+ * @return {AlgoliaSearchHelper}
1662
+ */
1663
+ init: function(client, index, options) {
1664
+ this.client = client;
1665
+ this.index = index;
1666
+ this.options = options;
1667
+ this.page = 0;
1668
+ this.refinements = {};
1669
+ this.excludes = {};
1670
+ this.disjunctiveRefinements = {};
1671
+ this.extraQueries = [];
1672
+ },
1673
+
1674
+ /**
1675
+ * Perform a query
1676
+ * @param {string} q the user query
1677
+ * @param {function} searchCallback the result callback called with two arguments:
1678
+ * success: boolean set to true if the request was successfull
1679
+ * content: the query answer with an extra 'disjunctiveFacets' attribute
1680
+ */
1681
+ search: function(q, searchCallback, searchParams) {
1682
+ this.q = q;
1683
+ this.searchCallback = searchCallback;
1684
+ this.searchParams = searchParams || {};
1685
+ this.page = this.page || 0;
1686
+ this.refinements = this.refinements || {};
1687
+ this.disjunctiveRefinements = this.disjunctiveRefinements || {};
1688
+ this._search();
1689
+ },
1690
+
1691
+ /**
1692
+ * Remove all refinements (disjunctive + conjunctive)
1693
+ */
1694
+ clearRefinements: function() {
1695
+ this.disjunctiveRefinements = {};
1696
+ this.refinements = {};
1697
+ },
1698
+
1699
+ /**
1700
+ * Ensure a facet refinement exists
1701
+ * @param {string} facet the facet to refine
1702
+ * @param {string} value the associated value
1703
+ */
1704
+ addDisjunctiveRefine: function(facet, value) {
1705
+ this.disjunctiveRefinements = this.disjunctiveRefinements || {};
1706
+ this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
1707
+ this.disjunctiveRefinements[facet][value] = true;
1708
+ },
1709
+
1710
+ /**
1711
+ * Ensure a facet refinement does not exist
1712
+ * @param {string} facet the facet to refine
1713
+ * @param {string} value the associated value
1714
+ */
1715
+ removeDisjunctiveRefine: function(facet, value) {
1716
+ this.disjunctiveRefinements = this.disjunctiveRefinements || {};
1717
+ this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
1718
+ try {
1719
+ delete this.disjunctiveRefinements[facet][value];
1720
+ } catch (e) {
1721
+ this.disjunctiveRefinements[facet][value] = undefined; // IE compat
1722
+ }
1723
+ },
1724
+
1725
+ /**
1726
+ * Ensure a facet refinement exists
1727
+ * @param {string} facet the facet to refine
1728
+ * @param {string} value the associated value
1729
+ */
1730
+ addRefine: function(facet, value) {
1731
+ var refinement = facet + ':' + value;
1732
+ this.refinements = this.refinements || {};
1733
+ this.refinements[refinement] = true;
1734
+ },
1735
+
1736
+ /**
1737
+ * Ensure a facet refinement does not exist
1738
+ * @param {string} facet the facet to refine
1739
+ * @param {string} value the associated value
1740
+ */
1741
+ removeRefine: function(facet, value) {
1742
+ var refinement = facet + ':' + value;
1743
+ this.refinements = this.refinements || {};
1744
+ this.refinements[refinement] = false;
1745
+ },
1746
+
1747
+ /**
1748
+ * Ensure a facet exclude exists
1749
+ * @param {string} facet the facet to refine
1750
+ * @param {string} value the associated value
1751
+ */
1752
+ addExclude: function(facet, value) {
1753
+ var refinement = facet + ':-' + value;
1754
+ this.excludes = this.excludes || {};
1755
+ this.excludes[refinement] = true;
1756
+ },
1757
+
1758
+ /**
1759
+ * Ensure a facet exclude does not exist
1760
+ * @param {string} facet the facet to refine
1761
+ * @param {string} value the associated value
1762
+ */
1763
+ removeExclude: function(facet, value) {
1764
+ var refinement = facet + ':-' + value;
1765
+ this.excludes = this.excludes || {};
1766
+ this.excludes[refinement] = false;
1767
+ },
1768
+
1769
+ /**
1770
+ * Toggle refinement state of an exclude
1771
+ * @param {string} facet the facet to refine
1772
+ * @param {string} value the associated value
1773
+ * @return {boolean} true if the facet has been found
1774
+ */
1775
+ toggleExclude: function(facet, value) {
1776
+ for (var i = 0; i < this.options.facets.length; ++i) {
1777
+ if (this.options.facets[i] == facet) {
1778
+ var refinement = facet + ':-' + value;
1779
+ this.excludes[refinement] = !this.excludes[refinement];
1780
+ this.page = 0;
1781
+ this._search();
1782
+ return true;
1783
+ }
1784
+ }
1785
+ return false;
1786
+ },
1787
+
1788
+ /**
1789
+ * Toggle refinement state of a facet
1790
+ * @param {string} facet the facet to refine
1791
+ * @param {string} value the associated value
1792
+ * @return {boolean} true if the facet has been found
1793
+ */
1794
+ toggleRefine: function(facet, value) {
1795
+ for (var i = 0; i < this.options.facets.length; ++i) {
1796
+ if (this.options.facets[i] == facet) {
1797
+ var refinement = facet + ':' + value;
1798
+ this.refinements[refinement] = !this.refinements[refinement];
1799
+ this.page = 0;
1800
+ this._search();
1801
+ return true;
1802
+ }
1803
+ }
1804
+ this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
1805
+ for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) {
1806
+ if (this.options.disjunctiveFacets[j] == facet) {
1807
+ this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value];
1808
+ this.page = 0;
1809
+ this._search();
1810
+ return true;
1811
+ }
1812
+ }
1813
+ return false;
1814
+ },
1815
+
1816
+ /**
1817
+ * Check the refinement state of a facet
1818
+ * @param {string} facet the facet
1819
+ * @param {string} value the associated value
1820
+ * @return {boolean} true if refined
1821
+ */
1822
+ isRefined: function(facet, value) {
1823
+ var refinement = facet + ':' + value;
1824
+ if (this.refinements[refinement]) {
1825
+ return true;
1826
+ }
1827
+ if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) {
1828
+ return true;
1829
+ }
1830
+ return false;
1831
+ },
1832
+
1833
+ /**
1834
+ * Check the exclude state of a facet
1835
+ * @param {string} facet the facet
1836
+ * @param {string} value the associated value
1837
+ * @return {boolean} true if refined
1838
+ */
1839
+ isExcluded: function(facet, value) {
1840
+ var refinement = facet + ':-' + value;
1841
+ if (this.excludes[refinement]) {
1842
+ return true;
1843
+ }
1844
+ return false;
1845
+ },
1846
+
1847
+ /**
1848
+ * Go to next page
1849
+ */
1850
+ nextPage: function() {
1851
+ this._gotoPage(this.page + 1);
1852
+ },
1853
+
1854
+ /**
1855
+ * Go to previous page
1856
+ */
1857
+ previousPage: function() {
1858
+ if (this.page > 0) {
1859
+ this._gotoPage(this.page - 1);
1860
+ }
1861
+ },
1862
+
1863
+ /**
1864
+ * Goto a page
1865
+ * @param {integer} page The page number
1866
+ */
1867
+ gotoPage: function(page) {
1868
+ this._gotoPage(page);
1869
+ },
1870
+
1871
+ /**
1872
+ * Configure the page but do not trigger a reload
1873
+ * @param {integer} page The page number
1874
+ */
1875
+ setPage: function(page) {
1876
+ this.page = page;
1877
+ },
1878
+
1879
+ /**
1880
+ * Configure the underlying index name
1881
+ * @param {string} name the index name
1882
+ */
1883
+ setIndex: function(name) {
1884
+ this.index = name;
1885
+ },
1886
+
1887
+ /**
1888
+ * Get the underlying configured index name
1889
+ */
1890
+ getIndex: function() {
1891
+ return this.index;
1892
+ },
1893
+
1894
+ /**
1895
+ * Clear the extra queries added to the underlying batch of queries
1896
+ */
1897
+ clearExtraQueries: function() {
1898
+ this.extraQueries = [];
1899
+ },
1900
+
1901
+ /**
1902
+ * Add an extra query to the underlying batch of queries. Once you add queries
1903
+ * to the batch, the 2nd parameter of the searchCallback will be an object with a `results`
1904
+ * attribute listing all search results.
1905
+ */
1906
+ addExtraQuery: function(index, query, params) {
1907
+ this.extraQueries.push({ index: index, query: query, params: (params || {}) });
1908
+ },
1909
+
1910
+ ///////////// PRIVATE
1911
+
1912
+ /**
1913
+ * Goto a page
1914
+ * @param {integer} page The page number
1915
+ */
1916
+ _gotoPage: function(page) {
1917
+ this.page = page;
1918
+ this._search();
1919
+ },
1920
+
1921
+ /**
1922
+ * Perform the underlying queries
1923
+ */
1924
+ _search: function() {
1925
+ this.client.startQueriesBatch();
1926
+ this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams());
1927
+ var disjunctiveFacets = [];
1928
+ var unusedDisjunctiveFacets = {};
1929
+ var i = 0;
1930
+ for (i = 0; i < this.options.disjunctiveFacets.length; ++i) {
1931
+ var facet = this.options.disjunctiveFacets[i];
1932
+ if (this._hasDisjunctiveRefinements(facet)) {
1933
+ disjunctiveFacets.push(facet);
1934
+ } else {
1935
+ unusedDisjunctiveFacets[facet] = true;
1936
+ }
1937
+ }
1938
+ for (i = 0; i < disjunctiveFacets.length; ++i) {
1939
+ this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(disjunctiveFacets[i]));
1940
+ }
1941
+ for (i = 0; i < this.extraQueries.length; ++i) {
1942
+ this.client.addQueryInBatch(this.extraQueries[i].index, this.extraQueries[i].query, this.extraQueries[i].params);
1943
+ }
1944
+ var self = this;
1945
+ this.client.sendQueriesBatch(function(success, content) {
1946
+ if (!success) {
1947
+ self.searchCallback(false, content);
1948
+ return;
1949
+ }
1950
+ var aggregatedAnswer = content.results[0];
1951
+ aggregatedAnswer.disjunctiveFacets = aggregatedAnswer.disjunctiveFacets || {};
1952
+ aggregatedAnswer.facets_stats = aggregatedAnswer.facets_stats || {};
1953
+ // create disjunctive facets from facets (disjunctive facets without refinements)
1954
+ for (var facet in unusedDisjunctiveFacets) {
1955
+ if (aggregatedAnswer.facets[facet] && !aggregatedAnswer.disjunctiveFacets[facet]) {
1956
+ aggregatedAnswer.disjunctiveFacets[facet] = aggregatedAnswer.facets[facet];
1957
+ try {
1958
+ delete aggregatedAnswer.facets[facet];
1959
+ } catch (e) {
1960
+ aggregatedAnswer.facets[facet] = undefined; // IE compat
1961
+ }
1962
+ }
1963
+ }
1964
+ // aggregate the disjunctive facets
1965
+ for (i = 0; i < disjunctiveFacets.length; ++i) {
1966
+ for (var dfacet in content.results[i + 1].facets) {
1967
+ aggregatedAnswer.disjunctiveFacets[dfacet] = content.results[i + 1].facets[dfacet];
1968
+ if (self.disjunctiveRefinements[dfacet]) {
1969
+ for (var value in self.disjunctiveRefinements[dfacet]) {
1970
+ // add the disjunctive reginements if it is no more retrieved
1971
+ if (!aggregatedAnswer.disjunctiveFacets[dfacet][value] && self.disjunctiveRefinements[dfacet][value]) {
1972
+ aggregatedAnswer.disjunctiveFacets[dfacet][value] = 0;
1973
+ }
1974
+ }
1975
+ }
1976
+ }
1977
+ // aggregate the disjunctive facets stats
1978
+ for (var stats in content.results[i + 1].facets_stats) {
1979
+ aggregatedAnswer.facets_stats[stats] = content.results[i + 1].facets_stats[stats];
1980
+ }
1981
+ }
1982
+
1983
+ // Backward compatibility
1984
+ aggregatedAnswer.facetStats = aggregatedAnswer.facets_stats;
1985
+
1986
+ // add the excludes
1987
+ for (var exclude in self.excludes) {
1988
+ if (self.excludes[exclude]) {
1989
+ var e = exclude.indexOf(':-');
1990
+ var facet = exclude.slice(0, e);
1991
+ var value = exclude.slice(e + 2);
1992
+ aggregatedAnswer.facets[facet] = aggregatedAnswer.facets[facet] || {};
1993
+ if (!aggregatedAnswer.facets[facet][value]) {
1994
+ aggregatedAnswer.facets[facet][value] = 0;
1995
+ }
1996
+ }
1997
+ }
1998
+ // call the actual callback
1999
+ if (self.extraQueries.length === 0) {
2000
+ self.searchCallback(true, aggregatedAnswer);
2001
+ } else {
2002
+ // append the extra queries
2003
+ var c = { results: [ aggregatedAnswer ] };
2004
+ for (i = 0; i < self.extraQueries.length; ++i) {
2005
+ c.results.push(content.results[1 + disjunctiveFacets.length + i]);
2006
+ }
2007
+ self.searchCallback(true, c);
2008
+ }
2009
+ });
2010
+ },
2011
+
2012
+ /**
2013
+ * Build search parameters used to fetch hits
2014
+ * @return {hash}
2015
+ */
2016
+ _getHitsSearchParams: function() {
2017
+ var facets = [];
2018
+ var i = 0;
2019
+ for (i = 0; i < this.options.facets.length; ++i) {
2020
+ facets.push(this.options.facets[i]);
2021
+ }
2022
+ for (i = 0; i < this.options.disjunctiveFacets.length; ++i) {
2023
+ var facet = this.options.disjunctiveFacets[i];
2024
+ if (!this._hasDisjunctiveRefinements(facet)) {
2025
+ facets.push(facet);
2026
+ }
2027
+ }
2028
+ return extend({}, {
2029
+ hitsPerPage: this.options.hitsPerPage,
2030
+ page: this.page,
2031
+ facets: facets,
2032
+ facetFilters: this._getFacetFilters()
2033
+ }, this.searchParams);
2034
+ },
2035
+
2036
+ /**
2037
+ * Build search parameters used to fetch a disjunctive facet
2038
+ * @param {string} facet the associated facet name
2039
+ * @return {hash}
2040
+ */
2041
+ _getDisjunctiveFacetSearchParams: function(facet) {
2042
+ return extend({}, this.searchParams, {
2043
+ hitsPerPage: 1,
2044
+ page: 0,
2045
+ attributesToRetrieve: [],
2046
+ attributesToHighlight: [],
2047
+ attributesToSnippet: [],
2048
+ facets: facet,
2049
+ facetFilters: this._getFacetFilters(facet),
2050
+ analytics: false
2051
+ });
2052
+ },
2053
+
2054
+ /**
2055
+ * Test if there are some disjunctive refinements on the facet
2056
+ */
2057
+ _hasDisjunctiveRefinements: function(facet) {
2058
+ for (var value in this.disjunctiveRefinements[facet]) {
2059
+ if (this.disjunctiveRefinements[facet][value]) {
2060
+ return true;
2061
+ }
2062
+ }
2063
+ return false;
2064
+ },
2065
+
2066
+ /**
2067
+ * Build facetFilters parameter based on current refinements
2068
+ * @param {string} facet if set, the current disjunctive facet
2069
+ * @return {hash}
2070
+ */
2071
+ _getFacetFilters: function(facet) {
2072
+ var facetFilters = [];
2073
+ if (this.options.defaultFacetFilters) {
2074
+ for (var i = 0; i < this.options.defaultFacetFilters.length; ++i) {
2075
+ facetFilters.push(this.options.defaultFacetFilters[i]);
2076
+ }
2077
+ }
2078
+ for (var refinement in this.refinements) {
2079
+ if (this.refinements[refinement]) {
2080
+ facetFilters.push(refinement);
2081
+ }
2082
+ }
2083
+ for (var refinement in this.excludes) {
2084
+ if (this.excludes[refinement]) {
2085
+ facetFilters.push(refinement);
2086
+ }
2087
+ }
2088
+ for (var disjunctiveRefinement in this.disjunctiveRefinements) {
2089
+ if (disjunctiveRefinement != facet) {
2090
+ var refinements = [];
2091
+ for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) {
2092
+ if (this.disjunctiveRefinements[disjunctiveRefinement][value]) {
2093
+ refinements.push(disjunctiveRefinement + ':' + value);
2094
+ }
2095
+ }
2096
+ if (refinements.length > 0) {
2097
+ facetFilters.push(refinements);
2098
+ }
2099
+ }
2100
+ }
2101
+ return facetFilters;
2102
+ }
2103
+ };
2104
+ })();
2105
+
2106
+ /*
2107
+ * Copyright (c) 2014 Algolia
2108
+ * http://www.algolia.com/
2109
+ *
2110
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
2111
+ * of this software and associated documentation files (the "Software"), to deal
2112
+ * in the Software without restriction, including without limitation the rights
2113
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2114
+ * copies of the Software, and to permit persons to whom the Software is
2115
+ * furnished to do so, subject to the following conditions:
2116
+ *
2117
+ * The above copyright notice and this permission notice shall be included in
2118
+ * all copies or substantial portions of the Software.
2119
+ *
2120
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2121
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2122
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2123
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2124
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2125
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2126
+ * THE SOFTWARE.
2127
+ */
2128
+
2129
+ (function($) {
2130
+
2131
+ /**
2132
+ * Algolia Places API
2133
+ * @param {string} Your application ID
2134
+ * @param {string} Your API Key
2135
+ */
2136
+ window.AlgoliaPlaces = function(applicationID, apiKey) {
2137
+ this.init(applicationID, apiKey);
2138
+ };
2139
+
2140
+ AlgoliaPlaces.prototype = {
2141
+ /**
2142
+ * @param {string} Your application ID
2143
+ * @param {string} Your API Key
2144
+ */
2145
+ init: function(applicationID, apiKey) {
2146
+ this.client = new AlgoliaSearch(applicationID, apiKey, 'http', true, ['places-1.algolia.io', 'places-2.algolia.io', 'places-3.algolia.io']);
2147
+ this.cache = {};
2148
+ },
2149
+
2150
+ /**
2151
+ * Perform a query
2152
+ * @param {string} q the user query
2153
+ * @param {function} searchCallback the result callback called with two arguments:
2154
+ * success: boolean set to true if the request was successfull
2155
+ * content: the query answer with an extra 'disjunctiveFacets' attribute
2156
+ * @param {hash} the list of search parameters
2157
+ */
2158
+ search: function(q, searchCallback, searchParams) {
2159
+ var indexObj = this;
2160
+ var params = 'query=' + encodeURIComponent(q);
2161
+ if (!this.client._isUndefined(searchParams) && searchParams != null) {
2162
+ params = this.client._getSearchParams(searchParams, params);
2163
+ }
2164
+ var pObj = {params: params, apiKey: this.client.apiKey, appID: this.client.applicationID};
2165
+ this.client._jsonRequest({ cache: this.cache,
2166
+ method: 'POST',
2167
+ url: '/1/places/query',
2168
+ body: pObj,
2169
+ callback: searchCallback,
2170
+ removeCustomHTTPHeaders: true });
2171
+ }
2172
+ };
2173
+ })();
2174
+
2175
+ /*
2176
+ json2.js
2177
+ 2014-02-04
2178
+
2179
+ Public Domain.
2180
+
2181
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
2182
+
2183
+ See http://www.JSON.org/js.html
2184
+
2185
+
2186
+ This code should be minified before deployment.
2187
+ See http://javascript.crockford.com/jsmin.html
2188
+
2189
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
2190
+ NOT CONTROL.
2191
+
2192
+
2193
+ This file creates a global JSON object containing two methods: stringify
2194
+ and parse.
2195
+
2196
+ JSON.stringify(value, replacer, space)
2197
+ value any JavaScript value, usually an object or array.
2198
+
2199
+ replacer an optional parameter that determines how object
2200
+ values are stringified for objects. It can be a
2201
+ function or an array of strings.
2202
+
2203
+ space an optional parameter that specifies the indentation
2204
+ of nested structures. If it is omitted, the text will
2205
+ be packed without extra whitespace. If it is a number,
2206
+ it will specify the number of spaces to indent at each
2207
+ level. If it is a string (such as '\t' or '&nbsp;'),
2208
+ it contains the characters used to indent at each level.
2209
+
2210
+ This method produces a JSON text from a JavaScript value.
2211
+
2212
+ When an object value is found, if the object contains a toJSON
2213
+ method, its toJSON method will be called and the result will be
2214
+ stringified. A toJSON method does not serialize: it returns the
2215
+ value represented by the name/value pair that should be serialized,
2216
+ or undefined if nothing should be serialized. The toJSON method
2217
+ will be passed the key associated with the value, and this will be
2218
+ bound to the value
2219
+
2220
+ For example, this would serialize Dates as ISO strings.
2221
+
2222
+ Date.prototype.toJSON = function (key) {
2223
+ function f(n) {
2224
+ // Format integers to have at least two digits.
2225
+ return n < 10 ? '0' + n : n;
2226
+ }
2227
+
2228
+ return this.getUTCFullYear() + '-' +
2229
+ f(this.getUTCMonth() + 1) + '-' +
2230
+ f(this.getUTCDate()) + 'T' +
2231
+ f(this.getUTCHours()) + ':' +
2232
+ f(this.getUTCMinutes()) + ':' +
2233
+ f(this.getUTCSeconds()) + 'Z';
2234
+ };
2235
+
2236
+ You can provide an optional replacer method. It will be passed the
2237
+ key and value of each member, with this bound to the containing
2238
+ object. The value that is returned from your method will be
2239
+ serialized. If your method returns undefined, then the member will
2240
+ be excluded from the serialization.
2241
+
2242
+ If the replacer parameter is an array of strings, then it will be
2243
+ used to select the members to be serialized. It filters the results
2244
+ such that only members with keys listed in the replacer array are
2245
+ stringified.
2246
+
2247
+ Values that do not have JSON representations, such as undefined or
2248
+ functions, will not be serialized. Such values in objects will be
2249
+ dropped; in arrays they will be replaced with null. You can use
2250
+ a replacer function to replace those with JSON values.
2251
+ JSON.stringify(undefined) returns undefined.
2252
+
2253
+ The optional space parameter produces a stringification of the
2254
+ value that is filled with line breaks and indentation to make it
2255
+ easier to read.
2256
+
2257
+ If the space parameter is a non-empty string, then that string will
2258
+ be used for indentation. If the space parameter is a number, then
2259
+ the indentation will be that many spaces.
2260
+
2261
+ Example:
2262
+
2263
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
2264
+ // text is '["e",{"pluribus":"unum"}]'
2265
+
2266
+
2267
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
2268
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
2269
+
2270
+ text = JSON.stringify([new Date()], function (key, value) {
2271
+ return this[key] instanceof Date ?
2272
+ 'Date(' + this[key] + ')' : value;
2273
+ });
2274
+ // text is '["Date(---current time---)"]'
2275
+
2276
+
2277
+ JSON.parse(text, reviver)
2278
+ This method parses a JSON text to produce an object or array.
2279
+ It can throw a SyntaxError exception.
2280
+
2281
+ The optional reviver parameter is a function that can filter and
2282
+ transform the results. It receives each of the keys and values,
2283
+ and its return value is used instead of the original value.
2284
+ If it returns what it received, then the structure is not modified.
2285
+ If it returns undefined then the member is deleted.
2286
+
2287
+ Example:
2288
+
2289
+ // Parse the text. Values that look like ISO date strings will
2290
+ // be converted to Date objects.
2291
+
2292
+ myData = JSON.parse(text, function (key, value) {
2293
+ var a;
2294
+ if (typeof value === 'string') {
2295
+ a =
2296
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
2297
+ if (a) {
2298
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
2299
+ +a[5], +a[6]));
2300
+ }
2301
+ }
2302
+ return value;
2303
+ });
2304
+
2305
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
2306
+ var d;
2307
+ if (typeof value === 'string' &&
2308
+ value.slice(0, 5) === 'Date(' &&
2309
+ value.slice(-1) === ')') {
2310
+ d = new Date(value.slice(5, -1));
2311
+ if (d) {
2312
+ return d;
2313
+ }
2314
+ }
2315
+ return value;
2316
+ });
2317
+
2318
+
2319
+ This is a reference implementation. You are free to copy, modify, or
2320
+ redistribute.
2321
+ */
2322
+
2323
+ /*jslint evil: true, regexp: true */
2324
+
2325
+ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
2326
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
2327
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
2328
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
2329
+ test, toJSON, toString, valueOf
2330
+ */
2331
+
2332
+
2333
+ // Create a JSON object only if one does not already exist. We create the
2334
+ // methods in a closure to avoid creating global variables.
2335
+
2336
+ if (typeof JSON !== 'object') {
2337
+ JSON = {};
2338
+ }
2339
+
2340
+ (function () {
2341
+ 'use strict';
2342
+
2343
+ function f(n) {
2344
+ // Format integers to have at least two digits.
2345
+ return n < 10 ? '0' + n : n;
2346
+ }
2347
+
2348
+ if (typeof Date.prototype.toJSON !== 'function') {
2349
+
2350
+ Date.prototype.toJSON = function () {
2351
+
2352
+ return isFinite(this.valueOf())
2353
+ ? this.getUTCFullYear() + '-' +
2354
+ f(this.getUTCMonth() + 1) + '-' +
2355
+ f(this.getUTCDate()) + 'T' +
2356
+ f(this.getUTCHours()) + ':' +
2357
+ f(this.getUTCMinutes()) + ':' +
2358
+ f(this.getUTCSeconds()) + 'Z'
2359
+ : null;
2360
+ };
2361
+
2362
+ String.prototype.toJSON =
2363
+ Number.prototype.toJSON =
2364
+ Boolean.prototype.toJSON = function () {
2365
+ return this.valueOf();
2366
+ };
2367
+ }
2368
+
2369
+ var cx,
2370
+ escapable,
2371
+ gap,
2372
+ indent,
2373
+ meta,
2374
+ rep;
2375
+
2376
+
2377
+ function quote(string) {
2378
+
2379
+ // If the string contains no control characters, no quote characters, and no
2380
+ // backslash characters, then we can safely slap some quotes around it.
2381
+ // Otherwise we must also replace the offending characters with safe escape
2382
+ // sequences.
2383
+
2384
+ escapable.lastIndex = 0;
2385
+ return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
2386
+ var c = meta[a];
2387
+ return typeof c === 'string'
2388
+ ? c
2389
+ : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
2390
+ }) + '"' : '"' + string + '"';
2391
+ }
2392
+
2393
+
2394
+ function str(key, holder) {
2395
+
2396
+ // Produce a string from holder[key].
2397
+
2398
+ var i, // The loop counter.
2399
+ k, // The member key.
2400
+ v, // The member value.
2401
+ length,
2402
+ mind = gap,
2403
+ partial,
2404
+ value = holder[key];
2405
+
2406
+ // If the value has a toJSON method, call it to obtain a replacement value.
2407
+
2408
+ if (value && typeof value === 'object' &&
2409
+ typeof value.toJSON === 'function') {
2410
+ value = value.toJSON(key);
2411
+ }
2412
+
2413
+ // If we were called with a replacer function, then call the replacer to
2414
+ // obtain a replacement value.
2415
+
2416
+ if (typeof rep === 'function') {
2417
+ value = rep.call(holder, key, value);
2418
+ }
2419
+
2420
+ // What happens next depends on the value's type.
2421
+
2422
+ switch (typeof value) {
2423
+ case 'string':
2424
+ return quote(value);
2425
+
2426
+ case 'number':
2427
+
2428
+ // JSON numbers must be finite. Encode non-finite numbers as null.
2429
+
2430
+ return isFinite(value) ? String(value) : 'null';
2431
+
2432
+ case 'boolean':
2433
+ case 'null':
2434
+
2435
+ // If the value is a boolean or null, convert it to a string. Note:
2436
+ // typeof null does not produce 'null'. The case is included here in
2437
+ // the remote chance that this gets fixed someday.
2438
+
2439
+ return String(value);
2440
+
2441
+ // If the type is 'object', we might be dealing with an object or an array or
2442
+ // null.
2443
+
2444
+ case 'object':
2445
+
2446
+ // Due to a specification blunder in ECMAScript, typeof null is 'object',
2447
+ // so watch out for that case.
2448
+
2449
+ if (!value) {
2450
+ return 'null';
2451
+ }
2452
+
2453
+ // Make an array to hold the partial results of stringifying this object value.
2454
+
2455
+ gap += indent;
2456
+ partial = [];
2457
+
2458
+ // Is the value an array?
2459
+
2460
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
2461
+
2462
+ // The value is an array. Stringify every element. Use null as a placeholder
2463
+ // for non-JSON values.
2464
+
2465
+ length = value.length;
2466
+ for (i = 0; i < length; i += 1) {
2467
+ partial[i] = str(i, value) || 'null';
2468
+ }
2469
+
2470
+ // Join all of the elements together, separated with commas, and wrap them in
2471
+ // brackets.
2472
+
2473
+ v = partial.length === 0
2474
+ ? '[]'
2475
+ : gap
2476
+ ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
2477
+ : '[' + partial.join(',') + ']';
2478
+ gap = mind;
2479
+ return v;
2480
+ }
2481
+
2482
+ // If the replacer is an array, use it to select the members to be stringified.
2483
+
2484
+ if (rep && typeof rep === 'object') {
2485
+ length = rep.length;
2486
+ for (i = 0; i < length; i += 1) {
2487
+ if (typeof rep[i] === 'string') {
2488
+ k = rep[i];
2489
+ v = str(k, value);
2490
+ if (v) {
2491
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
2492
+ }
2493
+ }
2494
+ }
2495
+ } else {
2496
+
2497
+ // Otherwise, iterate through all of the keys in the object.
2498
+
2499
+ for (k in value) {
2500
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
2501
+ v = str(k, value);
2502
+ if (v) {
2503
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+
2509
+ // Join all of the member texts together, separated with commas,
2510
+ // and wrap them in braces.
2511
+
2512
+ v = partial.length === 0
2513
+ ? '{}'
2514
+ : gap
2515
+ ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
2516
+ : '{' + partial.join(',') + '}';
2517
+ gap = mind;
2518
+ return v;
2519
+ }
2520
+ }
2521
+
2522
+ // If the JSON object does not yet have a stringify method, give it one.
2523
+
2524
+ if (typeof JSON.stringify !== 'function') {
2525
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
2526
+ meta = { // table of character substitutions
2527
+ '\b': '\\b',
2528
+ '\t': '\\t',
2529
+ '\n': '\\n',
2530
+ '\f': '\\f',
2531
+ '\r': '\\r',
2532
+ '"' : '\\"',
2533
+ '\\': '\\\\'
2534
+ };
2535
+ JSON.stringify = function (value, replacer, space) {
2536
+
2537
+ // The stringify method takes a value and an optional replacer, and an optional
2538
+ // space parameter, and returns a JSON text. The replacer can be a function
2539
+ // that can replace values, or an array of strings that will select the keys.
2540
+ // A default replacer method can be provided. Use of the space parameter can
2541
+ // produce text that is more easily readable.
2542
+
2543
+ var i;
2544
+ gap = '';
2545
+ indent = '';
2546
+
2547
+ // If the space parameter is a number, make an indent string containing that
2548
+ // many spaces.
2549
+
2550
+ if (typeof space === 'number') {
2551
+ for (i = 0; i < space; i += 1) {
2552
+ indent += ' ';
2553
+ }
2554
+
2555
+ // If the space parameter is a string, it will be used as the indent string.
2556
+
2557
+ } else if (typeof space === 'string') {
2558
+ indent = space;
2559
+ }
2560
+
2561
+ // If there is a replacer, it must be a function or an array.
2562
+ // Otherwise, throw an error.
2563
+
2564
+ rep = replacer;
2565
+ if (replacer && typeof replacer !== 'function' &&
2566
+ (typeof replacer !== 'object' ||
2567
+ typeof replacer.length !== 'number')) {
2568
+ throw new Error('JSON.stringify');
2569
+ }
2570
+
2571
+ // Make a fake root object containing our value under the key of ''.
2572
+ // Return the result of stringifying the value.
2573
+
2574
+ return str('', {'': value});
2575
+ };
2576
+ }
2577
+
2578
+
2579
+ // If the JSON object does not yet have a parse method, give it one.
2580
+
2581
+ if (typeof JSON.parse !== 'function') {
2582
+ cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
2583
+ JSON.parse = function (text, reviver) {
2584
+
2585
+ // The parse method takes a text and an optional reviver function, and returns
2586
+ // a JavaScript value if the text is a valid JSON text.
2587
+
2588
+ var j;
2589
+
2590
+ function walk(holder, key) {
2591
+
2592
+ // The walk method is used to recursively walk the resulting structure so
2593
+ // that modifications can be made.
2594
+
2595
+ var k, v, value = holder[key];
2596
+ if (value && typeof value === 'object') {
2597
+ for (k in value) {
2598
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
2599
+ v = walk(value, k);
2600
+ if (v !== undefined) {
2601
+ value[k] = v;
2602
+ } else {
2603
+ delete value[k];
2604
+ }
2605
+ }
2606
+ }
2607
+ }
2608
+ return reviver.call(holder, key, value);
2609
+ }
2610
+
2611
+
2612
+ // Parsing happens in four stages. In the first stage, we replace certain
2613
+ // Unicode characters with escape sequences. JavaScript handles many characters
2614
+ // incorrectly, either silently deleting them, or treating them as line endings.
2615
+
2616
+ text = String(text);
2617
+ cx.lastIndex = 0;
2618
+ if (cx.test(text)) {
2619
+ text = text.replace(cx, function (a) {
2620
+ return '\\u' +
2621
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
2622
+ });
2623
+ }
2624
+
2625
+ // In the second stage, we run the text against regular expressions that look
2626
+ // for non-JSON patterns. We are especially concerned with '()' and 'new'
2627
+ // because they can cause invocation, and '=' because it can cause mutation.
2628
+ // But just to be safe, we want to reject all unexpected forms.
2629
+
2630
+ // We split the second stage into 4 regexp operations in order to work around
2631
+ // crippling inefficiencies in IE's and Safari's regexp engines. First we
2632
+ // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
2633
+ // replace all simple value tokens with ']' characters. Third, we delete all
2634
+ // open brackets that follow a colon or comma or that begin the text. Finally,
2635
+ // we look to see that the remaining characters are only whitespace or ']' or
2636
+ // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
2637
+
2638
+ if (/^[\],:{}\s]*$/
2639
+ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
2640
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
2641
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
2642
+
2643
+ // In the third stage we use the eval function to compile the text into a
2644
+ // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
2645
+ // in JavaScript: it can begin a block or an object literal. We wrap the text
2646
+ // in parens to eliminate the ambiguity.
2647
+
2648
+ j = eval('(' + text + ')');
2649
+
2650
+ // In the optional fourth stage, we recursively walk the new structure, passing
2651
+ // each name/value pair to a reviver function for possible transformation.
2652
+
2653
+ return typeof reviver === 'function'
2654
+ ? walk({'': j}, '')
2655
+ : j;
2656
+ }
2657
+
2658
+ // If the text is not JSON parseable, then a SyntaxError is thrown.
2659
+
2660
+ throw new SyntaxError('JSON.parse');
2661
+ };
2662
+ }
2663
+ }());
2664
+
2665
+ /* global jQuery */
2666
+ (function ($) {
2667
+
2668
+ $.algolia = {};
2669
+ $.algolia.Client = function(applicationID, apiKey, options) {
2670
+ options = options || {};
2671
+ options.jQuery = {
2672
+ '$': $
2673
+ };
2674
+ options._ua = 'Algolia for jQuery ' + window.ALGOLIA_VERSION;
2675
+ return new AlgoliaSearch(applicationID, apiKey, options);
2676
+ };
2677
+
2678
+ }(jQuery));