yc-algoliasearch-rails 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
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,2663 @@
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
+ }());