331 lines
8.6 KiB
JavaScript
331 lines
8.6 KiB
JavaScript
/*
|
|
* typeahead.js
|
|
* https://github.com/twitter/typeahead.js
|
|
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
|
|
*/
|
|
|
|
var Dataset = (function() {
|
|
'use strict';
|
|
|
|
var keys, nameGenerator;
|
|
|
|
keys = {
|
|
val: 'tt-selectable-display',
|
|
obj: 'tt-selectable-object'
|
|
};
|
|
|
|
nameGenerator = _.getIdGenerator();
|
|
|
|
// constructor
|
|
// -----------
|
|
|
|
function Dataset(o, www) {
|
|
o = o || {};
|
|
o.templates = o.templates || {};
|
|
|
|
// DEPRECATED: empty will be dropped in v1
|
|
o.templates.notFound = o.templates.notFound || o.templates.empty;
|
|
|
|
if (!o.source) {
|
|
$.error('missing source');
|
|
}
|
|
|
|
if (!o.node) {
|
|
$.error('missing node');
|
|
}
|
|
|
|
if (o.name && !isValidName(o.name)) {
|
|
$.error('invalid dataset name: ' + o.name);
|
|
}
|
|
|
|
www.mixin(this);
|
|
|
|
this.highlight = !!o.highlight;
|
|
this.name = o.name || nameGenerator();
|
|
|
|
this.limit = o.limit || 5;
|
|
this.displayFn = getDisplayFn(o.display || o.displayKey);
|
|
this.templates = getTemplates(o.templates, this.displayFn);
|
|
|
|
// use duck typing to see if source is a bloodhound instance by checking
|
|
// for the __ttAdapter property; otherwise assume it is a function
|
|
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
|
|
|
|
// if the async option is undefined, inspect the source signature as
|
|
// a hint to figuring out of the source will return async suggestions
|
|
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
|
|
|
|
this._resetLastSuggestion();
|
|
|
|
this.$el = $(o.node)
|
|
.addClass(this.classes.dataset)
|
|
.addClass(this.classes.dataset + '-' + this.name);
|
|
}
|
|
|
|
// static methods
|
|
// --------------
|
|
|
|
Dataset.extractData = function extractData(el) {
|
|
var $el = $(el);
|
|
|
|
if ($el.data(keys.obj)) {
|
|
return {
|
|
val: $el.data(keys.val) || '',
|
|
obj: $el.data(keys.obj) || null
|
|
};
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// instance methods
|
|
// ----------------
|
|
|
|
_.mixin(Dataset.prototype, EventEmitter, {
|
|
|
|
// ### private
|
|
|
|
_overwrite: function overwrite(query, suggestions) {
|
|
suggestions = suggestions || [];
|
|
|
|
// got suggestions: overwrite dom with suggestions
|
|
if (suggestions.length) {
|
|
this._renderSuggestions(query, suggestions);
|
|
}
|
|
|
|
// no suggestions, expecting async: overwrite dom with pending
|
|
else if (this.async && this.templates.pending) {
|
|
this._renderPending(query);
|
|
}
|
|
|
|
// no suggestions, not expecting async: overwrite dom with not found
|
|
else if (!this.async && this.templates.notFound) {
|
|
this._renderNotFound(query);
|
|
}
|
|
|
|
// nothing to render: empty dom
|
|
else {
|
|
this._empty();
|
|
}
|
|
|
|
this.trigger('rendered', this.name, suggestions, false);
|
|
},
|
|
|
|
_append: function append(query, suggestions) {
|
|
suggestions = suggestions || [];
|
|
|
|
// got suggestions, sync suggestions exist: append suggestions to dom
|
|
if (suggestions.length && this.$lastSuggestion.length) {
|
|
this._appendSuggestions(query, suggestions);
|
|
}
|
|
|
|
// got suggestions, no sync suggestions: overwrite dom with suggestions
|
|
else if (suggestions.length) {
|
|
this._renderSuggestions(query, suggestions);
|
|
}
|
|
|
|
// no async/sync suggestions: overwrite dom with not found
|
|
else if (!this.$lastSuggestion.length && this.templates.notFound) {
|
|
this._renderNotFound(query);
|
|
}
|
|
|
|
this.trigger('rendered', this.name, suggestions, true);
|
|
},
|
|
|
|
_renderSuggestions: function renderSuggestions(query, suggestions) {
|
|
var $fragment;
|
|
|
|
$fragment = this._getSuggestionsFragment(query, suggestions);
|
|
this.$lastSuggestion = $fragment.children().last();
|
|
|
|
this.$el.html($fragment)
|
|
.prepend(this._getHeader(query, suggestions))
|
|
.append(this._getFooter(query, suggestions));
|
|
},
|
|
|
|
_appendSuggestions: function appendSuggestions(query, suggestions) {
|
|
var $fragment, $lastSuggestion;
|
|
|
|
$fragment = this._getSuggestionsFragment(query, suggestions);
|
|
$lastSuggestion = $fragment.children().last();
|
|
|
|
this.$lastSuggestion.after($fragment);
|
|
|
|
this.$lastSuggestion = $lastSuggestion;
|
|
},
|
|
|
|
_renderPending: function renderPending(query) {
|
|
var template = this.templates.pending;
|
|
|
|
this._resetLastSuggestion();
|
|
template && this.$el.html(template({
|
|
query: query,
|
|
dataset: this.name,
|
|
}));
|
|
},
|
|
|
|
_renderNotFound: function renderNotFound(query) {
|
|
var template = this.templates.notFound;
|
|
|
|
this._resetLastSuggestion();
|
|
template && this.$el.html(template({
|
|
query: query,
|
|
dataset: this.name,
|
|
}));
|
|
},
|
|
|
|
_empty: function empty() {
|
|
this.$el.empty();
|
|
this._resetLastSuggestion();
|
|
},
|
|
|
|
_getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
|
|
var that = this, fragment;
|
|
|
|
fragment = document.createDocumentFragment();
|
|
_.each(suggestions, function getSuggestionNode(suggestion) {
|
|
var $el, context;
|
|
|
|
context = that._injectQuery(query, suggestion);
|
|
|
|
$el = $(that.templates.suggestion(context))
|
|
.data(keys.obj, suggestion)
|
|
.data(keys.val, that.displayFn(suggestion))
|
|
.addClass(that.classes.suggestion + ' ' + that.classes.selectable);
|
|
|
|
fragment.appendChild($el[0]);
|
|
});
|
|
|
|
this.highlight && highlight({
|
|
className: this.classes.highlight,
|
|
node: fragment,
|
|
pattern: query
|
|
});
|
|
|
|
return $(fragment);
|
|
},
|
|
|
|
_getFooter: function getFooter(query, suggestions) {
|
|
return this.templates.footer ?
|
|
this.templates.footer({
|
|
query: query,
|
|
suggestions: suggestions,
|
|
dataset: this.name
|
|
}) : null;
|
|
},
|
|
|
|
_getHeader: function getHeader(query, suggestions) {
|
|
return this.templates.header ?
|
|
this.templates.header({
|
|
query: query,
|
|
suggestions: suggestions,
|
|
dataset: this.name
|
|
}) : null;
|
|
},
|
|
|
|
_resetLastSuggestion: function resetLastSuggestion() {
|
|
this.$lastSuggestion = $();
|
|
},
|
|
|
|
_injectQuery: function injectQuery(query, obj) {
|
|
return _.isObject(obj) ? _.mixin({ _query: query }, obj) : obj;
|
|
},
|
|
|
|
// ### public
|
|
|
|
update: function update(query) {
|
|
var that = this, canceled = false, syncCalled = false, rendered = 0;
|
|
|
|
// cancel possible pending update
|
|
this.cancel();
|
|
|
|
this.cancel = function cancel() {
|
|
canceled = true;
|
|
that.cancel = $.noop;
|
|
that.async && that.trigger('asyncCanceled', query);
|
|
};
|
|
|
|
this.source(query, sync, async);
|
|
!syncCalled && sync([]);
|
|
|
|
function sync(suggestions) {
|
|
if (syncCalled) { return; }
|
|
|
|
syncCalled = true;
|
|
suggestions = (suggestions || []).slice(0, that.limit);
|
|
rendered = suggestions.length;
|
|
|
|
that._overwrite(query, suggestions);
|
|
|
|
if (rendered < that.limit && that.async) {
|
|
that.trigger('asyncRequested', query);
|
|
}
|
|
}
|
|
|
|
function async(suggestions) {
|
|
suggestions = suggestions || [];
|
|
|
|
// if the update has been canceled or if the query has changed
|
|
// do not render the suggestions as they've become outdated
|
|
if (!canceled && rendered < that.limit) {
|
|
that.cancel = $.noop;
|
|
that._append(query, suggestions.slice(0, that.limit - rendered));
|
|
rendered += suggestions.length;
|
|
|
|
that.async && that.trigger('asyncReceived', query);
|
|
}
|
|
}
|
|
},
|
|
|
|
// cancel function gets set in #update
|
|
cancel: $.noop,
|
|
|
|
clear: function clear() {
|
|
this._empty();
|
|
this.cancel();
|
|
this.trigger('cleared');
|
|
},
|
|
|
|
isEmpty: function isEmpty() {
|
|
return this.$el.is(':empty');
|
|
},
|
|
|
|
destroy: function destroy() {
|
|
// #970
|
|
this.$el = $('<div>');
|
|
}
|
|
});
|
|
|
|
return Dataset;
|
|
|
|
// helper functions
|
|
// ----------------
|
|
|
|
function getDisplayFn(display) {
|
|
display = display || _.stringify;
|
|
|
|
return _.isFunction(display) ? display : displayFn;
|
|
|
|
function displayFn(obj) { return obj[display]; }
|
|
}
|
|
|
|
function getTemplates(templates, displayFn) {
|
|
return {
|
|
notFound: templates.notFound && _.templatify(templates.notFound),
|
|
pending: templates.pending && _.templatify(templates.pending),
|
|
header: templates.header && _.templatify(templates.header),
|
|
footer: templates.footer && _.templatify(templates.footer),
|
|
suggestion: templates.suggestion || suggestionTemplate
|
|
};
|
|
|
|
function suggestionTemplate(context) {
|
|
return $('<div>').text(displayFn(context));
|
|
}
|
|
}
|
|
|
|
function isValidName(str) {
|
|
// dashes, underscores, letters, and numbers
|
|
return (/^[_a-zA-Z0-9-]+$/).test(str);
|
|
}
|
|
})();
|