algolia-2.5.2.js 88 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039
  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. var ALGOLIA_VERSION = '2.5.2';
  24. /*
  25. * Copyright (c) 2013 Algolia
  26. * http://www.algolia.com/
  27. *
  28. * Permission is hereby granted, free of charge, to any person obtaining a copy
  29. * of this software and associated documentation files (the "Software"), to deal
  30. * in the Software without restriction, including without limitation the rights
  31. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  32. * copies of the Software, and to permit persons to whom the Software is
  33. * furnished to do so, subject to the following conditions:
  34. *
  35. * The above copyright notice and this permission notice shall be included in
  36. * all copies or substantial portions of the Software.
  37. *
  38. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  39. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  40. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  41. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  42. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  43. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  44. * THE SOFTWARE.
  45. */
  46. /*
  47. * Algolia Search library initialization
  48. * @param applicationID the application ID you have in your admin interface
  49. * @param apiKey a valid API key for the service
  50. * @param method specify if the protocol used is http or https (http by default to make the first search query faster).
  51. * You need to use https is you are doing something else than just search queries.
  52. * @param resolveDNS let you disable first empty query that is launch to warmup the service
  53. * @param hostsArray (optionnal) the list of hosts that you have received for the service
  54. */
  55. var AlgoliaSearch = function(applicationID, apiKey, method, resolveDNS, hostsArray) {
  56. var self = this;
  57. this.applicationID = applicationID;
  58. this.apiKey = apiKey;
  59. if (this._isUndefined(hostsArray)) {
  60. hostsArray = [applicationID + '-1.algolia.io',
  61. applicationID + '-2.algolia.io',
  62. applicationID + '-3.algolia.io'];
  63. }
  64. this.hosts = [];
  65. // Add hosts in random order
  66. for (var i = 0; i < hostsArray.length; ++i) {
  67. if (Math.random() > 0.5) {
  68. this.hosts.reverse();
  69. }
  70. if (this._isUndefined(method) || method == null) {
  71. this.hosts.push(('https:' == document.location.protocol ? 'https' : 'http') + '://' + hostsArray[i]);
  72. } else if (method === 'https' || method === 'HTTPS') {
  73. this.hosts.push('https://' + hostsArray[i]);
  74. } else {
  75. this.hosts.push('http://' + hostsArray[i]);
  76. }
  77. }
  78. if (Math.random() > 0.5) {
  79. this.hosts.reverse();
  80. }
  81. // resolve DNS + check CORS support (JSONP fallback)
  82. this.jsonp = null;
  83. this.jsonpWait = 0;
  84. this._jsonRequest({
  85. method: 'GET',
  86. url: '/1/isalive',
  87. callback: function(success, content) {
  88. self.jsonp = !success;
  89. }
  90. });
  91. this.extraHeaders = [];
  92. };
  93. function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) {
  94. function _getHitExplanationForOneAttr_recurse(obj, foundWords) {
  95. var res = [];
  96. if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) {
  97. var match = false;
  98. for (var j = 0; j < obj.matchedWords.length; ++j) {
  99. var word = obj.matchedWords[j];
  100. if (!(word in foundWords)) {
  101. foundWords[word] = 1;
  102. match = true;
  103. }
  104. }
  105. if (match) {
  106. res.push(obj.value);
  107. }
  108. } else if (Object.prototype.toString.call(obj) === '[object Array]') {
  109. for (var i = 0; i < obj.length; ++i) {
  110. var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords);
  111. res = res.concat(array);
  112. }
  113. } else if (typeof obj === 'object') {
  114. for (var prop in obj) {
  115. if (obj.hasOwnProperty(prop)){
  116. res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords));
  117. }
  118. }
  119. }
  120. return res;
  121. }
  122. function _getHitExplanationForOneAttr(hit, foundWords, attr) {
  123. var base = hit._highlightResult || hit;
  124. if (attr.indexOf('.') === -1) {
  125. if (attr in base) {
  126. return _getHitExplanationForOneAttr_recurse(base[attr], foundWords);
  127. }
  128. return [];
  129. }
  130. var array = attr.split('.');
  131. var obj = base;
  132. for (var i = 0; i < array.length; ++i) {
  133. if (Object.prototype.toString.call(obj) === '[object Array]') {
  134. var res = [];
  135. for (var j = 0; j < obj.length; ++j) {
  136. res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.')));
  137. }
  138. return res;
  139. }
  140. if (array[i] in obj) {
  141. obj = obj[array[i]];
  142. } else {
  143. return [];
  144. }
  145. }
  146. return _getHitExplanationForOneAttr_recurse(obj, foundWords);
  147. }
  148. var res = {};
  149. var foundWords = {};
  150. var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute);
  151. res.title = (title.length > 0) ? title[0] : '';
  152. res.subtitles = [];
  153. if (typeof otherAttributes !== 'undefined') {
  154. for (var i = 0; i < otherAttributes.length; ++i) {
  155. var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]);
  156. for (var j = 0; j < attr.length; ++j) {
  157. res.subtitles.push({ attr: otherAttributes[i], value: attr[j] });
  158. }
  159. }
  160. }
  161. return res;
  162. }
  163. AlgoliaSearch.prototype = {
  164. /*
  165. * Delete an index
  166. *
  167. * @param indexName the name of index to delete
  168. * @param callback the result callback with two arguments
  169. * success: boolean set to true if the request was successfull
  170. * content: the server answer that contains the task ID
  171. */
  172. deleteIndex: function(indexName, callback) {
  173. this._jsonRequest({ method: 'DELETE',
  174. url: '/1/indexes/' + encodeURIComponent(indexName),
  175. callback: callback });
  176. },
  177. /**
  178. * Move an existing index.
  179. * @param srcIndexName the name of index to copy.
  180. * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
  181. * @param callback the result callback with two arguments
  182. * success: boolean set to true if the request was successfull
  183. * content: the server answer that contains the task ID
  184. */
  185. moveIndex: function(srcIndexName, dstIndexName, callback) {
  186. var postObj = {operation: 'move', destination: dstIndexName};
  187. this._jsonRequest({ method: 'POST',
  188. url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
  189. body: postObj,
  190. callback: callback });
  191. },
  192. /**
  193. * Copy an existing index.
  194. * @param srcIndexName the name of index to copy.
  195. * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
  196. * @param callback the result callback with two arguments
  197. * success: boolean set to true if the request was successfull
  198. * content: the server answer that contains the task ID
  199. */
  200. copyIndex: function(srcIndexName, dstIndexName, callback) {
  201. var postObj = {operation: 'copy', destination: dstIndexName};
  202. this._jsonRequest({ method: 'POST',
  203. url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
  204. body: postObj,
  205. callback: callback });
  206. },
  207. /**
  208. * Return last log entries.
  209. * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
  210. * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
  211. * @param callback the result callback with two arguments
  212. * success: boolean set to true if the request was successfull
  213. * content: the server answer that contains the task ID
  214. */
  215. getLogs: function(callback, offset, length) {
  216. if (this._isUndefined(offset)) {
  217. offset = 0;
  218. }
  219. if (this._isUndefined(length)) {
  220. length = 10;
  221. }
  222. this._jsonRequest({ method: 'GET',
  223. url: '/1/logs?offset=' + offset + '&length=' + length,
  224. callback: callback });
  225. },
  226. /*
  227. * List all existing indexes
  228. *
  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 with index list or error description if success is false.
  232. */
  233. listIndexes: function(callback) {
  234. this._jsonRequest({ method: 'GET',
  235. url: '/1/indexes',
  236. callback: callback });
  237. },
  238. /*
  239. * Get the index object initialized
  240. *
  241. * @param indexName the name of index
  242. * @param callback the result callback with one argument (the Index instance)
  243. */
  244. initIndex: function(indexName) {
  245. return new this.Index(this, indexName);
  246. },
  247. /*
  248. * List all existing user keys with their associated ACLs
  249. *
  250. * @param callback the result callback with two arguments
  251. * success: boolean set to true if the request was successfull
  252. * content: the server answer with user keys list or error description if success is false.
  253. */
  254. listUserKeys: function(callback) {
  255. this._jsonRequest({ method: 'GET',
  256. url: '/1/keys',
  257. callback: callback });
  258. },
  259. /*
  260. * Get ACL of a user key
  261. *
  262. * @param callback the result callback with two arguments
  263. * success: boolean set to true if the request was successfull
  264. * content: the server answer with user keys list or error description if success is false.
  265. */
  266. getUserKeyACL: function(key, callback) {
  267. this._jsonRequest({ method: 'GET',
  268. url: '/1/keys/' + key,
  269. callback: callback });
  270. },
  271. /*
  272. * Delete an existing user key
  273. *
  274. * @param callback the result callback with two arguments
  275. * success: boolean set to true if the request was successfull
  276. * content: the server answer with user keys list or error description if success is false.
  277. */
  278. deleteUserKey: function(key, callback) {
  279. this._jsonRequest({ method: 'DELETE',
  280. url: '/1/keys/' + key,
  281. callback: callback });
  282. },
  283. /*
  284. * Add an existing user key
  285. *
  286. * @param acls the list of ACL for this key. Defined by an array of strings that
  287. * can contains the following values:
  288. * - search: allow to search (https and http)
  289. * - addObject: allows to add/update an object in the index (https only)
  290. * - deleteObject : allows to delete an existing object (https only)
  291. * - deleteIndex : allows to delete index content (https only)
  292. * - settings : allows to get index settings (https only)
  293. * - editSettings : allows to change index settings (https only)
  294. * @param callback the result callback with two arguments
  295. * success: boolean set to true if the request was successfull
  296. * content: the server answer with user keys list or error description if success is false.
  297. */
  298. addUserKey: function(acls, callback) {
  299. var aclsObject = {};
  300. aclsObject.acl = acls;
  301. this._jsonRequest({ method: 'POST',
  302. url: '/1/keys',
  303. body: aclsObject,
  304. callback: callback });
  305. },
  306. /*
  307. * Add an existing user key
  308. *
  309. * @param acls the list of ACL for this key. Defined by an array of strings that
  310. * can contains the following values:
  311. * - search: allow to search (https and http)
  312. * - addObject: allows to add/update an object in the index (https only)
  313. * - deleteObject : allows to delete an existing object (https only)
  314. * - deleteIndex : allows to delete index content (https only)
  315. * - settings : allows to get index settings (https only)
  316. * - editSettings : allows to change index settings (https only)
  317. * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
  318. * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
  319. * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
  320. * @param callback the result callback with two arguments
  321. * success: boolean set to true if the request was successfull
  322. * content: the server answer with user keys list or error description if success is false.
  323. */
  324. addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
  325. var indexObj = this;
  326. var aclsObject = {};
  327. aclsObject.acl = acls;
  328. aclsObject.validity = validity;
  329. aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
  330. aclsObject.maxHitsPerQuery = maxHitsPerQuery;
  331. this._jsonRequest({ method: 'POST',
  332. url: '/1/indexes/' + indexObj.indexName + '/keys',
  333. body: aclsObject,
  334. callback: callback });
  335. },
  336. /**
  337. * Set the extra security tagFilters header
  338. * @param {string|array} tags The list of tags defining the current security filters
  339. */
  340. setSecurityTags: function(tags) {
  341. if (Object.prototype.toString.call(tags) === '[object Array]') {
  342. var strTags = [];
  343. for (var i = 0; i < tags.length; ++i) {
  344. if (Object.prototype.toString.call(tags[i]) === '[object Array]') {
  345. var oredTags = [];
  346. for (var j = 0; j < tags[i].length; ++j) {
  347. oredTags.push(tags[i][j]);
  348. }
  349. strTags.push('(' + oredTags.join(',') + ')');
  350. } else {
  351. strTags.push(tags[i]);
  352. }
  353. }
  354. tags = strTags.join(',');
  355. }
  356. this.tagFilters = tags;
  357. },
  358. /**
  359. * Set the extra user token header
  360. * @param {string} userToken The token identifying a uniq user (used to apply rate limits)
  361. */
  362. setUserToken: function(userToken) {
  363. this.userToken = userToken;
  364. },
  365. /*
  366. * Initialize a new batch of search queries
  367. */
  368. startQueriesBatch: function() {
  369. this.batch = [];
  370. },
  371. /*
  372. * Add a search query in the batch
  373. *
  374. * @param query the full text query
  375. * @param args (optional) if set, contains an object with query parameters:
  376. * - attributes: an array of object attribute names to retrieve
  377. * (if not set all attributes are retrieve)
  378. * - attributesToHighlight: an array of object attribute names to highlight
  379. * (if not set indexed attributes are highlighted)
  380. * - minWordSizefor1Typo: the minimum number of characters to accept one typo.
  381. * Defaults to 3.
  382. * - minWordSizefor2Typos: the minimum number of characters to accept two typos.
  383. * Defaults to 7.
  384. * - getRankingInfo: if set, the result hits will contain ranking information in
  385. * _rankingInfo attribute
  386. * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
  387. * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
  388. */
  389. addQueryInBatch: function(indexName, query, args) {
  390. var params = 'query=' + encodeURIComponent(query);
  391. if (!this._isUndefined(args) && args != null) {
  392. params = this._getSearchParams(args, params);
  393. }
  394. this.batch.push({ indexName: indexName, params: params });
  395. },
  396. /*
  397. * Clear all queries in cache
  398. */
  399. clearCache: function() {
  400. this.cache = {};
  401. },
  402. /*
  403. * Launch the batch of queries using XMLHttpRequest.
  404. * (Optimized for browser using a POST query to minimize number of OPTIONS queries)
  405. *
  406. * @param callback the function that will receive results
  407. * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime.
  408. */
  409. sendQueriesBatch: function(callback, delay) {
  410. var as = this;
  411. var params = {requests: [], apiKey: this.apiKey, appID: this.applicationID};
  412. if (this.userToken) {
  413. params['X-Algolia-UserToken'] = this.userToken;
  414. }
  415. if (this.tagFilters) {
  416. params['X-Algolia-TagFilters'] = this.tagFilters;
  417. }
  418. for (var i = 0; i < as.batch.length; ++i) {
  419. params.requests.push(as.batch[i]);
  420. }
  421. window.clearTimeout(as.onDelayTrigger);
  422. if (!this._isUndefined(delay) && delay != null && delay > 0) {
  423. var onDelayTrigger = window.setTimeout( function() {
  424. as._sendQueriesBatch(params, callback);
  425. }, delay);
  426. as.onDelayTrigger = onDelayTrigger;
  427. } else {
  428. this._sendQueriesBatch(params, callback);
  429. }
  430. },
  431. /*
  432. * Index class constructor.
  433. * You should not use this method directly but use initIndex() function
  434. */
  435. Index: function(algoliasearch, indexName) {
  436. this.indexName = indexName;
  437. this.as = algoliasearch;
  438. this.typeAheadArgs = null;
  439. this.typeAheadValueOption = null;
  440. },
  441. setExtraHeader: function(key, value) {
  442. this.extraHeaders.push({ key: key, value: value});
  443. },
  444. _sendQueriesBatch: function(params, callback) {
  445. if (this.jsonp == null) {
  446. var self = this;
  447. this._waitReady(function() { self._sendQueriesBatch(params, callback); });
  448. return;
  449. }
  450. if (this.jsonp) {
  451. var jsonpParams = '';
  452. for (var i = 0; i < params.requests.length; ++i) {
  453. var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params;
  454. jsonpParams += i + '=' + encodeURIComponent(q) + '&';
  455. }
  456. this._jsonRequest({ cache: this.cache,
  457. method: 'GET', jsonp: true,
  458. url: '/1/indexes/*',
  459. body: { params: jsonpParams },
  460. callback: callback });
  461. } else {
  462. this._jsonRequest({ cache: this.cache,
  463. method: 'POST',
  464. url: '/1/indexes/*/queries',
  465. body: params,
  466. callback: callback });
  467. }
  468. },
  469. /*
  470. * Wrapper that try all hosts to maximize the quality of service
  471. */
  472. _jsonRequest: function(opts) {
  473. var self = this;
  474. var callback = opts.callback;
  475. var cache = null;
  476. var cacheID = opts.url;
  477. if (!this._isUndefined(opts.body)) {
  478. cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
  479. }
  480. if (!this._isUndefined(opts.cache)) {
  481. cache = opts.cache;
  482. if (!this._isUndefined(cache[cacheID])) {
  483. if (!this._isUndefined(callback)) {
  484. setTimeout(function () { callback(true, cache[cacheID]); }, 1);
  485. }
  486. return;
  487. }
  488. }
  489. var impl = function(position) {
  490. var idx = 0;
  491. if (!self._isUndefined(position)) {
  492. idx = position;
  493. }
  494. if (self.hosts.length <= idx) {
  495. if (!self._isUndefined(callback)) {
  496. callback(false, { message: 'Cannot contact server'});
  497. }
  498. return;
  499. }
  500. opts.callback = function(retry, success, res, body) {
  501. if (!success && !self._isUndefined(body)) {
  502. if (window.console) { console.log('Error: ' + body.message); }
  503. }
  504. if (success && !self._isUndefined(opts.cache)) {
  505. cache[cacheID] = body;
  506. }
  507. if (!success && retry && (idx + 1) < self.hosts.length) {
  508. impl(idx + 1);
  509. } else {
  510. if (!self._isUndefined(callback)) {
  511. callback(success, body);
  512. }
  513. }
  514. };
  515. opts.hostname = self.hosts[idx];
  516. self._jsonRequestByHost(opts);
  517. };
  518. impl();
  519. },
  520. _jsonRequestByHost: function(opts) {
  521. var self = this;
  522. var url = opts.hostname + opts.url;
  523. if (this.jsonp) {
  524. if (!opts.jsonp) {
  525. opts.callback(true, false, null, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' });
  526. return;
  527. }
  528. this.jsonpCounter = this.jsonpCounter || 0;
  529. this.jsonpCounter += 1;
  530. var cb = 'algoliaJSONP_' + this.jsonpCounter;
  531. window[cb] = function(data) {
  532. opts.callback(false, true, null, data);
  533. try { delete window[cb]; } catch (e) { window[cb] = undefined; }
  534. };
  535. var script = document.createElement('script');
  536. script.type = 'text/javascript';
  537. script.src = url + '?callback=' + cb + ',' + this.applicationID + ',' + this.apiKey;
  538. if (opts['X-Algolia-TagFilters']) {
  539. script.src += '&X-Algolia-TagFilters=' + opts['X-Algolia-TagFilters'];
  540. }
  541. if (opts['X-Algolia-UserToken']) {
  542. script.src += '&X-Algolia-UserToken=' + opts['X-Algolia-UserToken'];
  543. }
  544. if (opts.body && opts.body.params) {
  545. script.src += '&' + opts.body.params;
  546. }
  547. var head = document.getElementsByTagName('head')[0];
  548. script.onerror = function() {
  549. opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
  550. head.removeChild(script);
  551. try { delete window[cb]; } catch (e) { window[cb] = undefined; }
  552. };
  553. var done = false;
  554. script.onload = script.onreadystatechange = function() {
  555. if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {
  556. done = true;
  557. if (typeof window[cb + '_loaded'] === 'undefined') {
  558. opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
  559. try { delete window[cb]; } catch (e) { window[cb] = undefined; }
  560. } else {
  561. try { delete window[cb + '_loaded']; } catch (e) { window[cb + '_loaded'] = undefined; }
  562. }
  563. script.onload = script.onreadystatechange = null; // Handle memory leak in IE
  564. head.removeChild(script);
  565. }
  566. };
  567. head.appendChild(script);
  568. } else {
  569. var body = null;
  570. if (!this._isUndefined(opts.body)) {
  571. body = JSON.stringify(opts.body);
  572. }
  573. var xmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() : {};
  574. if ('withCredentials' in xmlHttp) {
  575. xmlHttp.open(opts.method, url , true);
  576. xmlHttp.setRequestHeader('X-Algolia-API-Key', this.apiKey);
  577. xmlHttp.setRequestHeader('X-Algolia-Application-Id', this.applicationID);
  578. for (var i = 0; i < this.extraHeaders.length; ++i) {
  579. xmlHttp.setRequestHeader(this.extraHeaders[i].key, this.extraHeaders[i].value);
  580. }
  581. if (body != null) {
  582. xmlHttp.setRequestHeader('Content-type', 'application/json');
  583. }
  584. } else if (typeof XDomainRequest != 'undefined') {
  585. // Handle IE8/IE9
  586. // XDomainRequest only exists in IE, and is IE's way of making CORS requests.
  587. xmlHttp = new XDomainRequest();
  588. xmlHttp.open(opts.method, url);
  589. } else {
  590. // very old browser, not supported
  591. if (window.console) { console.log('Your browser is too old to support CORS requests'); }
  592. opts.callback(false, false, null, { 'message': 'CORS not supported' });
  593. return;
  594. }
  595. xmlHttp.send(body);
  596. xmlHttp.onload = function(event) {
  597. if (!self._isUndefined(event) && event.target != null) {
  598. var retry = (event.target.status === 0 || event.target.status === 503);
  599. var success = (event.target.status === 200 || event.target.status === 201);
  600. opts.callback(retry, success, event.target, event.target.response != null ? JSON.parse(event.target.response) : null);
  601. } else {
  602. opts.callback(false, true, event, JSON.parse(xmlHttp.responseText));
  603. }
  604. };
  605. xmlHttp.onerror = function(event) {
  606. opts.callback(true, false, null, { 'message': 'Could not connect to host', 'error': event } );
  607. };
  608. }
  609. },
  610. /**
  611. * Wait until JSONP flag has been set to perform the first query
  612. */
  613. _waitReady: function(cb) {
  614. if (this.jsonp == null) {
  615. this.jsonpWait += 100;
  616. if (this.jsonpWait > 2000) {
  617. this.jsonp = true;
  618. }
  619. setTimeout(cb, 100);
  620. }
  621. },
  622. /*
  623. * Transform search param object in query string
  624. */
  625. _getSearchParams: function(args, params) {
  626. if (this._isUndefined(args) || args == null) {
  627. return params;
  628. }
  629. for (var key in args) {
  630. if (key != null && args.hasOwnProperty(key)) {
  631. params += (params.length === 0) ? '?' : '&';
  632. params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]);
  633. }
  634. }
  635. return params;
  636. },
  637. _isUndefined: function(obj) {
  638. return obj === void 0;
  639. },
  640. /// internal attributes
  641. applicationID: null,
  642. apiKey: null,
  643. tagFilters: null,
  644. userToken: null,
  645. hosts: [],
  646. cache: {},
  647. extraHeaders: []
  648. };
  649. /*
  650. * Contains all the functions related to one index
  651. * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object
  652. */
  653. AlgoliaSearch.prototype.Index.prototype = {
  654. /*
  655. * Clear all queries in cache
  656. */
  657. clearCache: function() {
  658. this.cache = {};
  659. },
  660. /*
  661. * Add an object in this index
  662. *
  663. * @param content contains the javascript object to add inside the index
  664. * @param callback (optional) the result callback with two arguments:
  665. * success: boolean set to true if the request was successfull
  666. * content: the server answer that contains 3 elements: createAt, taskId and objectID
  667. * @param objectID (optional) an objectID you want to attribute to this object
  668. * (if the attribute already exist the old object will be overwrite)
  669. */
  670. addObject: function(content, callback, objectID) {
  671. var indexObj = this;
  672. if (this.as._isUndefined(objectID)) {
  673. this.as._jsonRequest({ method: 'POST',
  674. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName),
  675. body: content,
  676. callback: callback });
  677. } else {
  678. this.as._jsonRequest({ method: 'PUT',
  679. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
  680. body: content,
  681. callback: callback });
  682. }
  683. },
  684. /*
  685. * Add several objects
  686. *
  687. * @param objects contains an array of objects to add
  688. * @param callback (optional) the result callback with two arguments:
  689. * success: boolean set to true if the request was successfull
  690. * content: the server answer that updateAt and taskID
  691. */
  692. addObjects: function(objects, callback) {
  693. var indexObj = this;
  694. var postObj = {requests:[]};
  695. for (var i = 0; i < objects.length; ++i) {
  696. var request = { action: 'addObject',
  697. body: objects[i] };
  698. postObj.requests.push(request);
  699. }
  700. this.as._jsonRequest({ method: 'POST',
  701. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
  702. body: postObj,
  703. callback: callback });
  704. },
  705. /*
  706. * Get an object from this index
  707. *
  708. * @param objectID the unique identifier of the object to retrieve
  709. * @param callback (optional) the result callback with two arguments
  710. * success: boolean set to true if the request was successfull
  711. * content: the object to retrieve or the error message if a failure occured
  712. * @param attributes (optional) if set, contains the array of attribute names to retrieve
  713. */
  714. getObject: function(objectID, callback, attributes) {
  715. if (this.as.jsonp == null) {
  716. var self = this;
  717. this.as._waitReady(function() { self.getObject(objectID, callback, attributes); });
  718. return;
  719. }
  720. var indexObj = this;
  721. var params = '';
  722. if (!this.as._isUndefined(attributes)) {
  723. params = '?attributes=';
  724. for (var i = 0; i < attributes.length; ++i) {
  725. if (i !== 0) {
  726. params += ',';
  727. }
  728. params += attributes[i];
  729. }
  730. }
  731. this.as._jsonRequest({ method: 'GET', jsonp: true,
  732. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params,
  733. callback: callback });
  734. },
  735. /*
  736. * Update partially an object (only update attributes passed in argument)
  737. *
  738. * @param partialObject contains the javascript attributes to override, the
  739. * object must contains an objectID attribute
  740. * @param callback (optional) the result callback with two arguments:
  741. * success: boolean set to true if the request was successfull
  742. * content: the server answer that contains 3 elements: createAt, taskId and objectID
  743. */
  744. partialUpdateObject: function(partialObject, callback) {
  745. var indexObj = this;
  746. this.as._jsonRequest({ method: 'POST',
  747. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial',
  748. body: partialObject,
  749. callback: callback });
  750. },
  751. /*
  752. * Partially Override the content of several objects
  753. *
  754. * @param objects contains an array of objects to update (each object must contains a objectID attribute)
  755. * @param callback (optional) the result callback with two arguments:
  756. * success: boolean set to true if the request was successfull
  757. * content: the server answer that updateAt and taskID
  758. */
  759. partialUpdateObjects: function(objects, callback) {
  760. var indexObj = this;
  761. var postObj = {requests:[]};
  762. for (var i = 0; i < objects.length; ++i) {
  763. var request = { action: 'partialUpdateObject',
  764. objectID: objects[i].objectID,
  765. body: objects[i] };
  766. postObj.requests.push(request);
  767. }
  768. this.as._jsonRequest({ method: 'POST',
  769. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
  770. body: postObj,
  771. callback: callback });
  772. },
  773. /*
  774. * Override the content of object
  775. *
  776. * @param object contains the javascript object to save, the object must contains an objectID attribute
  777. * @param callback (optional) the result callback with two arguments:
  778. * success: boolean set to true if the request was successfull
  779. * content: the server answer that updateAt and taskID
  780. */
  781. saveObject: function(object, callback) {
  782. var indexObj = this;
  783. this.as._jsonRequest({ method: 'PUT',
  784. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID),
  785. body: object,
  786. callback: callback });
  787. },
  788. /*
  789. * Override the content of several objects
  790. *
  791. * @param objects contains an array of objects to update (each object must contains a objectID attribute)
  792. * @param callback (optional) the result callback with two arguments:
  793. * success: boolean set to true if the request was successfull
  794. * content: the server answer that updateAt and taskID
  795. */
  796. saveObjects: function(objects, callback) {
  797. var indexObj = this;
  798. var postObj = {requests:[]};
  799. for (var i = 0; i < objects.length; ++i) {
  800. var request = { action: 'updateObject',
  801. objectID: objects[i].objectID,
  802. body: objects[i] };
  803. postObj.requests.push(request);
  804. }
  805. this.as._jsonRequest({ method: 'POST',
  806. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
  807. body: postObj,
  808. callback: callback });
  809. },
  810. /*
  811. * Delete an object from the index
  812. *
  813. * @param objectID the unique identifier of object to delete
  814. * @param callback (optional) the result callback with two arguments:
  815. * success: boolean set to true if the request was successfull
  816. * content: the server answer that contains 3 elements: createAt, taskId and objectID
  817. */
  818. deleteObject: function(objectID, callback) {
  819. if (objectID == null || objectID.length === 0) {
  820. callback(false, { message: 'empty objectID'});
  821. return;
  822. }
  823. var indexObj = this;
  824. this.as._jsonRequest({ method: 'DELETE',
  825. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
  826. callback: callback });
  827. },
  828. /*
  829. * Search inside the index using XMLHttpRequest request (Using a POST query to
  830. * minimize number of OPTIONS queries: Cross-Origin Resource Sharing).
  831. *
  832. * @param query the full text query
  833. * @param callback the result callback with two arguments:
  834. * success: boolean set to true if the request was successfull. If false, the content contains the error.
  835. * content: the server answer that contains the list of results.
  836. * @param args (optional) if set, contains an object with query parameters:
  837. * - page: (integer) Pagination parameter used to select the page to retrieve.
  838. * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
  839. * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
  840. * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
  841. * Attributes are separated with a comma (for example "name,address").
  842. * You can also use a string array encoding (for example ["name","address"]).
  843. * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
  844. * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
  845. * Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]).
  846. * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
  847. * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
  848. * A matchLevel is returned for each highlighted attribute and can contain:
  849. * - full: if all the query terms were found in the attribute,
  850. * - partial: if only some of the query terms were found,
  851. * - none: if none of the query terms were found.
  852. * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
  853. * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
  854. * You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed.
  855. * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
  856. * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
  857. * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
  858. * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
  859. * For example aroundLatLng=47.316669,5.016670).
  860. * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
  861. * (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).
  862. * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
  863. * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
  864. * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
  865. * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
  866. * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
  867. * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
  868. * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
  869. * You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]).
  870. * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
  871. * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
  872. * You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
  873. * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
  874. * - facetFilters: filter the query by a list of facets.
  875. * Facets are separated by commas and each facet is encoded as `attributeName:value`.
  876. * For example: `facetFilters=category:Book,author:John%20Doe`.
  877. * You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`).
  878. * - facets: List of object attributes that you want to use for faceting.
  879. * Attributes are separated with a comma (for example `"category,author"` ).
  880. * You can also use a JSON string array encoding (for example ["category","author"]).
  881. * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
  882. * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
  883. * - queryType: select how the query words are interpreted, it can be one of the following value:
  884. * - prefixAll: all query words are interpreted as prefixes,
  885. * - prefixLast: only the last word is interpreted as a prefix (default behavior),
  886. * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
  887. * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
  888. * The list of words is comma separated.
  889. * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
  890. * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
  891. * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
  892. * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
  893. * one is kept and others are removed.
  894. * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime.
  895. */
  896. search: function(query, callback, args, delay) {
  897. var indexObj = this;
  898. var params = 'query=' + encodeURIComponent(query);
  899. if (!this.as._isUndefined(args) && args != null) {
  900. params = this.as._getSearchParams(args, params);
  901. }
  902. window.clearTimeout(indexObj.onDelayTrigger);
  903. if (!this.as._isUndefined(delay) && delay != null && delay > 0) {
  904. var onDelayTrigger = window.setTimeout( function() {
  905. indexObj._search(params, callback);
  906. }, delay);
  907. indexObj.onDelayTrigger = onDelayTrigger;
  908. } else {
  909. this._search(params, callback);
  910. }
  911. },
  912. /*
  913. * Browse all index content
  914. *
  915. * @param page Pagination parameter used to select the page to retrieve.
  916. * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
  917. * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
  918. */
  919. browse: function(page, callback, hitsPerPage) {
  920. var indexObj = this;
  921. var params = '?page=' + page;
  922. if (!_.isUndefined(hitsPerPage)) {
  923. params += '&hitsPerPage=' + hitsPerPage;
  924. }
  925. this.as._jsonRequest({ method: 'GET',
  926. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params,
  927. callback: callback });
  928. },
  929. /*
  930. * Get a Typeahead.js adapter
  931. * @param searchParams contains an object with query parameters (see search for details)
  932. */
  933. ttAdapter: function(params) {
  934. var self = this;
  935. return function(query, cb) {
  936. self.search(query, function(success, content) {
  937. if (success) {
  938. cb(content.hits);
  939. }
  940. }, params);
  941. };
  942. },
  943. /*
  944. * Wait the publication of a task on the server.
  945. * All server task are asynchronous and you can check with this method that the task is published.
  946. *
  947. * @param taskID the id of the task returned by server
  948. * @param callback the result callback with with two arguments:
  949. * success: boolean set to true if the request was successfull
  950. * content: the server answer that contains the list of results
  951. */
  952. waitTask: function(taskID, callback) {
  953. var indexObj = this;
  954. this.as._jsonRequest({ method: 'GET',
  955. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID,
  956. callback: function(success, body) {
  957. if (success) {
  958. if (body.status === 'published') {
  959. callback(true, body);
  960. } else {
  961. setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100);
  962. }
  963. } else {
  964. callback(false, body);
  965. }
  966. }});
  967. },
  968. /*
  969. * This function deletes the index content. Settings and index specific API keys are kept untouched.
  970. *
  971. * @param callback (optional) the result callback with two arguments
  972. * success: boolean set to true if the request was successfull
  973. * content: the settings object or the error message if a failure occured
  974. */
  975. clearIndex: function(callback) {
  976. var indexObj = this;
  977. this.as._jsonRequest({ method: 'POST',
  978. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear',
  979. callback: callback });
  980. },
  981. /*
  982. * Get settings of this index
  983. *
  984. * @param callback (optional) the result callback with two arguments
  985. * success: boolean set to true if the request was successfull
  986. * content: the settings object or the error message if a failure occured
  987. */
  988. getSettings: function(callback) {
  989. var indexObj = this;
  990. this.as._jsonRequest({ method: 'GET',
  991. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
  992. callback: callback });
  993. },
  994. /*
  995. * Set settings for this index
  996. *
  997. * @param settigns the settings object that can contains :
  998. * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
  999. * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
  1000. * - hitsPerPage: (integer) the number of hits per page (default = 10).
  1001. * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
  1002. * If set to null, all attributes are retrieved.
  1003. * - attributesToHighlight: (array of strings) default list of attributes to highlight.
  1004. * If set to null, all indexed attributes are highlighted.
  1005. * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
  1006. * By default no snippet is computed. If set to null, no snippet is computed.
  1007. * - attributesToIndex: (array of strings) the list of fields you want to index.
  1008. * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
  1009. * This parameter has two important uses:
  1010. * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
  1011. * retrieve it but you don't want to search in the base64 string.
  1012. * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
  1013. * the list will be considered more important than matches in attributes further down the list.
  1014. * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
  1015. * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
  1016. * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
  1017. * 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.
  1018. * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
  1019. * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
  1020. * 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.
  1021. * - ranking: (array of strings) controls the way results are sorted.
  1022. * We have six available criteria:
  1023. * - typo: sort according to number of typos,
  1024. * - geo: sort according to decreassing distance when performing a geo-location based search,
  1025. * - proximity: sort according to the proximity of query words in hits,
  1026. * - attribute: sort according to the order of attributes defined by attributesToIndex,
  1027. * - exact:
  1028. * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
  1029. * 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
  1030. * show starting by the v letter before it.
  1031. * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
  1032. * - custom: sort according to a user defined formula set in **customRanking** attribute.
  1033. * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
  1034. * - customRanking: (array of strings) lets you specify part of the ranking.
  1035. * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
  1036. * For example `"customRanking" => ["desc(population)", "asc(name)"]`
  1037. * - queryType: Select how the query words are interpreted, it can be one of the following value:
  1038. * - prefixAll: all query words are interpreted as prefixes,
  1039. * - prefixLast: only the last word is interpreted as a prefix (default behavior),
  1040. * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
  1041. * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
  1042. * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
  1043. * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
  1044. * @param callback (optional) the result callback with two arguments
  1045. * success: boolean set to true if the request was successfull
  1046. * content: the server answer or the error message if a failure occured
  1047. */
  1048. setSettings: function(settings, callback) {
  1049. var indexObj = this;
  1050. this.as._jsonRequest({ method: 'PUT',
  1051. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
  1052. body: settings,
  1053. callback: callback });
  1054. },
  1055. /*
  1056. * List all existing user keys associated to this index
  1057. *
  1058. * @param callback the result callback with two arguments
  1059. * success: boolean set to true if the request was successfull
  1060. * content: the server answer with user keys list or error description if success is false.
  1061. */
  1062. listUserKeys: function(callback) {
  1063. var indexObj = this;
  1064. this.as._jsonRequest({ method: 'GET',
  1065. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
  1066. callback: callback });
  1067. },
  1068. /*
  1069. * Get ACL of a user key associated to this index
  1070. *
  1071. * @param callback the result callback with two arguments
  1072. * success: boolean set to true if the request was successfull
  1073. * content: the server answer with user keys list or error description if success is false.
  1074. */
  1075. getUserKeyACL: function(key, callback) {
  1076. var indexObj = this;
  1077. this.as._jsonRequest({ method: 'GET',
  1078. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
  1079. callback: callback });
  1080. },
  1081. /*
  1082. * Delete an existing user key associated to this index
  1083. *
  1084. * @param callback the result callback with two arguments
  1085. * success: boolean set to true if the request was successfull
  1086. * content: the server answer with user keys list or error description if success is false.
  1087. */
  1088. deleteUserKey: function(key, callback) {
  1089. var indexObj = this;
  1090. this.as._jsonRequest({ method: 'DELETE',
  1091. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
  1092. callback: callback });
  1093. },
  1094. /*
  1095. * Add an existing user key associated to this index
  1096. *
  1097. * @param acls the list of ACL for this key. Defined by an array of strings that
  1098. * can contains the following values:
  1099. * - search: allow to search (https and http)
  1100. * - addObject: allows to add/update an object in the index (https only)
  1101. * - deleteObject : allows to delete an existing object (https only)
  1102. * - deleteIndex : allows to delete index content (https only)
  1103. * - settings : allows to get index settings (https only)
  1104. * - editSettings : allows to change index settings (https only)
  1105. * @param callback the result callback with two arguments
  1106. * success: boolean set to true if the request was successfull
  1107. * content: the server answer with user keys list or error description if success is false.
  1108. */
  1109. addUserKey: function(acls, callback) {
  1110. var indexObj = this;
  1111. var aclsObject = {};
  1112. aclsObject.acl = acls;
  1113. this.as._jsonRequest({ method: 'POST',
  1114. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
  1115. body: aclsObject,
  1116. callback: callback });
  1117. },
  1118. /*
  1119. * Add an existing user key associated to this index
  1120. *
  1121. * @param acls the list of ACL for this key. Defined by an array of strings that
  1122. * can contains the following values:
  1123. * - search: allow to search (https and http)
  1124. * - addObject: allows to add/update an object in the index (https only)
  1125. * - deleteObject : allows to delete an existing object (https only)
  1126. * - deleteIndex : allows to delete index content (https only)
  1127. * - settings : allows to get index settings (https only)
  1128. * - editSettings : allows to change index settings (https only)
  1129. * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
  1130. * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
  1131. * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
  1132. * @param callback the result callback with two arguments
  1133. * success: boolean set to true if the request was successfull
  1134. * content: the server answer with user keys list or error description if success is false.
  1135. */
  1136. addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
  1137. var indexObj = this;
  1138. var aclsObject = {};
  1139. aclsObject.acl = acls;
  1140. aclsObject.validity = validity;
  1141. aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
  1142. aclsObject.maxHitsPerQuery = maxHitsPerQuery;
  1143. this.as._jsonRequest({ method: 'POST',
  1144. url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
  1145. body: aclsObject,
  1146. callback: callback });
  1147. },
  1148. ///
  1149. /// Internal methods only after this line
  1150. ///
  1151. _search: function(params, callback) {
  1152. if (this.as.jsonp == null) {
  1153. var self = this;
  1154. this.as._waitReady(function() { self._search(params, callback); });
  1155. return;
  1156. }
  1157. var pObj = {params: params, apiKey: this.as.apiKey, appID: this.as.applicationID};
  1158. if (this.as.tagFilters) {
  1159. pObj['X-Algolia-TagFilters'] = this.as.tagFilters;
  1160. }
  1161. if (this.as.userToken) {
  1162. pObj['X-Algolia-UserToken'] = this.as.userToken;
  1163. }
  1164. if (this.as.jsonp) {
  1165. this.as._jsonRequest({ cache: this.cache,
  1166. method: 'GET', jsonp: true,
  1167. url: '/1/indexes/' + encodeURIComponent(this.indexName),
  1168. body: pObj,
  1169. callback: callback });
  1170. } else {
  1171. this.as._jsonRequest({ cache: this.cache,
  1172. method: 'POST',
  1173. url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
  1174. body: pObj,
  1175. callback: callback });
  1176. }
  1177. },
  1178. // internal attributes
  1179. as: null,
  1180. indexName: null,
  1181. cache: {},
  1182. typeAheadArgs: null,
  1183. typeAheadValueOption: null,
  1184. emptyConstructor: function() {}
  1185. };
  1186. /*
  1187. * Copyright (c) 2014 Algolia
  1188. * http://www.algolia.com/
  1189. *
  1190. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1191. * of this software and associated documentation files (the "Software"), to deal
  1192. * in the Software without restriction, including without limitation the rights
  1193. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1194. * copies of the Software, and to permit persons to whom the Software is
  1195. * furnished to do so, subject to the following conditions:
  1196. *
  1197. * The above copyright notice and this permission notice shall be included in
  1198. * all copies or substantial portions of the Software.
  1199. *
  1200. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1201. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1202. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1203. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1204. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1205. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1206. * THE SOFTWARE.
  1207. */
  1208. (function($) {
  1209. var extend = function(out) {
  1210. out = out || {};
  1211. for (var i = 1; i < arguments.length; i++) {
  1212. if (!arguments[i]) {
  1213. continue;
  1214. }
  1215. for (var key in arguments[i]) {
  1216. if (arguments[i].hasOwnProperty(key)) {
  1217. out[key] = arguments[i][key];
  1218. }
  1219. }
  1220. }
  1221. return out;
  1222. };
  1223. /**
  1224. * Algolia Search Helper providing faceting and disjunctive faceting
  1225. * @param {AlgoliaSearch} client an AlgoliaSearch client
  1226. * @param {string} index the index name to query
  1227. * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets
  1228. */
  1229. window.AlgoliaSearchHelper = function(client, index, options) {
  1230. /// Default options
  1231. var defaults = {
  1232. facets: [], // list of facets to compute
  1233. disjunctiveFacets: [], // list of disjunctive facets to compute
  1234. hitsPerPage: 20 // number of hits per page
  1235. };
  1236. this.init(client, index, extend({}, defaults, options));
  1237. };
  1238. AlgoliaSearchHelper.prototype = {
  1239. /**
  1240. * Initialize a new AlgoliaSearchHelper
  1241. * @param {AlgoliaSearch} client an AlgoliaSearch client
  1242. * @param {string} index the index name to query
  1243. * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets
  1244. * @return {AlgoliaSearchHelper}
  1245. */
  1246. init: function(client, index, options) {
  1247. this.client = client;
  1248. this.index = index;
  1249. this.options = options;
  1250. this.page = 0;
  1251. this.refinements = {};
  1252. this.disjunctiveRefinements = {};
  1253. },
  1254. /**
  1255. * Perform a query
  1256. * @param {string} q the user query
  1257. * @param {function} searchCallback the result callback called with two arguments:
  1258. * success: boolean set to true if the request was successfull
  1259. * content: the query answer with an extra 'disjunctiveFacets' attribute
  1260. */
  1261. search: function(q, searchCallback, searchParams) {
  1262. this.q = q;
  1263. this.searchCallback = searchCallback;
  1264. this.searchParams = searchParams || {};
  1265. this.page = this.page || 0;
  1266. this.refinements = this.refinements || {};
  1267. this.disjunctiveRefinements = this.disjunctiveRefinements || {};
  1268. this._search();
  1269. },
  1270. /**
  1271. * Remove all refinements (disjunctive + conjunctive)
  1272. */
  1273. clearRefinements: function() {
  1274. this.disjunctiveRefinements = {};
  1275. this.refinements = {};
  1276. },
  1277. /**
  1278. * Ensure a facet refinement exists
  1279. * @param {string} facet the facet to refine
  1280. * @param {string} value the associated value
  1281. */
  1282. addDisjunctiveRefine: function(facet, value) {
  1283. this.disjunctiveRefinements = this.disjunctiveRefinements || {};
  1284. this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
  1285. this.disjunctiveRefinements[facet][value] = true;
  1286. },
  1287. /**
  1288. * Ensure a facet refinement does not exist
  1289. * @param {string} facet the facet to refine
  1290. * @param {string} value the associated value
  1291. */
  1292. removeDisjunctiveRefine: function(facet, value) {
  1293. this.disjunctiveRefinements = this.disjunctiveRefinements || {};
  1294. this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
  1295. try {
  1296. delete this.disjunctiveRefinements[facet][value];
  1297. } catch (e) {
  1298. this.disjunctiveRefinements[facet][value] = undefined; // IE compat
  1299. }
  1300. },
  1301. /**
  1302. * Ensure a facet refinement exists
  1303. * @param {string} facet the facet to refine
  1304. * @param {string} value the associated value
  1305. */
  1306. addRefine: function(facet, value) {
  1307. var refinement = facet + ':' + value;
  1308. this.refinements = this.refinements || {};
  1309. this.refinements[refinement] = true;
  1310. },
  1311. /**
  1312. * Ensure a facet refinement does not exist
  1313. * @param {string} facet the facet to refine
  1314. * @param {string} value the associated value
  1315. */
  1316. removeRefine: function(facet, value) {
  1317. var refinement = facet + ':' + value;
  1318. this.refinements = this.refinements || {};
  1319. this.refinements[refinement] = false;
  1320. },
  1321. /**
  1322. * Toggle refinement state of a facet
  1323. * @param {string} facet the facet to refine
  1324. * @param {string} value the associated value
  1325. * @return {boolean} true if the facet has been found
  1326. */
  1327. toggleRefine: function(facet, value) {
  1328. for (var i = 0; i < this.options.facets.length; ++i) {
  1329. if (this.options.facets[i] == facet) {
  1330. var refinement = facet + ':' + value;
  1331. this.refinements[refinement] = !this.refinements[refinement];
  1332. this.page = 0;
  1333. this._search();
  1334. return true;
  1335. }
  1336. }
  1337. this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
  1338. for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) {
  1339. if (this.options.disjunctiveFacets[j] == facet) {
  1340. this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value];
  1341. this.page = 0;
  1342. this._search();
  1343. return true;
  1344. }
  1345. }
  1346. return false;
  1347. },
  1348. /**
  1349. * Check the refinement state of a facet
  1350. * @param {string} facet the facet
  1351. * @param {string} value the associated value
  1352. * @return {boolean} true if refined
  1353. */
  1354. isRefined: function(facet, value) {
  1355. var refinement = facet + ':' + value;
  1356. if (this.refinements[refinement]) {
  1357. return true;
  1358. }
  1359. if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) {
  1360. return true;
  1361. }
  1362. return false;
  1363. },
  1364. /**
  1365. * Go to next page
  1366. */
  1367. nextPage: function() {
  1368. this._gotoPage(this.page + 1);
  1369. },
  1370. /**
  1371. * Go to previous page
  1372. */
  1373. previousPage: function() {
  1374. if (this.page > 0) {
  1375. this._gotoPage(this.page - 1);
  1376. }
  1377. },
  1378. /**
  1379. * Goto a page
  1380. * @param {integer} page The page number
  1381. */
  1382. gotoPage: function(page) {
  1383. this._gotoPage(page);
  1384. },
  1385. /**
  1386. * Configure the page but do not trigger a reload
  1387. * @param {integer} page The page number
  1388. */
  1389. setPage: function(page) {
  1390. this.page = page;
  1391. },
  1392. ///////////// PRIVATE
  1393. /**
  1394. * Goto a page
  1395. * @param {integer} page The page number
  1396. */
  1397. _gotoPage: function(page) {
  1398. this.page = page;
  1399. this._search();
  1400. },
  1401. /**
  1402. * Perform the underlying queries
  1403. */
  1404. _search: function() {
  1405. this.client.startQueriesBatch();
  1406. this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams());
  1407. for (var i = 0; i < this.options.disjunctiveFacets.length; ++i) {
  1408. this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(this.options.disjunctiveFacets[i]));
  1409. }
  1410. var self = this;
  1411. this.client.sendQueriesBatch(function(success, content) {
  1412. if (!success) {
  1413. self.searchCallback(false, content);
  1414. return;
  1415. }
  1416. var aggregatedAnswer = content.results[0];
  1417. aggregatedAnswer.disjunctiveFacets = {};
  1418. aggregatedAnswer.facetStats = {};
  1419. for (var i = 1; i < content.results.length; ++i) {
  1420. for (var facet in content.results[i].facets) {
  1421. aggregatedAnswer.disjunctiveFacets[facet] = content.results[i].facets[facet];
  1422. if (self.disjunctiveRefinements[facet]) {
  1423. for (var value in self.disjunctiveRefinements[facet]) {
  1424. if (!aggregatedAnswer.disjunctiveFacets[facet][value] && self.disjunctiveRefinements[facet][value]) {
  1425. aggregatedAnswer.disjunctiveFacets[facet][value] = 0;
  1426. }
  1427. }
  1428. }
  1429. }
  1430. for (var stats in content.results[i].facets_stats)
  1431. {
  1432. aggregatedAnswer.facetStats[stats] = content.results[i].facets_stats[stats];
  1433. }
  1434. }
  1435. self.searchCallback(true, aggregatedAnswer);
  1436. });
  1437. },
  1438. /**
  1439. * Build search parameters used to fetch hits
  1440. * @return {hash}
  1441. */
  1442. _getHitsSearchParams: function() {
  1443. return extend({}, {
  1444. hitsPerPage: this.options.hitsPerPage,
  1445. page: this.page,
  1446. facets: this.options.facets,
  1447. facetFilters: this._getFacetFilters()
  1448. }, this.searchParams);
  1449. },
  1450. /**
  1451. * Build search parameters used to fetch a disjunctive facet
  1452. * @param {string} facet the associated facet name
  1453. * @return {hash}
  1454. */
  1455. _getDisjunctiveFacetSearchParams: function(facet) {
  1456. return extend({}, this.searchParams, {
  1457. hitsPerPage: 1,
  1458. page: 0,
  1459. facets: facet,
  1460. facetFilters: this._getFacetFilters(facet)
  1461. });
  1462. },
  1463. /**
  1464. * Build facetFilters parameter based on current refinements
  1465. * @param {string} facet if set, the current disjunctive facet
  1466. * @return {hash}
  1467. */
  1468. _getFacetFilters: function(facet) {
  1469. var facetFilters = [];
  1470. for (var refinement in this.refinements) {
  1471. if (this.refinements[refinement]) {
  1472. facetFilters.push(refinement);
  1473. }
  1474. }
  1475. for (var disjunctiveRefinement in this.disjunctiveRefinements) {
  1476. if (disjunctiveRefinement != facet) {
  1477. var refinements = [];
  1478. for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) {
  1479. if (this.disjunctiveRefinements[disjunctiveRefinement][value]) {
  1480. refinements.push(disjunctiveRefinement + ':' + value);
  1481. }
  1482. }
  1483. if (refinements.length > 0) {
  1484. facetFilters.push(refinements);
  1485. }
  1486. }
  1487. }
  1488. return facetFilters;
  1489. }
  1490. };
  1491. })();
  1492. /*
  1493. json2.js
  1494. 2014-02-04
  1495. Public Domain.
  1496. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  1497. See http://www.JSON.org/js.html
  1498. This code should be minified before deployment.
  1499. See http://javascript.crockford.com/jsmin.html
  1500. USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
  1501. NOT CONTROL.
  1502. This file creates a global JSON object containing two methods: stringify
  1503. and parse.
  1504. JSON.stringify(value, replacer, space)
  1505. value any JavaScript value, usually an object or array.
  1506. replacer an optional parameter that determines how object
  1507. values are stringified for objects. It can be a
  1508. function or an array of strings.
  1509. space an optional parameter that specifies the indentation
  1510. of nested structures. If it is omitted, the text will
  1511. be packed without extra whitespace. If it is a number,
  1512. it will specify the number of spaces to indent at each
  1513. level. If it is a string (such as '\t' or '&nbsp;'),
  1514. it contains the characters used to indent at each level.
  1515. This method produces a JSON text from a JavaScript value.
  1516. When an object value is found, if the object contains a toJSON
  1517. method, its toJSON method will be called and the result will be
  1518. stringified. A toJSON method does not serialize: it returns the
  1519. value represented by the name/value pair that should be serialized,
  1520. or undefined if nothing should be serialized. The toJSON method
  1521. will be passed the key associated with the value, and this will be
  1522. bound to the value
  1523. For example, this would serialize Dates as ISO strings.
  1524. Date.prototype.toJSON = function (key) {
  1525. function f(n) {
  1526. // Format integers to have at least two digits.
  1527. return n < 10 ? '0' + n : n;
  1528. }
  1529. return this.getUTCFullYear() + '-' +
  1530. f(this.getUTCMonth() + 1) + '-' +
  1531. f(this.getUTCDate()) + 'T' +
  1532. f(this.getUTCHours()) + ':' +
  1533. f(this.getUTCMinutes()) + ':' +
  1534. f(this.getUTCSeconds()) + 'Z';
  1535. };
  1536. You can provide an optional replacer method. It will be passed the
  1537. key and value of each member, with this bound to the containing
  1538. object. The value that is returned from your method will be
  1539. serialized. If your method returns undefined, then the member will
  1540. be excluded from the serialization.
  1541. If the replacer parameter is an array of strings, then it will be
  1542. used to select the members to be serialized. It filters the results
  1543. such that only members with keys listed in the replacer array are
  1544. stringified.
  1545. Values that do not have JSON representations, such as undefined or
  1546. functions, will not be serialized. Such values in objects will be
  1547. dropped; in arrays they will be replaced with null. You can use
  1548. a replacer function to replace those with JSON values.
  1549. JSON.stringify(undefined) returns undefined.
  1550. The optional space parameter produces a stringification of the
  1551. value that is filled with line breaks and indentation to make it
  1552. easier to read.
  1553. If the space parameter is a non-empty string, then that string will
  1554. be used for indentation. If the space parameter is a number, then
  1555. the indentation will be that many spaces.
  1556. Example:
  1557. text = JSON.stringify(['e', {pluribus: 'unum'}]);
  1558. // text is '["e",{"pluribus":"unum"}]'
  1559. text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
  1560. // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
  1561. text = JSON.stringify([new Date()], function (key, value) {
  1562. return this[key] instanceof Date ?
  1563. 'Date(' + this[key] + ')' : value;
  1564. });
  1565. // text is '["Date(---current time---)"]'
  1566. JSON.parse(text, reviver)
  1567. This method parses a JSON text to produce an object or array.
  1568. It can throw a SyntaxError exception.
  1569. The optional reviver parameter is a function that can filter and
  1570. transform the results. It receives each of the keys and values,
  1571. and its return value is used instead of the original value.
  1572. If it returns what it received, then the structure is not modified.
  1573. If it returns undefined then the member is deleted.
  1574. Example:
  1575. // Parse the text. Values that look like ISO date strings will
  1576. // be converted to Date objects.
  1577. myData = JSON.parse(text, function (key, value) {
  1578. var a;
  1579. if (typeof value === 'string') {
  1580. a =
  1581. /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
  1582. if (a) {
  1583. return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
  1584. +a[5], +a[6]));
  1585. }
  1586. }
  1587. return value;
  1588. });
  1589. myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
  1590. var d;
  1591. if (typeof value === 'string' &&
  1592. value.slice(0, 5) === 'Date(' &&
  1593. value.slice(-1) === ')') {
  1594. d = new Date(value.slice(5, -1));
  1595. if (d) {
  1596. return d;
  1597. }
  1598. }
  1599. return value;
  1600. });
  1601. This is a reference implementation. You are free to copy, modify, or
  1602. redistribute.
  1603. */
  1604. /*jslint evil: true, regexp: true */
  1605. /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
  1606. call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
  1607. getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
  1608. lastIndex, length, parse, prototype, push, replace, slice, stringify,
  1609. test, toJSON, toString, valueOf
  1610. */
  1611. // Create a JSON object only if one does not already exist. We create the
  1612. // methods in a closure to avoid creating global variables.
  1613. if (typeof JSON !== 'object') {
  1614. JSON = {};
  1615. }
  1616. (function () {
  1617. 'use strict';
  1618. function f(n) {
  1619. // Format integers to have at least two digits.
  1620. return n < 10 ? '0' + n : n;
  1621. }
  1622. if (typeof Date.prototype.toJSON !== 'function') {
  1623. Date.prototype.toJSON = function () {
  1624. return isFinite(this.valueOf())
  1625. ? this.getUTCFullYear() + '-' +
  1626. f(this.getUTCMonth() + 1) + '-' +
  1627. f(this.getUTCDate()) + 'T' +
  1628. f(this.getUTCHours()) + ':' +
  1629. f(this.getUTCMinutes()) + ':' +
  1630. f(this.getUTCSeconds()) + 'Z'
  1631. : null;
  1632. };
  1633. String.prototype.toJSON =
  1634. Number.prototype.toJSON =
  1635. Boolean.prototype.toJSON = function () {
  1636. return this.valueOf();
  1637. };
  1638. }
  1639. var cx,
  1640. escapable,
  1641. gap,
  1642. indent,
  1643. meta,
  1644. rep;
  1645. function quote(string) {
  1646. // If the string contains no control characters, no quote characters, and no
  1647. // backslash characters, then we can safely slap some quotes around it.
  1648. // Otherwise we must also replace the offending characters with safe escape
  1649. // sequences.
  1650. escapable.lastIndex = 0;
  1651. return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
  1652. var c = meta[a];
  1653. return typeof c === 'string'
  1654. ? c
  1655. : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
  1656. }) + '"' : '"' + string + '"';
  1657. }
  1658. function str(key, holder) {
  1659. // Produce a string from holder[key].
  1660. var i, // The loop counter.
  1661. k, // The member key.
  1662. v, // The member value.
  1663. length,
  1664. mind = gap,
  1665. partial,
  1666. value = holder[key];
  1667. // If the value has a toJSON method, call it to obtain a replacement value.
  1668. if (value && typeof value === 'object' &&
  1669. typeof value.toJSON === 'function') {
  1670. value = value.toJSON(key);
  1671. }
  1672. // If we were called with a replacer function, then call the replacer to
  1673. // obtain a replacement value.
  1674. if (typeof rep === 'function') {
  1675. value = rep.call(holder, key, value);
  1676. }
  1677. // What happens next depends on the value's type.
  1678. switch (typeof value) {
  1679. case 'string':
  1680. return quote(value);
  1681. case 'number':
  1682. // JSON numbers must be finite. Encode non-finite numbers as null.
  1683. return isFinite(value) ? String(value) : 'null';
  1684. case 'boolean':
  1685. case 'null':
  1686. // If the value is a boolean or null, convert it to a string. Note:
  1687. // typeof null does not produce 'null'. The case is included here in
  1688. // the remote chance that this gets fixed someday.
  1689. return String(value);
  1690. // If the type is 'object', we might be dealing with an object or an array or
  1691. // null.
  1692. case 'object':
  1693. // Due to a specification blunder in ECMAScript, typeof null is 'object',
  1694. // so watch out for that case.
  1695. if (!value) {
  1696. return 'null';
  1697. }
  1698. // Make an array to hold the partial results of stringifying this object value.
  1699. gap += indent;
  1700. partial = [];
  1701. // Is the value an array?
  1702. if (Object.prototype.toString.apply(value) === '[object Array]') {
  1703. // The value is an array. Stringify every element. Use null as a placeholder
  1704. // for non-JSON values.
  1705. length = value.length;
  1706. for (i = 0; i < length; i += 1) {
  1707. partial[i] = str(i, value) || 'null';
  1708. }
  1709. // Join all of the elements together, separated with commas, and wrap them in
  1710. // brackets.
  1711. v = partial.length === 0
  1712. ? '[]'
  1713. : gap
  1714. ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
  1715. : '[' + partial.join(',') + ']';
  1716. gap = mind;
  1717. return v;
  1718. }
  1719. // If the replacer is an array, use it to select the members to be stringified.
  1720. if (rep && typeof rep === 'object') {
  1721. length = rep.length;
  1722. for (i = 0; i < length; i += 1) {
  1723. if (typeof rep[i] === 'string') {
  1724. k = rep[i];
  1725. v = str(k, value);
  1726. if (v) {
  1727. partial.push(quote(k) + (gap ? ': ' : ':') + v);
  1728. }
  1729. }
  1730. }
  1731. } else {
  1732. // Otherwise, iterate through all of the keys in the object.
  1733. for (k in value) {
  1734. if (Object.prototype.hasOwnProperty.call(value, k)) {
  1735. v = str(k, value);
  1736. if (v) {
  1737. partial.push(quote(k) + (gap ? ': ' : ':') + v);
  1738. }
  1739. }
  1740. }
  1741. }
  1742. // Join all of the member texts together, separated with commas,
  1743. // and wrap them in braces.
  1744. v = partial.length === 0
  1745. ? '{}'
  1746. : gap
  1747. ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
  1748. : '{' + partial.join(',') + '}';
  1749. gap = mind;
  1750. return v;
  1751. }
  1752. }
  1753. // If the JSON object does not yet have a stringify method, give it one.
  1754. if (typeof JSON.stringify !== 'function') {
  1755. escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  1756. meta = { // table of character substitutions
  1757. '\b': '\\b',
  1758. '\t': '\\t',
  1759. '\n': '\\n',
  1760. '\f': '\\f',
  1761. '\r': '\\r',
  1762. '"' : '\\"',
  1763. '\\': '\\\\'
  1764. };
  1765. JSON.stringify = function (value, replacer, space) {
  1766. // The stringify method takes a value and an optional replacer, and an optional
  1767. // space parameter, and returns a JSON text. The replacer can be a function
  1768. // that can replace values, or an array of strings that will select the keys.
  1769. // A default replacer method can be provided. Use of the space parameter can
  1770. // produce text that is more easily readable.
  1771. var i;
  1772. gap = '';
  1773. indent = '';
  1774. // If the space parameter is a number, make an indent string containing that
  1775. // many spaces.
  1776. if (typeof space === 'number') {
  1777. for (i = 0; i < space; i += 1) {
  1778. indent += ' ';
  1779. }
  1780. // If the space parameter is a string, it will be used as the indent string.
  1781. } else if (typeof space === 'string') {
  1782. indent = space;
  1783. }
  1784. // If there is a replacer, it must be a function or an array.
  1785. // Otherwise, throw an error.
  1786. rep = replacer;
  1787. if (replacer && typeof replacer !== 'function' &&
  1788. (typeof replacer !== 'object' ||
  1789. typeof replacer.length !== 'number')) {
  1790. throw new Error('JSON.stringify');
  1791. }
  1792. // Make a fake root object containing our value under the key of ''.
  1793. // Return the result of stringifying the value.
  1794. return str('', {'': value});
  1795. };
  1796. }
  1797. // If the JSON object does not yet have a parse method, give it one.
  1798. if (typeof JSON.parse !== 'function') {
  1799. cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  1800. JSON.parse = function (text, reviver) {
  1801. // The parse method takes a text and an optional reviver function, and returns
  1802. // a JavaScript value if the text is a valid JSON text.
  1803. var j;
  1804. function walk(holder, key) {
  1805. // The walk method is used to recursively walk the resulting structure so
  1806. // that modifications can be made.
  1807. var k, v, value = holder[key];
  1808. if (value && typeof value === 'object') {
  1809. for (k in value) {
  1810. if (Object.prototype.hasOwnProperty.call(value, k)) {
  1811. v = walk(value, k);
  1812. if (v !== undefined) {
  1813. value[k] = v;
  1814. } else {
  1815. delete value[k];
  1816. }
  1817. }
  1818. }
  1819. }
  1820. return reviver.call(holder, key, value);
  1821. }
  1822. // Parsing happens in four stages. In the first stage, we replace certain
  1823. // Unicode characters with escape sequences. JavaScript handles many characters
  1824. // incorrectly, either silently deleting them, or treating them as line endings.
  1825. text = String(text);
  1826. cx.lastIndex = 0;
  1827. if (cx.test(text)) {
  1828. text = text.replace(cx, function (a) {
  1829. return '\\u' +
  1830. ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
  1831. });
  1832. }
  1833. // In the second stage, we run the text against regular expressions that look
  1834. // for non-JSON patterns. We are especially concerned with '()' and 'new'
  1835. // because they can cause invocation, and '=' because it can cause mutation.
  1836. // But just to be safe, we want to reject all unexpected forms.
  1837. // We split the second stage into 4 regexp operations in order to work around
  1838. // crippling inefficiencies in IE's and Safari's regexp engines. First we
  1839. // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
  1840. // replace all simple value tokens with ']' characters. Third, we delete all
  1841. // open brackets that follow a colon or comma or that begin the text. Finally,
  1842. // we look to see that the remaining characters are only whitespace or ']' or
  1843. // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
  1844. if (/^[\],:{}\s]*$/
  1845. .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
  1846. .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
  1847. .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
  1848. // In the third stage we use the eval function to compile the text into a
  1849. // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
  1850. // in JavaScript: it can begin a block or an object literal. We wrap the text
  1851. // in parens to eliminate the ambiguity.
  1852. j = eval('(' + text + ')');
  1853. // In the optional fourth stage, we recursively walk the new structure, passing
  1854. // each name/value pair to a reviver function for possible transformation.
  1855. return typeof reviver === 'function'
  1856. ? walk({'': j}, '')
  1857. : j;
  1858. }
  1859. // If the text is not JSON parseable, then a SyntaxError is thrown.
  1860. throw new SyntaxError('JSON.parse');
  1861. };
  1862. }
  1863. }());