alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
235 lines (173 loc) • 6.84 kB
JavaScript
/*
* 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 context
root.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 matching
this.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 impossible
this.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 bootstrapping
that.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 sent
this.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 changed
isExpired = 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 prefetch
local && 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 datums
that.add(_.isFunction(local) ? local() : local);
}
},
// ### public
initialize: 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 loop
if (!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 duplicates
isDuplicate = _.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 loop
return 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);