Blame | Last modification | View Log | RSS feed
/** typeahead.js* https://github.com/twitter/typeahead.js* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT*/(function(root) {'use strict';var old, keys;old = root.Bloodhound;keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' };// add Bloodhoud to global contextroot.Bloodhound = Bloodhound;// constructor// -----------function Bloodhound(o) {if (!o || (!o.local && !o.prefetch && !o.remote)) {$.error('one of local, prefetch, or remote is required');}this.limit = o.limit || 5;this.sorter = getSorter(o.sorter);this.dupDetector = o.dupDetector || ignoreDuplicates;this.local = oParser.local(o);this.prefetch = oParser.prefetch(o);this.remote = oParser.remote(o);this.cacheKey = this.prefetch ?(this.prefetch.cacheKey || this.prefetch.url) : null;// the backing data structure used for fast pattern matchingthis.index = new SearchIndex({datumTokenizer: o.datumTokenizer,queryTokenizer: o.queryTokenizer});// only initialize storage if there's a cacheKey otherwise// loading from storage on subsequent page loads is impossiblethis.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;}// static methods// --------------Bloodhound.noConflict = function noConflict() {root.Bloodhound = old;return Bloodhound;};Bloodhound.tokenizers = tokenizers;// instance methods// ----------------_.mixin(Bloodhound.prototype, {// ### private_loadPrefetch: function loadPrefetch(o) {var that = this, serialized, deferred;if (serialized = this._readFromStorage(o.thumbprint)) {this.index.bootstrap(serialized);deferred = $.Deferred().resolve();}else {deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);}return deferred;function handlePrefetchResponse(resp) {// clear to mirror the behavior of bootstrappingthat.clear();that.add(o.filter ? o.filter(resp) : resp);that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);}},_getFromRemote: function getFromRemote(query, cb) {var that = this, url, uriEncodedQuery;if (!this.transport) { return; }query = query || '';uriEncodedQuery = encodeURIComponent(query);url = this.remote.replace ?this.remote.replace(this.remote.url, query) :this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);return this.transport.get(url, this.remote.ajax, handleRemoteResponse);function handleRemoteResponse(err, resp) {err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);}},_cancelLastRemoteRequest: function cancelLastRemoteRequest() {// #149: prevents outdated rate-limited requests from being sentthis.transport && this.transport.cancel();},_saveToStorage: function saveToStorage(data, thumbprint, ttl) {if (this.storage) {this.storage.set(keys.data, data, ttl);this.storage.set(keys.protocol, location.protocol, ttl);this.storage.set(keys.thumbprint, thumbprint, ttl);}},_readFromStorage: function readFromStorage(thumbprint) {var stored = {}, isExpired;if (this.storage) {stored.data = this.storage.get(keys.data);stored.protocol = this.storage.get(keys.protocol);stored.thumbprint = this.storage.get(keys.thumbprint);}// the stored data is considered expired if the thumbprints// don't match or if the protocol it was originally stored under// has changedisExpired = stored.thumbprint !== thumbprint ||stored.protocol !== location.protocol;return stored.data && !isExpired ? stored.data : null;},_initialize: function initialize() {var that = this, local = this.local, deferred;deferred = this.prefetch ?this._loadPrefetch(this.prefetch) : $.Deferred().resolve();// make sure local is added to the index after prefetchlocal && deferred.done(addLocalToIndex);this.transport = this.remote ? new Transport(this.remote) : null;return (this.initPromise = deferred.promise());function addLocalToIndex() {// local can be a function that returns an array of datumsthat.add(_.isFunction(local) ? local() : local);}},// ### publicinitialize: function initialize(force) {return !this.initPromise || force ? this._initialize() : this.initPromise;},add: function add(data) {this.index.add(data);},get: function get(query, cb) {var that = this, matches = [], cacheHit = false;matches = this.index.get(query);matches = this.sorter(matches).slice(0, this.limit);matches.length < this.limit ?(cacheHit = this._getFromRemote(query, returnRemoteMatches)) :this._cancelLastRemoteRequest();// if a cache hit occurred, skip rendering local matches// because the rendering of local/remote matches is already// in the event loopif (!cacheHit) {// only render if there are some local suggestions or we're// going to the network to backfill(matches.length > 0 || !this.transport) && cb && cb(matches);}function returnRemoteMatches(remoteMatches) {var matchesWithBackfill = matches.slice(0);_.each(remoteMatches, function(remoteMatch) {var isDuplicate;// checks for duplicatesisDuplicate = _.some(matchesWithBackfill, function(match) {return that.dupDetector(remoteMatch, match);});!isDuplicate && matchesWithBackfill.push(remoteMatch);// if we're at the limit, we no longer need to process// the remote results and can break out of the each loopreturn matchesWithBackfill.length < that.limit;});cb && cb(that.sorter(matchesWithBackfill));}},clear: function clear() {this.index.reset();},clearPrefetchCache: function clearPrefetchCache() {this.storage && this.storage.clear();},clearRemoteCache: function clearRemoteCache() {this.transport && Transport.resetCache();},ttAdapter: function ttAdapter() { return _.bind(this.get, this); }});return Bloodhound;// helper functions// ----------------function getSorter(sortFn) {return _.isFunction(sortFn) ? sort : noSort;function sort(array) { return array.sort(sortFn); }function noSort(array) { return array; }}function ignoreDuplicates() { return false; }})(this);