algoliasearch-rails 1.11.17 → 1.11.18

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