/* * 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; rendered += suggestions.length; that._append(query, suggestions.slice(0, that.limit - rendered)); 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 = $('