Skip to content
Permalink
6e641d30d9
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
322 lines (249 sloc) 9.16 KB
/*
* typeahead.js
* https://github.com/twitter/typeahead
* Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
*/
var Dataset = (function() {
var keys = {
thumbprint: 'thumbprint',
protocol: 'protocol',
itemHash: 'itemHash',
adjacencyList: 'adjacencyList'
};
function Dataset(o) {
utils.bindAll(this);
if (utils.isString(o.template) && !o.engine) {
$.error('no template engine specified');
}
if (!o.local && !o.prefetch && !o.remote) {
$.error('one of local, prefetch, or remote is required');
}
this.name = o.name || utils.getUniqueId();
this.limit = o.limit || 5;
this.minLength = o.minLength || 1;
this.header = o.header;
this.footer = o.footer;
this.valueKey = o.valueKey || 'value';
this.template = compileTemplate(o.template, o.engine, this.valueKey);
// used then deleted in #initialize
this.local = o.local;
this.prefetch = o.prefetch;
this.remote = o.remote;
this.itemHash = {};
this.adjacencyList = {};
// only initialize storage if there's a name otherwise
// loading from storage on subsequent page loads is impossible
this.storage = o.name ? new PersistentStorage(o.name) : null;
}
utils.mixin(Dataset.prototype, {
// private methods
// ---------------
_processLocalData: function(data) {
this._mergeProcessedData(this._processData(data));
},
_loadPrefetchData: function(o) {
var that = this,
thumbprint = VERSION + (o.thumbprint || ''),
storedThumbprint,
storedProtocol,
storedItemHash,
storedAdjacencyList,
isExpired,
deferred;
if (this.storage) {
storedThumbprint = this.storage.get(keys.thumbprint);
storedProtocol = this.storage.get(keys.protocol);
storedItemHash = this.storage.get(keys.itemHash);
storedAdjacencyList = this.storage.get(keys.adjacencyList);
}
isExpired = storedThumbprint !== thumbprint ||
storedProtocol !== utils.getProtocol();
o = utils.isString(o) ? { url: o } : o;
o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1000;
// data was available in local storage, use it
if (storedItemHash && storedAdjacencyList && !isExpired) {
this._mergeProcessedData({
itemHash: storedItemHash,
adjacencyList: storedAdjacencyList
});
deferred = $.Deferred().resolve();
}
else {
deferred = $.getJSON(o.url).done(processPrefetchData);
}
return deferred;
function processPrefetchData(data) {
var filteredData = o.filter ? o.filter(data) : data,
processedData = that._processData(filteredData),
itemHash = processedData.itemHash,
adjacencyList = processedData.adjacencyList;
// store process data in local storage, if storage is available
// this saves us from processing the data on every page load
if (that.storage) {
that.storage.set(keys.itemHash, itemHash, o.ttl);
that.storage.set(keys.adjacencyList, adjacencyList, o.ttl);
that.storage.set(keys.thumbprint, thumbprint, o.ttl);
that.storage.set(keys.protocol, utils.getProtocol(), o.ttl);
}
that._mergeProcessedData(processedData);
}
},
_transformDatum: function(datum) {
var value = utils.isString(datum) ? datum : datum[this.valueKey],
tokens = datum.tokens || utils.tokenizeText(value),
item = { value: value, tokens: tokens };
if (utils.isString(datum)) {
item.datum = {};
item.datum[this.valueKey] = datum;
}
else {
item.datum = datum;
}
// filter out falsy tokens
item.tokens = utils.filter(item.tokens, function(token) {
return !utils.isBlankString(token);
});
// normalize tokens
item.tokens = utils.map(item.tokens, function(token) {
return token.toLowerCase();
});
return item;
},
_processData: function(data) {
var that = this, itemHash = {}, adjacencyList = {};
utils.each(data, function(i, datum) {
var item = that._transformDatum(datum),
id = utils.getUniqueId(item.value);
itemHash[id] = item;
utils.each(item.tokens, function(i, token) {
var character = token.charAt(0),
adjacency = adjacencyList[character] ||
(adjacencyList[character] = [id]);
!~utils.indexOf(adjacency, id) && adjacency.push(id);
});
});
return { itemHash: itemHash, adjacencyList: adjacencyList };
},
_mergeProcessedData: function(processedData) {
var that = this;
// merge item hash
utils.mixin(this.itemHash, processedData.itemHash);
// merge adjacency list
utils.each(processedData.adjacencyList, function(character, adjacency) {
var masterAdjacency = that.adjacencyList[character];
that.adjacencyList[character] = masterAdjacency ?
masterAdjacency.concat(adjacency) : adjacency;
});
},
_getLocalSuggestions: function(terms) {
var that = this,
firstChars = [],
lists = [],
shortestList,
suggestions = [];
// create a unique array of the first chars in
// the terms this comes in handy when multiple
// terms start with the same letter
utils.each(terms, function(i, term) {
var firstChar = term.charAt(0);
!~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar);
});
utils.each(firstChars, function(i, firstChar) {
var list = that.adjacencyList[firstChar];
// break out of the loop early
if (!list) { return false; }
lists.push(list);
if (!shortestList || list.length < shortestList.length) {
shortestList = list;
}
});
// no suggestions :(
if (lists.length < firstChars.length) {
return [];
}
// populate suggestions
utils.each(shortestList, function(i, id) {
var item = that.itemHash[id], isCandidate, isMatch;
isCandidate = utils.every(lists, function(list) {
return ~utils.indexOf(list, id);
});
isMatch = isCandidate && utils.every(terms, function(term) {
return utils.some(item.tokens, function(token) {
return token.indexOf(term) === 0;
});
});
isMatch && suggestions.push(item);
});
return suggestions;
},
// public methods
// ---------------
// the contents of this function are broken out of the constructor
// to help improve the testability of datasets
initialize: function() {
var deferred;
this.local && this._processLocalData(this.local);
this.transport = this.remote ? new Transport(this.remote) : null;
deferred = this.prefetch ?
this._loadPrefetchData(this.prefetch) :
$.Deferred().resolve();
this.local = this.prefetch = this.remote = null;
this.initialize = function() { return deferred; };
return deferred;
},
getSuggestions: function(query, cb) {
var that = this, terms, suggestions, cacheHit = false;
// don't do anything until the minLength constraint is met
if (query.length < this.minLength) {
return;
}
terms = utils.tokenizeQuery(query);
suggestions = this._getLocalSuggestions(terms).slice(0, this.limit);
if (suggestions.length < this.limit && this.transport) {
cacheHit = this.transport.get(query, processRemoteData);
}
// if a cache hit occurred, skip rendering local suggestions
// because the rendering of local/remote suggestions is already
// in the event loop
!cacheHit && cb && cb(suggestions);
// callback for transport.get
function processRemoteData(data) {
suggestions = suggestions.slice(0);
// convert remote suggestions to object
utils.each(data, function(i, datum) {
var item = that._transformDatum(datum), isDuplicate;
// checks for duplicates
isDuplicate = utils.some(suggestions, function(suggestion) {
return item.value === suggestion.value;
});
!isDuplicate && suggestions.push(item);
// if we're at the limit, we no longer need to process
// the remote results and can break out of the each loop
return suggestions.length < that.limit;
});
cb && cb(suggestions);
}
}
});
return Dataset;
function compileTemplate(template, engine, valueKey) {
var renderFn, compiledTemplate;
// precompiled template
if (utils.isFunction(template)) {
renderFn = template;
}
// string template that needs to be compiled
else if (utils.isString(template)) {
compiledTemplate = engine.compile(template);
renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
}
// if no template is provided, render suggestion
// as its value wrapped in a p tag
else {
renderFn = function(context) {
return '<p>' + context[valueKey] + '</p>';
};
}
return renderFn;
}
})();