algoliasearch-rails 1.11.15 → 1.11.16

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