Commit e75e59213ac27d511c15f24d144196e812619d42

Authored by Braulio Bhavamitra
1 parent 5f4794df

Add typeahead (use locally with <%=content_for :head do %>)

public/javascripts/typeahead.bundle.js 0 → 100644
@@ -0,0 +1,1782 @@ @@ -0,0 +1,1782 @@
  1 +/*!
  2 + * typeahead.js 0.10.5
  3 + * https://github.com/twitter/typeahead.js
  4 + * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
  5 + */
  6 +
  7 +(function($) {
  8 + var _ = function() {
  9 + "use strict";
  10 + return {
  11 + isMsie: function() {
  12 + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
  13 + },
  14 + isBlankString: function(str) {
  15 + return !str || /^\s*$/.test(str);
  16 + },
  17 + escapeRegExChars: function(str) {
  18 + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  19 + },
  20 + isString: function(obj) {
  21 + return typeof obj === "string";
  22 + },
  23 + isNumber: function(obj) {
  24 + return typeof obj === "number";
  25 + },
  26 + isArray: $.isArray,
  27 + isFunction: $.isFunction,
  28 + isObject: $.isPlainObject,
  29 + isUndefined: function(obj) {
  30 + return typeof obj === "undefined";
  31 + },
  32 + toStr: function toStr(s) {
  33 + return _.isUndefined(s) || s === null ? "" : s + "";
  34 + },
  35 + bind: $.proxy,
  36 + each: function(collection, cb) {
  37 + $.each(collection, reverseArgs);
  38 + function reverseArgs(index, value) {
  39 + return cb(value, index);
  40 + }
  41 + },
  42 + map: $.map,
  43 + filter: $.grep,
  44 + every: function(obj, test) {
  45 + var result = true;
  46 + if (!obj) {
  47 + return result;
  48 + }
  49 + $.each(obj, function(key, val) {
  50 + if (!(result = test.call(null, val, key, obj))) {
  51 + return false;
  52 + }
  53 + });
  54 + return !!result;
  55 + },
  56 + some: function(obj, test) {
  57 + var result = false;
  58 + if (!obj) {
  59 + return result;
  60 + }
  61 + $.each(obj, function(key, val) {
  62 + if (result = test.call(null, val, key, obj)) {
  63 + return false;
  64 + }
  65 + });
  66 + return !!result;
  67 + },
  68 + mixin: $.extend,
  69 + getUniqueId: function() {
  70 + var counter = 0;
  71 + return function() {
  72 + return counter++;
  73 + };
  74 + }(),
  75 + templatify: function templatify(obj) {
  76 + return $.isFunction(obj) ? obj : template;
  77 + function template() {
  78 + return String(obj);
  79 + }
  80 + },
  81 + defer: function(fn) {
  82 + setTimeout(fn, 0);
  83 + },
  84 + debounce: function(func, wait, immediate) {
  85 + var timeout, result;
  86 + return function() {
  87 + var context = this, args = arguments, later, callNow;
  88 + later = function() {
  89 + timeout = null;
  90 + if (!immediate) {
  91 + result = func.apply(context, args);
  92 + }
  93 + };
  94 + callNow = immediate && !timeout;
  95 + clearTimeout(timeout);
  96 + timeout = setTimeout(later, wait);
  97 + if (callNow) {
  98 + result = func.apply(context, args);
  99 + }
  100 + return result;
  101 + };
  102 + },
  103 + throttle: function(func, wait) {
  104 + var context, args, timeout, result, previous, later;
  105 + previous = 0;
  106 + later = function() {
  107 + previous = new Date();
  108 + timeout = null;
  109 + result = func.apply(context, args);
  110 + };
  111 + return function() {
  112 + var now = new Date(), remaining = wait - (now - previous);
  113 + context = this;
  114 + args = arguments;
  115 + if (remaining <= 0) {
  116 + clearTimeout(timeout);
  117 + timeout = null;
  118 + previous = now;
  119 + result = func.apply(context, args);
  120 + } else if (!timeout) {
  121 + timeout = setTimeout(later, remaining);
  122 + }
  123 + return result;
  124 + };
  125 + },
  126 + noop: function() {}
  127 + };
  128 + }();
  129 + var VERSION = "0.10.5";
  130 + var tokenizers = function() {
  131 + "use strict";
  132 + return {
  133 + nonword: nonword,
  134 + whitespace: whitespace,
  135 + obj: {
  136 + nonword: getObjTokenizer(nonword),
  137 + whitespace: getObjTokenizer(whitespace)
  138 + }
  139 + };
  140 + function whitespace(str) {
  141 + str = _.toStr(str);
  142 + return str ? str.split(/\s+/) : [];
  143 + }
  144 + function nonword(str) {
  145 + str = _.toStr(str);
  146 + return str ? str.split(/\W+/) : [];
  147 + }
  148 + function getObjTokenizer(tokenizer) {
  149 + return function setKey() {
  150 + var args = [].slice.call(arguments, 0);
  151 + return function tokenize(o) {
  152 + var tokens = [];
  153 + _.each(args, function(k) {
  154 + tokens = tokens.concat(tokenizer(_.toStr(o[k])));
  155 + });
  156 + return tokens;
  157 + };
  158 + };
  159 + }
  160 + }();
  161 + var LruCache = function() {
  162 + "use strict";
  163 + function LruCache(maxSize) {
  164 + this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
  165 + this.reset();
  166 + if (this.maxSize <= 0) {
  167 + this.set = this.get = $.noop;
  168 + }
  169 + }
  170 + _.mixin(LruCache.prototype, {
  171 + set: function set(key, val) {
  172 + var tailItem = this.list.tail, node;
  173 + if (this.size >= this.maxSize) {
  174 + this.list.remove(tailItem);
  175 + delete this.hash[tailItem.key];
  176 + }
  177 + if (node = this.hash[key]) {
  178 + node.val = val;
  179 + this.list.moveToFront(node);
  180 + } else {
  181 + node = new Node(key, val);
  182 + this.list.add(node);
  183 + this.hash[key] = node;
  184 + this.size++;
  185 + }
  186 + },
  187 + get: function get(key) {
  188 + var node = this.hash[key];
  189 + if (node) {
  190 + this.list.moveToFront(node);
  191 + return node.val;
  192 + }
  193 + },
  194 + reset: function reset() {
  195 + this.size = 0;
  196 + this.hash = {};
  197 + this.list = new List();
  198 + }
  199 + });
  200 + function List() {
  201 + this.head = this.tail = null;
  202 + }
  203 + _.mixin(List.prototype, {
  204 + add: function add(node) {
  205 + if (this.head) {
  206 + node.next = this.head;
  207 + this.head.prev = node;
  208 + }
  209 + this.head = node;
  210 + this.tail = this.tail || node;
  211 + },
  212 + remove: function remove(node) {
  213 + node.prev ? node.prev.next = node.next : this.head = node.next;
  214 + node.next ? node.next.prev = node.prev : this.tail = node.prev;
  215 + },
  216 + moveToFront: function(node) {
  217 + this.remove(node);
  218 + this.add(node);
  219 + }
  220 + });
  221 + function Node(key, val) {
  222 + this.key = key;
  223 + this.val = val;
  224 + this.prev = this.next = null;
  225 + }
  226 + return LruCache;
  227 + }();
  228 + var PersistentStorage = function() {
  229 + "use strict";
  230 + var ls, methods;
  231 + try {
  232 + ls = window.localStorage;
  233 + ls.setItem("~~~", "!");
  234 + ls.removeItem("~~~");
  235 + } catch (err) {
  236 + ls = null;
  237 + }
  238 + function PersistentStorage(namespace) {
  239 + this.prefix = [ "__", namespace, "__" ].join("");
  240 + this.ttlKey = "__ttl__";
  241 + this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
  242 + }
  243 + if (ls && window.JSON) {
  244 + methods = {
  245 + _prefix: function(key) {
  246 + return this.prefix + key;
  247 + },
  248 + _ttlKey: function(key) {
  249 + return this._prefix(key) + this.ttlKey;
  250 + },
  251 + get: function(key) {
  252 + if (this.isExpired(key)) {
  253 + this.remove(key);
  254 + }
  255 + return decode(ls.getItem(this._prefix(key)));
  256 + },
  257 + set: function(key, val, ttl) {
  258 + if (_.isNumber(ttl)) {
  259 + ls.setItem(this._ttlKey(key), encode(now() + ttl));
  260 + } else {
  261 + ls.removeItem(this._ttlKey(key));
  262 + }
  263 + return ls.setItem(this._prefix(key), encode(val));
  264 + },
  265 + remove: function(key) {
  266 + ls.removeItem(this._ttlKey(key));
  267 + ls.removeItem(this._prefix(key));
  268 + return this;
  269 + },
  270 + clear: function() {
  271 + var i, key, keys = [], len = ls.length;
  272 + for (i = 0; i < len; i++) {
  273 + if ((key = ls.key(i)).match(this.keyMatcher)) {
  274 + keys.push(key.replace(this.keyMatcher, ""));
  275 + }
  276 + }
  277 + for (i = keys.length; i--; ) {
  278 + this.remove(keys[i]);
  279 + }
  280 + return this;
  281 + },
  282 + isExpired: function(key) {
  283 + var ttl = decode(ls.getItem(this._ttlKey(key)));
  284 + return _.isNumber(ttl) && now() > ttl ? true : false;
  285 + }
  286 + };
  287 + } else {
  288 + methods = {
  289 + get: _.noop,
  290 + set: _.noop,
  291 + remove: _.noop,
  292 + clear: _.noop,
  293 + isExpired: _.noop
  294 + };
  295 + }
  296 + _.mixin(PersistentStorage.prototype, methods);
  297 + return PersistentStorage;
  298 + function now() {
  299 + return new Date().getTime();
  300 + }
  301 + function encode(val) {
  302 + return JSON.stringify(_.isUndefined(val) ? null : val);
  303 + }
  304 + function decode(val) {
  305 + return JSON.parse(val);
  306 + }
  307 + }();
  308 + var Transport = function() {
  309 + "use strict";
  310 + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
  311 + function Transport(o) {
  312 + o = o || {};
  313 + this.cancelled = false;
  314 + this.lastUrl = null;
  315 + this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax;
  316 + this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get;
  317 + this._cache = o.cache === false ? new LruCache(0) : sharedCache;
  318 + }
  319 + Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
  320 + maxPendingRequests = num;
  321 + };
  322 + Transport.resetCache = function resetCache() {
  323 + sharedCache.reset();
  324 + };
  325 + _.mixin(Transport.prototype, {
  326 + _get: function(url, o, cb) {
  327 + var that = this, jqXhr;
  328 + if (this.cancelled || url !== this.lastUrl) {
  329 + return;
  330 + }
  331 + if (jqXhr = pendingRequests[url]) {
  332 + jqXhr.done(done).fail(fail);
  333 + } else if (pendingRequestsCount < maxPendingRequests) {
  334 + pendingRequestsCount++;
  335 + pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always);
  336 + } else {
  337 + this.onDeckRequestArgs = [].slice.call(arguments, 0);
  338 + }
  339 + function done(resp) {
  340 + cb && cb(null, resp);
  341 + that._cache.set(url, resp);
  342 + }
  343 + function fail() {
  344 + cb && cb(true);
  345 + }
  346 + function always() {
  347 + pendingRequestsCount--;
  348 + delete pendingRequests[url];
  349 + if (that.onDeckRequestArgs) {
  350 + that._get.apply(that, that.onDeckRequestArgs);
  351 + that.onDeckRequestArgs = null;
  352 + }
  353 + }
  354 + },
  355 + get: function(url, o, cb) {
  356 + var resp;
  357 + if (_.isFunction(o)) {
  358 + cb = o;
  359 + o = {};
  360 + }
  361 + this.cancelled = false;
  362 + this.lastUrl = url;
  363 + if (resp = this._cache.get(url)) {
  364 + _.defer(function() {
  365 + cb && cb(null, resp);
  366 + });
  367 + } else {
  368 + this._get(url, o, cb);
  369 + }
  370 + return !!resp;
  371 + },
  372 + cancel: function() {
  373 + this.cancelled = true;
  374 + }
  375 + });
  376 + return Transport;
  377 + function callbackToDeferred(fn) {
  378 + return function customSendWrapper(url, o) {
  379 + var deferred = $.Deferred();
  380 + fn(url, o, onSuccess, onError);
  381 + return deferred;
  382 + function onSuccess(resp) {
  383 + _.defer(function() {
  384 + deferred.resolve(resp);
  385 + });
  386 + }
  387 + function onError(err) {
  388 + _.defer(function() {
  389 + deferred.reject(err);
  390 + });
  391 + }
  392 + };
  393 + }
  394 + }();
  395 + var SearchIndex = function() {
  396 + "use strict";
  397 + function SearchIndex(o) {
  398 + o = o || {};
  399 + if (!o.datumTokenizer || !o.queryTokenizer) {
  400 + $.error("datumTokenizer and queryTokenizer are both required");
  401 + }
  402 + this.datumTokenizer = o.datumTokenizer;
  403 + this.queryTokenizer = o.queryTokenizer;
  404 + this.reset();
  405 + }
  406 + _.mixin(SearchIndex.prototype, {
  407 + bootstrap: function bootstrap(o) {
  408 + this.datums = o.datums;
  409 + this.trie = o.trie;
  410 + },
  411 + add: function(data) {
  412 + var that = this;
  413 + data = _.isArray(data) ? data : [ data ];
  414 + _.each(data, function(datum) {
  415 + var id, tokens;
  416 + id = that.datums.push(datum) - 1;
  417 + tokens = normalizeTokens(that.datumTokenizer(datum));
  418 + _.each(tokens, function(token) {
  419 + var node, chars, ch;
  420 + node = that.trie;
  421 + chars = token.split("");
  422 + while (ch = chars.shift()) {
  423 + node = node.children[ch] || (node.children[ch] = newNode());
  424 + node.ids.push(id);
  425 + }
  426 + });
  427 + });
  428 + },
  429 + get: function get(query) {
  430 + var that = this, tokens, matches;
  431 + tokens = normalizeTokens(this.queryTokenizer(query));
  432 + _.each(tokens, function(token) {
  433 + var node, chars, ch, ids;
  434 + if (matches && matches.length === 0) {
  435 + return false;
  436 + }
  437 + node = that.trie;
  438 + chars = token.split("");
  439 + while (node && (ch = chars.shift())) {
  440 + node = node.children[ch];
  441 + }
  442 + if (node && chars.length === 0) {
  443 + ids = node.ids.slice(0);
  444 + matches = matches ? getIntersection(matches, ids) : ids;
  445 + } else {
  446 + matches = [];
  447 + return false;
  448 + }
  449 + });
  450 + return matches ? _.map(unique(matches), function(id) {
  451 + return that.datums[id];
  452 + }) : [];
  453 + },
  454 + reset: function reset() {
  455 + this.datums = [];
  456 + this.trie = newNode();
  457 + },
  458 + serialize: function serialize() {
  459 + return {
  460 + datums: this.datums,
  461 + trie: this.trie
  462 + };
  463 + }
  464 + });
  465 + return SearchIndex;
  466 + function normalizeTokens(tokens) {
  467 + tokens = _.filter(tokens, function(token) {
  468 + return !!token;
  469 + });
  470 + tokens = _.map(tokens, function(token) {
  471 + return token.toLowerCase();
  472 + });
  473 + return tokens;
  474 + }
  475 + function newNode() {
  476 + return {
  477 + ids: [],
  478 + children: {}
  479 + };
  480 + }
  481 + function unique(array) {
  482 + var seen = {}, uniques = [];
  483 + for (var i = 0, len = array.length; i < len; i++) {
  484 + if (!seen[array[i]]) {
  485 + seen[array[i]] = true;
  486 + uniques.push(array[i]);
  487 + }
  488 + }
  489 + return uniques;
  490 + }
  491 + function getIntersection(arrayA, arrayB) {
  492 + var ai = 0, bi = 0, intersection = [];
  493 + arrayA = arrayA.sort(compare);
  494 + arrayB = arrayB.sort(compare);
  495 + var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
  496 + while (ai < lenArrayA && bi < lenArrayB) {
  497 + if (arrayA[ai] < arrayB[bi]) {
  498 + ai++;
  499 + } else if (arrayA[ai] > arrayB[bi]) {
  500 + bi++;
  501 + } else {
  502 + intersection.push(arrayA[ai]);
  503 + ai++;
  504 + bi++;
  505 + }
  506 + }
  507 + return intersection;
  508 + function compare(a, b) {
  509 + return a - b;
  510 + }
  511 + }
  512 + }();
  513 + var oParser = function() {
  514 + "use strict";
  515 + return {
  516 + local: getLocal,
  517 + prefetch: getPrefetch,
  518 + remote: getRemote
  519 + };
  520 + function getLocal(o) {
  521 + return o.local || null;
  522 + }
  523 + function getPrefetch(o) {
  524 + var prefetch, defaults;
  525 + defaults = {
  526 + url: null,
  527 + thumbprint: "",
  528 + ttl: 24 * 60 * 60 * 1e3,
  529 + filter: null,
  530 + ajax: {}
  531 + };
  532 + if (prefetch = o.prefetch || null) {
  533 + prefetch = _.isString(prefetch) ? {
  534 + url: prefetch
  535 + } : prefetch;
  536 + prefetch = _.mixin(defaults, prefetch);
  537 + prefetch.thumbprint = VERSION + prefetch.thumbprint;
  538 + prefetch.ajax.type = prefetch.ajax.type || "GET";
  539 + prefetch.ajax.dataType = prefetch.ajax.dataType || "json";
  540 + !prefetch.url && $.error("prefetch requires url to be set");
  541 + }
  542 + return prefetch;
  543 + }
  544 + function getRemote(o) {
  545 + var remote, defaults;
  546 + defaults = {
  547 + url: null,
  548 + cache: true,
  549 + wildcard: "%QUERY",
  550 + replace: null,
  551 + rateLimitBy: "debounce",
  552 + rateLimitWait: 300,
  553 + send: null,
  554 + filter: null,
  555 + ajax: {}
  556 + };
  557 + if (remote = o.remote || null) {
  558 + remote = _.isString(remote) ? {
  559 + url: remote
  560 + } : remote;
  561 + remote = _.mixin(defaults, remote);
  562 + remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait);
  563 + remote.ajax.type = remote.ajax.type || "GET";
  564 + remote.ajax.dataType = remote.ajax.dataType || "json";
  565 + delete remote.rateLimitBy;
  566 + delete remote.rateLimitWait;
  567 + !remote.url && $.error("remote requires url to be set");
  568 + }
  569 + return remote;
  570 + function byDebounce(wait) {
  571 + return function(fn) {
  572 + return _.debounce(fn, wait);
  573 + };
  574 + }
  575 + function byThrottle(wait) {
  576 + return function(fn) {
  577 + return _.throttle(fn, wait);
  578 + };
  579 + }
  580 + }
  581 + }();
  582 + (function(root) {
  583 + "use strict";
  584 + var old, keys;
  585 + old = root.Bloodhound;
  586 + keys = {
  587 + data: "data",
  588 + protocol: "protocol",
  589 + thumbprint: "thumbprint"
  590 + };
  591 + root.Bloodhound = Bloodhound;
  592 + function Bloodhound(o) {
  593 + if (!o || !o.local && !o.prefetch && !o.remote) {
  594 + $.error("one of local, prefetch, or remote is required");
  595 + }
  596 + this.limit = o.limit || 5;
  597 + this.sorter = getSorter(o.sorter);
  598 + this.dupDetector = o.dupDetector || ignoreDuplicates;
  599 + this.local = oParser.local(o);
  600 + this.prefetch = oParser.prefetch(o);
  601 + this.remote = oParser.remote(o);
  602 + this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null;
  603 + this.index = new SearchIndex({
  604 + datumTokenizer: o.datumTokenizer,
  605 + queryTokenizer: o.queryTokenizer
  606 + });
  607 + this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;
  608 + }
  609 + Bloodhound.noConflict = function noConflict() {
  610 + root.Bloodhound = old;
  611 + return Bloodhound;
  612 + };
  613 + Bloodhound.tokenizers = tokenizers;
  614 + _.mixin(Bloodhound.prototype, {
  615 + _loadPrefetch: function loadPrefetch(o) {
  616 + var that = this, serialized, deferred;
  617 + if (serialized = this._readFromStorage(o.thumbprint)) {
  618 + this.index.bootstrap(serialized);
  619 + deferred = $.Deferred().resolve();
  620 + } else {
  621 + deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);
  622 + }
  623 + return deferred;
  624 + function handlePrefetchResponse(resp) {
  625 + that.clear();
  626 + that.add(o.filter ? o.filter(resp) : resp);
  627 + that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);
  628 + }
  629 + },
  630 + _getFromRemote: function getFromRemote(query, cb) {
  631 + var that = this, url, uriEncodedQuery;
  632 + if (!this.transport) {
  633 + return;
  634 + }
  635 + query = query || "";
  636 + uriEncodedQuery = encodeURIComponent(query);
  637 + url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);
  638 + return this.transport.get(url, this.remote.ajax, handleRemoteResponse);
  639 + function handleRemoteResponse(err, resp) {
  640 + err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);
  641 + }
  642 + },
  643 + _cancelLastRemoteRequest: function cancelLastRemoteRequest() {
  644 + this.transport && this.transport.cancel();
  645 + },
  646 + _saveToStorage: function saveToStorage(data, thumbprint, ttl) {
  647 + if (this.storage) {
  648 + this.storage.set(keys.data, data, ttl);
  649 + this.storage.set(keys.protocol, location.protocol, ttl);
  650 + this.storage.set(keys.thumbprint, thumbprint, ttl);
  651 + }
  652 + },
  653 + _readFromStorage: function readFromStorage(thumbprint) {
  654 + var stored = {}, isExpired;
  655 + if (this.storage) {
  656 + stored.data = this.storage.get(keys.data);
  657 + stored.protocol = this.storage.get(keys.protocol);
  658 + stored.thumbprint = this.storage.get(keys.thumbprint);
  659 + }
  660 + isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol;
  661 + return stored.data && !isExpired ? stored.data : null;
  662 + },
  663 + _initialize: function initialize() {
  664 + var that = this, local = this.local, deferred;
  665 + deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve();
  666 + local && deferred.done(addLocalToIndex);
  667 + this.transport = this.remote ? new Transport(this.remote) : null;
  668 + return this.initPromise = deferred.promise();
  669 + function addLocalToIndex() {
  670 + that.add(_.isFunction(local) ? local() : local);
  671 + }
  672 + },
  673 + initialize: function initialize(force) {
  674 + return !this.initPromise || force ? this._initialize() : this.initPromise;
  675 + },
  676 + add: function add(data) {
  677 + this.index.add(data);
  678 + },
  679 + get: function get(query, cb) {
  680 + var that = this, matches = [], cacheHit = false;
  681 + matches = this.index.get(query);
  682 + matches = this.sorter(matches).slice(0, this.limit);
  683 + matches.length < this.limit ? cacheHit = this._getFromRemote(query, returnRemoteMatches) : this._cancelLastRemoteRequest();
  684 + if (!cacheHit) {
  685 + (matches.length > 0 || !this.transport) && cb && cb(matches);
  686 + }
  687 + function returnRemoteMatches(remoteMatches) {
  688 + var matchesWithBackfill = matches.slice(0);
  689 + _.each(remoteMatches, function(remoteMatch) {
  690 + var isDuplicate;
  691 + isDuplicate = _.some(matchesWithBackfill, function(match) {
  692 + return that.dupDetector(remoteMatch, match);
  693 + });
  694 + !isDuplicate && matchesWithBackfill.push(remoteMatch);
  695 + return matchesWithBackfill.length < that.limit;
  696 + });
  697 + cb && cb(that.sorter(matchesWithBackfill));
  698 + }
  699 + },
  700 + clear: function clear() {
  701 + this.index.reset();
  702 + },
  703 + clearPrefetchCache: function clearPrefetchCache() {
  704 + this.storage && this.storage.clear();
  705 + },
  706 + clearRemoteCache: function clearRemoteCache() {
  707 + this.transport && Transport.resetCache();
  708 + },
  709 + ttAdapter: function ttAdapter() {
  710 + return _.bind(this.get, this);
  711 + }
  712 + });
  713 + return Bloodhound;
  714 + function getSorter(sortFn) {
  715 + return _.isFunction(sortFn) ? sort : noSort;
  716 + function sort(array) {
  717 + return array.sort(sortFn);
  718 + }
  719 + function noSort(array) {
  720 + return array;
  721 + }
  722 + }
  723 + function ignoreDuplicates() {
  724 + return false;
  725 + }
  726 + })(this);
  727 + var html = function() {
  728 + return {
  729 + wrapper: '<span class="twitter-typeahead"></span>',
  730 + dropdown: '<span class="tt-dropdown-menu"></span>',
  731 + dataset: '<div class="tt-dataset-%CLASS%"></div>',
  732 + suggestions: '<span class="tt-suggestions"></span>',
  733 + suggestion: '<div class="tt-suggestion"></div>'
  734 + };
  735 + }();
  736 + var css = function() {
  737 + "use strict";
  738 + var css = {
  739 + wrapper: {
  740 + position: "relative",
  741 + display: "inline-block"
  742 + },
  743 + hint: {
  744 + position: "absolute",
  745 + top: "0",
  746 + left: "0",
  747 + borderColor: "transparent",
  748 + boxShadow: "none",
  749 + opacity: "1"
  750 + },
  751 + input: {
  752 + position: "relative",
  753 + verticalAlign: "top",
  754 + backgroundColor: "transparent"
  755 + },
  756 + inputWithNoHint: {
  757 + position: "relative",
  758 + verticalAlign: "top"
  759 + },
  760 + dropdown: {
  761 + position: "absolute",
  762 + top: "100%",
  763 + left: "0",
  764 + zIndex: "100",
  765 + display: "none"
  766 + },
  767 + suggestions: {
  768 + display: "block"
  769 + },
  770 + suggestion: {
  771 + whiteSpace: "nowrap",
  772 + cursor: "pointer"
  773 + },
  774 + suggestionChild: {
  775 + whiteSpace: "normal"
  776 + },
  777 + ltr: {
  778 + left: "0",
  779 + right: "auto"
  780 + },
  781 + rtl: {
  782 + left: "auto",
  783 + right: " 0"
  784 + }
  785 + };
  786 + if (_.isMsie()) {
  787 + _.mixin(css.input, {
  788 + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
  789 + });
  790 + }
  791 + if (_.isMsie() && _.isMsie() <= 7) {
  792 + _.mixin(css.input, {
  793 + marginTop: "-1px"
  794 + });
  795 + }
  796 + return css;
  797 + }();
  798 + var EventBus = function() {
  799 + "use strict";
  800 + var namespace = "typeahead:";
  801 + function EventBus(o) {
  802 + if (!o || !o.el) {
  803 + $.error("EventBus initialized without el");
  804 + }
  805 + this.$el = $(o.el);
  806 + }
  807 + _.mixin(EventBus.prototype, {
  808 + trigger: function(type) {
  809 + var args = [].slice.call(arguments, 1);
  810 + this.$el.trigger(namespace + type, args);
  811 + }
  812 + });
  813 + return EventBus;
  814 + }();
  815 + var EventEmitter = function() {
  816 + "use strict";
  817 + var splitter = /\s+/, nextTick = getNextTick();
  818 + return {
  819 + onSync: onSync,
  820 + onAsync: onAsync,
  821 + off: off,
  822 + trigger: trigger
  823 + };
  824 + function on(method, types, cb, context) {
  825 + var type;
  826 + if (!cb) {
  827 + return this;
  828 + }
  829 + types = types.split(splitter);
  830 + cb = context ? bindContext(cb, context) : cb;
  831 + this._callbacks = this._callbacks || {};
  832 + while (type = types.shift()) {
  833 + this._callbacks[type] = this._callbacks[type] || {
  834 + sync: [],
  835 + async: []
  836 + };
  837 + this._callbacks[type][method].push(cb);
  838 + }
  839 + return this;
  840 + }
  841 + function onAsync(types, cb, context) {
  842 + return on.call(this, "async", types, cb, context);
  843 + }
  844 + function onSync(types, cb, context) {
  845 + return on.call(this, "sync", types, cb, context);
  846 + }
  847 + function off(types) {
  848 + var type;
  849 + if (!this._callbacks) {
  850 + return this;
  851 + }
  852 + types = types.split(splitter);
  853 + while (type = types.shift()) {
  854 + delete this._callbacks[type];
  855 + }
  856 + return this;
  857 + }
  858 + function trigger(types) {
  859 + var type, callbacks, args, syncFlush, asyncFlush;
  860 + if (!this._callbacks) {
  861 + return this;
  862 + }
  863 + types = types.split(splitter);
  864 + args = [].slice.call(arguments, 1);
  865 + while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
  866 + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
  867 + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
  868 + syncFlush() && nextTick(asyncFlush);
  869 + }
  870 + return this;
  871 + }
  872 + function getFlush(callbacks, context, args) {
  873 + return flush;
  874 + function flush() {
  875 + var cancelled;
  876 + for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
  877 + cancelled = callbacks[i].apply(context, args) === false;
  878 + }
  879 + return !cancelled;
  880 + }
  881 + }
  882 + function getNextTick() {
  883 + var nextTickFn;
  884 + if (window.setImmediate) {
  885 + nextTickFn = function nextTickSetImmediate(fn) {
  886 + setImmediate(function() {
  887 + fn();
  888 + });
  889 + };
  890 + } else {
  891 + nextTickFn = function nextTickSetTimeout(fn) {
  892 + setTimeout(function() {
  893 + fn();
  894 + }, 0);
  895 + };
  896 + }
  897 + return nextTickFn;
  898 + }
  899 + function bindContext(fn, context) {
  900 + return fn.bind ? fn.bind(context) : function() {
  901 + fn.apply(context, [].slice.call(arguments, 0));
  902 + };
  903 + }
  904 + }();
  905 + var highlight = function(doc) {
  906 + "use strict";
  907 + var defaults = {
  908 + node: null,
  909 + pattern: null,
  910 + tagName: "strong",
  911 + className: null,
  912 + wordsOnly: false,
  913 + caseSensitive: false
  914 + };
  915 + return function hightlight(o) {
  916 + var regex;
  917 + o = _.mixin({}, defaults, o);
  918 + if (!o.node || !o.pattern) {
  919 + return;
  920 + }
  921 + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
  922 + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
  923 + traverse(o.node, hightlightTextNode);
  924 + function hightlightTextNode(textNode) {
  925 + var match, patternNode, wrapperNode;
  926 + if (match = regex.exec(textNode.data)) {
  927 + wrapperNode = doc.createElement(o.tagName);
  928 + o.className && (wrapperNode.className = o.className);
  929 + patternNode = textNode.splitText(match.index);
  930 + patternNode.splitText(match[0].length);
  931 + wrapperNode.appendChild(patternNode.cloneNode(true));
  932 + textNode.parentNode.replaceChild(wrapperNode, patternNode);
  933 + }
  934 + return !!match;
  935 + }
  936 + function traverse(el, hightlightTextNode) {
  937 + var childNode, TEXT_NODE_TYPE = 3;
  938 + for (var i = 0; i < el.childNodes.length; i++) {
  939 + childNode = el.childNodes[i];
  940 + if (childNode.nodeType === TEXT_NODE_TYPE) {
  941 + i += hightlightTextNode(childNode) ? 1 : 0;
  942 + } else {
  943 + traverse(childNode, hightlightTextNode);
  944 + }
  945 + }
  946 + }
  947 + };
  948 + function getRegex(patterns, caseSensitive, wordsOnly) {
  949 + var escapedPatterns = [], regexStr;
  950 + for (var i = 0, len = patterns.length; i < len; i++) {
  951 + escapedPatterns.push(_.escapeRegExChars(patterns[i]));
  952 + }
  953 + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
  954 + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
  955 + }
  956 + }(window.document);
  957 + var Input = function() {
  958 + "use strict";
  959 + var specialKeyCodeMap;
  960 + specialKeyCodeMap = {
  961 + 9: "tab",
  962 + 27: "esc",
  963 + 37: "left",
  964 + 39: "right",
  965 + 13: "enter",
  966 + 38: "up",
  967 + 40: "down"
  968 + };
  969 + function Input(o) {
  970 + var that = this, onBlur, onFocus, onKeydown, onInput;
  971 + o = o || {};
  972 + if (!o.input) {
  973 + $.error("input is missing");
  974 + }
  975 + onBlur = _.bind(this._onBlur, this);
  976 + onFocus = _.bind(this._onFocus, this);
  977 + onKeydown = _.bind(this._onKeydown, this);
  978 + onInput = _.bind(this._onInput, this);
  979 + this.$hint = $(o.hint);
  980 + this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
  981 + if (this.$hint.length === 0) {
  982 + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
  983 + }
  984 + if (!_.isMsie()) {
  985 + this.$input.on("input.tt", onInput);
  986 + } else {
  987 + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
  988 + if (specialKeyCodeMap[$e.which || $e.keyCode]) {
  989 + return;
  990 + }
  991 + _.defer(_.bind(that._onInput, that, $e));
  992 + });
  993 + }
  994 + this.query = this.$input.val();
  995 + this.$overflowHelper = buildOverflowHelper(this.$input);
  996 + }
  997 + Input.normalizeQuery = function(str) {
  998 + return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
  999 + };
  1000 + _.mixin(Input.prototype, EventEmitter, {
  1001 + _onBlur: function onBlur() {
  1002 + this.resetInputValue();
  1003 + this.trigger("blurred");
  1004 + },
  1005 + _onFocus: function onFocus() {
  1006 + this.trigger("focused");
  1007 + },
  1008 + _onKeydown: function onKeydown($e) {
  1009 + var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
  1010 + this._managePreventDefault(keyName, $e);
  1011 + if (keyName && this._shouldTrigger(keyName, $e)) {
  1012 + this.trigger(keyName + "Keyed", $e);
  1013 + }
  1014 + },
  1015 + _onInput: function onInput() {
  1016 + this._checkInputValue();
  1017 + },
  1018 + _managePreventDefault: function managePreventDefault(keyName, $e) {
  1019 + var preventDefault, hintValue, inputValue;
  1020 + switch (keyName) {
  1021 + case "tab":
  1022 + hintValue = this.getHint();
  1023 + inputValue = this.getInputValue();
  1024 + preventDefault = hintValue && hintValue !== inputValue && !withModifier($e);
  1025 + break;
  1026 +
  1027 + case "up":
  1028 + case "down":
  1029 + preventDefault = !withModifier($e);
  1030 + break;
  1031 +
  1032 + default:
  1033 + preventDefault = false;
  1034 + }
  1035 + preventDefault && $e.preventDefault();
  1036 + },
  1037 + _shouldTrigger: function shouldTrigger(keyName, $e) {
  1038 + var trigger;
  1039 + switch (keyName) {
  1040 + case "tab":
  1041 + trigger = !withModifier($e);
  1042 + break;
  1043 +
  1044 + default:
  1045 + trigger = true;
  1046 + }
  1047 + return trigger;
  1048 + },
  1049 + _checkInputValue: function checkInputValue() {
  1050 + var inputValue, areEquivalent, hasDifferentWhitespace;
  1051 + inputValue = this.getInputValue();
  1052 + areEquivalent = areQueriesEquivalent(inputValue, this.query);
  1053 + hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false;
  1054 + this.query = inputValue;
  1055 + if (!areEquivalent) {
  1056 + this.trigger("queryChanged", this.query);
  1057 + } else if (hasDifferentWhitespace) {
  1058 + this.trigger("whitespaceChanged", this.query);
  1059 + }
  1060 + },
  1061 + focus: function focus() {
  1062 + this.$input.focus();
  1063 + },
  1064 + blur: function blur() {
  1065 + this.$input.blur();
  1066 + },
  1067 + getQuery: function getQuery() {
  1068 + return this.query;
  1069 + },
  1070 + setQuery: function setQuery(query) {
  1071 + this.query = query;
  1072 + },
  1073 + getInputValue: function getInputValue() {
  1074 + return this.$input.val();
  1075 + },
  1076 + setInputValue: function setInputValue(value, silent) {
  1077 + this.$input.val(value);
  1078 + silent ? this.clearHint() : this._checkInputValue();
  1079 + },
  1080 + resetInputValue: function resetInputValue() {
  1081 + this.setInputValue(this.query, true);
  1082 + },
  1083 + getHint: function getHint() {
  1084 + return this.$hint.val();
  1085 + },
  1086 + setHint: function setHint(value) {
  1087 + this.$hint.val(value);
  1088 + },
  1089 + clearHint: function clearHint() {
  1090 + this.setHint("");
  1091 + },
  1092 + clearHintIfInvalid: function clearHintIfInvalid() {
  1093 + var val, hint, valIsPrefixOfHint, isValid;
  1094 + val = this.getInputValue();
  1095 + hint = this.getHint();
  1096 + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
  1097 + isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
  1098 + !isValid && this.clearHint();
  1099 + },
  1100 + getLanguageDirection: function getLanguageDirection() {
  1101 + return (this.$input.css("direction") || "ltr").toLowerCase();
  1102 + },
  1103 + hasOverflow: function hasOverflow() {
  1104 + var constraint = this.$input.width() - 2;
  1105 + this.$overflowHelper.text(this.getInputValue());
  1106 + return this.$overflowHelper.width() >= constraint;
  1107 + },
  1108 + isCursorAtEnd: function() {
  1109 + var valueLength, selectionStart, range;
  1110 + valueLength = this.$input.val().length;
  1111 + selectionStart = this.$input[0].selectionStart;
  1112 + if (_.isNumber(selectionStart)) {
  1113 + return selectionStart === valueLength;
  1114 + } else if (document.selection) {
  1115 + range = document.selection.createRange();
  1116 + range.moveStart("character", -valueLength);
  1117 + return valueLength === range.text.length;
  1118 + }
  1119 + return true;
  1120 + },
  1121 + destroy: function destroy() {
  1122 + this.$hint.off(".tt");
  1123 + this.$input.off(".tt");
  1124 + this.$hint = this.$input = this.$overflowHelper = null;
  1125 + }
  1126 + });
  1127 + return Input;
  1128 + function buildOverflowHelper($input) {
  1129 + return $('<pre aria-hidden="true"></pre>').css({
  1130 + position: "absolute",
  1131 + visibility: "hidden",
  1132 + whiteSpace: "pre",
  1133 + fontFamily: $input.css("font-family"),
  1134 + fontSize: $input.css("font-size"),
  1135 + fontStyle: $input.css("font-style"),
  1136 + fontVariant: $input.css("font-variant"),
  1137 + fontWeight: $input.css("font-weight"),
  1138 + wordSpacing: $input.css("word-spacing"),
  1139 + letterSpacing: $input.css("letter-spacing"),
  1140 + textIndent: $input.css("text-indent"),
  1141 + textRendering: $input.css("text-rendering"),
  1142 + textTransform: $input.css("text-transform")
  1143 + }).insertAfter($input);
  1144 + }
  1145 + function areQueriesEquivalent(a, b) {
  1146 + return Input.normalizeQuery(a) === Input.normalizeQuery(b);
  1147 + }
  1148 + function withModifier($e) {
  1149 + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
  1150 + }
  1151 + }();
  1152 + var Dataset = function() {
  1153 + "use strict";
  1154 + var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum";
  1155 + function Dataset(o) {
  1156 + o = o || {};
  1157 + o.templates = o.templates || {};
  1158 + if (!o.source) {
  1159 + $.error("missing source");
  1160 + }
  1161 + if (o.name && !isValidName(o.name)) {
  1162 + $.error("invalid dataset name: " + o.name);
  1163 + }
  1164 + this.query = null;
  1165 + this.highlight = !!o.highlight;
  1166 + this.name = o.name || _.getUniqueId();
  1167 + this.source = o.source;
  1168 + this.displayFn = getDisplayFn(o.display || o.displayKey);
  1169 + this.templates = getTemplates(o.templates, this.displayFn);
  1170 + this.$el = $(html.dataset.replace("%CLASS%", this.name));
  1171 + }
  1172 + Dataset.extractDatasetName = function extractDatasetName(el) {
  1173 + return $(el).data(datasetKey);
  1174 + };
  1175 + Dataset.extractValue = function extractDatum(el) {
  1176 + return $(el).data(valueKey);
  1177 + };
  1178 + Dataset.extractDatum = function extractDatum(el) {
  1179 + return $(el).data(datumKey);
  1180 + };
  1181 + _.mixin(Dataset.prototype, EventEmitter, {
  1182 + _render: function render(query, suggestions) {
  1183 + if (!this.$el) {
  1184 + return;
  1185 + }
  1186 + var that = this, hasSuggestions;
  1187 + this.$el.empty();
  1188 + hasSuggestions = suggestions && suggestions.length;
  1189 + if (!hasSuggestions && this.templates.empty) {
  1190 + this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
  1191 + } else if (hasSuggestions) {
  1192 + this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
  1193 + }
  1194 + this.trigger("rendered");
  1195 + function getEmptyHtml() {
  1196 + return that.templates.empty({
  1197 + query: query,
  1198 + isEmpty: true
  1199 + });
  1200 + }
  1201 + function getSuggestionsHtml() {
  1202 + var $suggestions, nodes;
  1203 + $suggestions = $(html.suggestions).css(css.suggestions);
  1204 + nodes = _.map(suggestions, getSuggestionNode);
  1205 + $suggestions.append.apply($suggestions, nodes);
  1206 + that.highlight && highlight({
  1207 + className: "tt-highlight",
  1208 + node: $suggestions[0],
  1209 + pattern: query
  1210 + });
  1211 + return $suggestions;
  1212 + function getSuggestionNode(suggestion) {
  1213 + var $el;
  1214 + $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion);
  1215 + $el.children().each(function() {
  1216 + $(this).css(css.suggestionChild);
  1217 + });
  1218 + return $el;
  1219 + }
  1220 + }
  1221 + function getHeaderHtml() {
  1222 + return that.templates.header({
  1223 + query: query,
  1224 + isEmpty: !hasSuggestions
  1225 + });
  1226 + }
  1227 + function getFooterHtml() {
  1228 + return that.templates.footer({
  1229 + query: query,
  1230 + isEmpty: !hasSuggestions
  1231 + });
  1232 + }
  1233 + },
  1234 + getRoot: function getRoot() {
  1235 + return this.$el;
  1236 + },
  1237 + update: function update(query) {
  1238 + var that = this;
  1239 + this.query = query;
  1240 + this.canceled = false;
  1241 + this.source(query, render);
  1242 + function render(suggestions) {
  1243 + if (!that.canceled && query === that.query) {
  1244 + that._render(query, suggestions);
  1245 + }
  1246 + }
  1247 + },
  1248 + cancel: function cancel() {
  1249 + this.canceled = true;
  1250 + },
  1251 + clear: function clear() {
  1252 + this.cancel();
  1253 + this.$el.empty();
  1254 + this.trigger("rendered");
  1255 + },
  1256 + isEmpty: function isEmpty() {
  1257 + return this.$el.is(":empty");
  1258 + },
  1259 + destroy: function destroy() {
  1260 + this.$el = null;
  1261 + }
  1262 + });
  1263 + return Dataset;
  1264 + function getDisplayFn(display) {
  1265 + display = display || "value";
  1266 + return _.isFunction(display) ? display : displayFn;
  1267 + function displayFn(obj) {
  1268 + return obj[display];
  1269 + }
  1270 + }
  1271 + function getTemplates(templates, displayFn) {
  1272 + return {
  1273 + empty: templates.empty && _.templatify(templates.empty),
  1274 + header: templates.header && _.templatify(templates.header),
  1275 + footer: templates.footer && _.templatify(templates.footer),
  1276 + suggestion: templates.suggestion || suggestionTemplate
  1277 + };
  1278 + function suggestionTemplate(context) {
  1279 + return "<p>" + displayFn(context) + "</p>";
  1280 + }
  1281 + }
  1282 + function isValidName(str) {
  1283 + return /^[_a-zA-Z0-9-]+$/.test(str);
  1284 + }
  1285 + }();
  1286 + var Dropdown = function() {
  1287 + "use strict";
  1288 + function Dropdown(o) {
  1289 + var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave;
  1290 + o = o || {};
  1291 + if (!o.menu) {
  1292 + $.error("menu is required");
  1293 + }
  1294 + this.isOpen = false;
  1295 + this.isEmpty = true;
  1296 + this.datasets = _.map(o.datasets, initializeDataset);
  1297 + onSuggestionClick = _.bind(this._onSuggestionClick, this);
  1298 + onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
  1299 + onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);
  1300 + this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave);
  1301 + _.each(this.datasets, function(dataset) {
  1302 + that.$menu.append(dataset.getRoot());
  1303 + dataset.onSync("rendered", that._onRendered, that);
  1304 + });
  1305 + }
  1306 + _.mixin(Dropdown.prototype, EventEmitter, {
  1307 + _onSuggestionClick: function onSuggestionClick($e) {
  1308 + this.trigger("suggestionClicked", $($e.currentTarget));
  1309 + },
  1310 + _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
  1311 + this._removeCursor();
  1312 + this._setCursor($($e.currentTarget), true);
  1313 + },
  1314 + _onSuggestionMouseLeave: function onSuggestionMouseLeave() {
  1315 + this._removeCursor();
  1316 + },
  1317 + _onRendered: function onRendered() {
  1318 + this.isEmpty = _.every(this.datasets, isDatasetEmpty);
  1319 + this.isEmpty ? this._hide() : this.isOpen && this._show();
  1320 + this.trigger("datasetRendered");
  1321 + function isDatasetEmpty(dataset) {
  1322 + return dataset.isEmpty();
  1323 + }
  1324 + },
  1325 + _hide: function() {
  1326 + this.$menu.hide();
  1327 + },
  1328 + _show: function() {
  1329 + this.$menu.css("display", "block");
  1330 + },
  1331 + _getSuggestions: function getSuggestions() {
  1332 + return this.$menu.find(".tt-suggestion");
  1333 + },
  1334 + _getCursor: function getCursor() {
  1335 + return this.$menu.find(".tt-cursor").first();
  1336 + },
  1337 + _setCursor: function setCursor($el, silent) {
  1338 + $el.first().addClass("tt-cursor");
  1339 + !silent && this.trigger("cursorMoved");
  1340 + },
  1341 + _removeCursor: function removeCursor() {
  1342 + this._getCursor().removeClass("tt-cursor");
  1343 + },
  1344 + _moveCursor: function moveCursor(increment) {
  1345 + var $suggestions, $oldCursor, newCursorIndex, $newCursor;
  1346 + if (!this.isOpen) {
  1347 + return;
  1348 + }
  1349 + $oldCursor = this._getCursor();
  1350 + $suggestions = this._getSuggestions();
  1351 + this._removeCursor();
  1352 + newCursorIndex = $suggestions.index($oldCursor) + increment;
  1353 + newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
  1354 + if (newCursorIndex === -1) {
  1355 + this.trigger("cursorRemoved");
  1356 + return;
  1357 + } else if (newCursorIndex < -1) {
  1358 + newCursorIndex = $suggestions.length - 1;
  1359 + }
  1360 + this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
  1361 + this._ensureVisible($newCursor);
  1362 + },
  1363 + _ensureVisible: function ensureVisible($el) {
  1364 + var elTop, elBottom, menuScrollTop, menuHeight;
  1365 + elTop = $el.position().top;
  1366 + elBottom = elTop + $el.outerHeight(true);
  1367 + menuScrollTop = this.$menu.scrollTop();
  1368 + menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10);
  1369 + if (elTop < 0) {
  1370 + this.$menu.scrollTop(menuScrollTop + elTop);
  1371 + } else if (menuHeight < elBottom) {
  1372 + this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
  1373 + }
  1374 + },
  1375 + close: function close() {
  1376 + if (this.isOpen) {
  1377 + this.isOpen = false;
  1378 + this._removeCursor();
  1379 + this._hide();
  1380 + this.trigger("closed");
  1381 + }
  1382 + },
  1383 + open: function open() {
  1384 + if (!this.isOpen) {
  1385 + this.isOpen = true;
  1386 + !this.isEmpty && this._show();
  1387 + this.trigger("opened");
  1388 + }
  1389 + },
  1390 + setLanguageDirection: function setLanguageDirection(dir) {
  1391 + this.$menu.css(dir === "ltr" ? css.ltr : css.rtl);
  1392 + },
  1393 + moveCursorUp: function moveCursorUp() {
  1394 + this._moveCursor(-1);
  1395 + },
  1396 + moveCursorDown: function moveCursorDown() {
  1397 + this._moveCursor(+1);
  1398 + },
  1399 + getDatumForSuggestion: function getDatumForSuggestion($el) {
  1400 + var datum = null;
  1401 + if ($el.length) {
  1402 + datum = {
  1403 + raw: Dataset.extractDatum($el),
  1404 + value: Dataset.extractValue($el),
  1405 + datasetName: Dataset.extractDatasetName($el)
  1406 + };
  1407 + }
  1408 + return datum;
  1409 + },
  1410 + getDatumForCursor: function getDatumForCursor() {
  1411 + return this.getDatumForSuggestion(this._getCursor().first());
  1412 + },
  1413 + getDatumForTopSuggestion: function getDatumForTopSuggestion() {
  1414 + return this.getDatumForSuggestion(this._getSuggestions().first());
  1415 + },
  1416 + update: function update(query) {
  1417 + _.each(this.datasets, updateDataset);
  1418 + function updateDataset(dataset) {
  1419 + dataset.update(query);
  1420 + }
  1421 + },
  1422 + empty: function empty() {
  1423 + _.each(this.datasets, clearDataset);
  1424 + this.isEmpty = true;
  1425 + function clearDataset(dataset) {
  1426 + dataset.clear();
  1427 + }
  1428 + },
  1429 + isVisible: function isVisible() {
  1430 + return this.isOpen && !this.isEmpty;
  1431 + },
  1432 + destroy: function destroy() {
  1433 + this.$menu.off(".tt");
  1434 + this.$menu = null;
  1435 + _.each(this.datasets, destroyDataset);
  1436 + function destroyDataset(dataset) {
  1437 + dataset.destroy();
  1438 + }
  1439 + }
  1440 + });
  1441 + return Dropdown;
  1442 + function initializeDataset(oDataset) {
  1443 + return new Dataset(oDataset);
  1444 + }
  1445 + }();
  1446 + var Typeahead = function() {
  1447 + "use strict";
  1448 + var attrsKey = "ttAttrs";
  1449 + function Typeahead(o) {
  1450 + var $menu, $input, $hint;
  1451 + o = o || {};
  1452 + if (!o.input) {
  1453 + $.error("missing input");
  1454 + }
  1455 + this.isActivated = false;
  1456 + this.autoselect = !!o.autoselect;
  1457 + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
  1458 + this.$node = buildDom(o.input, o.withHint);
  1459 + $menu = this.$node.find(".tt-dropdown-menu");
  1460 + $input = this.$node.find(".tt-input");
  1461 + $hint = this.$node.find(".tt-hint");
  1462 + $input.on("blur.tt", function($e) {
  1463 + var active, isActive, hasActive;
  1464 + active = document.activeElement;
  1465 + isActive = $menu.is(active);
  1466 + hasActive = $menu.has(active).length > 0;
  1467 + if (_.isMsie() && (isActive || hasActive)) {
  1468 + $e.preventDefault();
  1469 + $e.stopImmediatePropagation();
  1470 + _.defer(function() {
  1471 + $input.focus();
  1472 + });
  1473 + }
  1474 + });
  1475 + $menu.on("mousedown.tt", function($e) {
  1476 + $e.preventDefault();
  1477 + });
  1478 + this.eventBus = o.eventBus || new EventBus({
  1479 + el: $input
  1480 + });
  1481 + this.dropdown = new Dropdown({
  1482 + menu: $menu,
  1483 + datasets: o.datasets
  1484 + }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this);
  1485 + this.input = new Input({
  1486 + input: $input,
  1487 + hint: $hint
  1488 + }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this);
  1489 + this._setLanguageDirection();
  1490 + }
  1491 + _.mixin(Typeahead.prototype, {
  1492 + _onSuggestionClicked: function onSuggestionClicked(type, $el) {
  1493 + var datum;
  1494 + if (datum = this.dropdown.getDatumForSuggestion($el)) {
  1495 + this._select(datum);
  1496 + }
  1497 + },
  1498 + _onCursorMoved: function onCursorMoved() {
  1499 + var datum = this.dropdown.getDatumForCursor();
  1500 + this.input.setInputValue(datum.value, true);
  1501 + this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName);
  1502 + },
  1503 + _onCursorRemoved: function onCursorRemoved() {
  1504 + this.input.resetInputValue();
  1505 + this._updateHint();
  1506 + },
  1507 + _onDatasetRendered: function onDatasetRendered() {
  1508 + this._updateHint();
  1509 + },
  1510 + _onOpened: function onOpened() {
  1511 + this._updateHint();
  1512 + this.eventBus.trigger("opened");
  1513 + },
  1514 + _onClosed: function onClosed() {
  1515 + this.input.clearHint();
  1516 + this.eventBus.trigger("closed");
  1517 + },
  1518 + _onFocused: function onFocused() {
  1519 + this.isActivated = true;
  1520 + this.dropdown.open();
  1521 + },
  1522 + _onBlurred: function onBlurred() {
  1523 + this.isActivated = false;
  1524 + this.dropdown.empty();
  1525 + this.dropdown.close();
  1526 + },
  1527 + _onEnterKeyed: function onEnterKeyed(type, $e) {
  1528 + var cursorDatum, topSuggestionDatum;
  1529 + cursorDatum = this.dropdown.getDatumForCursor();
  1530 + topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
  1531 + if (cursorDatum) {
  1532 + this._select(cursorDatum);
  1533 + $e.preventDefault();
  1534 + } else if (this.autoselect && topSuggestionDatum) {
  1535 + this._select(topSuggestionDatum);
  1536 + $e.preventDefault();
  1537 + }
  1538 + },
  1539 + _onTabKeyed: function onTabKeyed(type, $e) {
  1540 + var datum;
  1541 + if (datum = this.dropdown.getDatumForCursor()) {
  1542 + this._select(datum);
  1543 + $e.preventDefault();
  1544 + } else {
  1545 + this._autocomplete(true);
  1546 + }
  1547 + },
  1548 + _onEscKeyed: function onEscKeyed() {
  1549 + this.dropdown.close();
  1550 + this.input.resetInputValue();
  1551 + },
  1552 + _onUpKeyed: function onUpKeyed() {
  1553 + var query = this.input.getQuery();
  1554 + this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp();
  1555 + this.dropdown.open();
  1556 + },
  1557 + _onDownKeyed: function onDownKeyed() {
  1558 + var query = this.input.getQuery();
  1559 + this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown();
  1560 + this.dropdown.open();
  1561 + },
  1562 + _onLeftKeyed: function onLeftKeyed() {
  1563 + this.dir === "rtl" && this._autocomplete();
  1564 + },
  1565 + _onRightKeyed: function onRightKeyed() {
  1566 + this.dir === "ltr" && this._autocomplete();
  1567 + },
  1568 + _onQueryChanged: function onQueryChanged(e, query) {
  1569 + this.input.clearHintIfInvalid();
  1570 + query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty();
  1571 + this.dropdown.open();
  1572 + this._setLanguageDirection();
  1573 + },
  1574 + _onWhitespaceChanged: function onWhitespaceChanged() {
  1575 + this._updateHint();
  1576 + this.dropdown.open();
  1577 + },
  1578 + _setLanguageDirection: function setLanguageDirection() {
  1579 + var dir;
  1580 + if (this.dir !== (dir = this.input.getLanguageDirection())) {
  1581 + this.dir = dir;
  1582 + this.$node.css("direction", dir);
  1583 + this.dropdown.setLanguageDirection(dir);
  1584 + }
  1585 + },
  1586 + _updateHint: function updateHint() {
  1587 + var datum, val, query, escapedQuery, frontMatchRegEx, match;
  1588 + datum = this.dropdown.getDatumForTopSuggestion();
  1589 + if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) {
  1590 + val = this.input.getInputValue();
  1591 + query = Input.normalizeQuery(val);
  1592 + escapedQuery = _.escapeRegExChars(query);
  1593 + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
  1594 + match = frontMatchRegEx.exec(datum.value);
  1595 + match ? this.input.setHint(val + match[1]) : this.input.clearHint();
  1596 + } else {
  1597 + this.input.clearHint();
  1598 + }
  1599 + },
  1600 + _autocomplete: function autocomplete(laxCursor) {
  1601 + var hint, query, isCursorAtEnd, datum;
  1602 + hint = this.input.getHint();
  1603 + query = this.input.getQuery();
  1604 + isCursorAtEnd = laxCursor || this.input.isCursorAtEnd();
  1605 + if (hint && query !== hint && isCursorAtEnd) {
  1606 + datum = this.dropdown.getDatumForTopSuggestion();
  1607 + datum && this.input.setInputValue(datum.value);
  1608 + this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName);
  1609 + }
  1610 + },
  1611 + _select: function select(datum) {
  1612 + this.input.setQuery(datum.value);
  1613 + this.input.setInputValue(datum.value, true);
  1614 + this._setLanguageDirection();
  1615 + this.eventBus.trigger("selected", datum.raw, datum.datasetName);
  1616 + this.dropdown.close();
  1617 + _.defer(_.bind(this.dropdown.empty, this.dropdown));
  1618 + },
  1619 + open: function open() {
  1620 + this.dropdown.open();
  1621 + },
  1622 + close: function close() {
  1623 + this.dropdown.close();
  1624 + },
  1625 + setVal: function setVal(val) {
  1626 + val = _.toStr(val);
  1627 + if (this.isActivated) {
  1628 + this.input.setInputValue(val);
  1629 + } else {
  1630 + this.input.setQuery(val);
  1631 + this.input.setInputValue(val, true);
  1632 + }
  1633 + this._setLanguageDirection();
  1634 + },
  1635 + getVal: function getVal() {
  1636 + return this.input.getQuery();
  1637 + },
  1638 + destroy: function destroy() {
  1639 + this.input.destroy();
  1640 + this.dropdown.destroy();
  1641 + destroyDomStructure(this.$node);
  1642 + this.$node = null;
  1643 + }
  1644 + });
  1645 + return Typeahead;
  1646 + function buildDom(input, withHint) {
  1647 + var $input, $wrapper, $dropdown, $hint;
  1648 + $input = $(input);
  1649 + $wrapper = $(html.wrapper).css(css.wrapper);
  1650 + $dropdown = $(html.dropdown).css(css.dropdown);
  1651 + $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input));
  1652 + $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder required").prop("readonly", true).attr({
  1653 + autocomplete: "off",
  1654 + spellcheck: "false",
  1655 + tabindex: -1
  1656 + });
  1657 + $input.data(attrsKey, {
  1658 + dir: $input.attr("dir"),
  1659 + autocomplete: $input.attr("autocomplete"),
  1660 + spellcheck: $input.attr("spellcheck"),
  1661 + style: $input.attr("style")
  1662 + });
  1663 + $input.addClass("tt-input").attr({
  1664 + autocomplete: "off",
  1665 + spellcheck: false
  1666 + }).css(withHint ? css.input : css.inputWithNoHint);
  1667 + try {
  1668 + !$input.attr("dir") && $input.attr("dir", "auto");
  1669 + } catch (e) {}
  1670 + return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown);
  1671 + }
  1672 + function getBackgroundStyles($el) {
  1673 + return {
  1674 + backgroundAttachment: $el.css("background-attachment"),
  1675 + backgroundClip: $el.css("background-clip"),
  1676 + backgroundColor: $el.css("background-color"),
  1677 + backgroundImage: $el.css("background-image"),
  1678 + backgroundOrigin: $el.css("background-origin"),
  1679 + backgroundPosition: $el.css("background-position"),
  1680 + backgroundRepeat: $el.css("background-repeat"),
  1681 + backgroundSize: $el.css("background-size")
  1682 + };
  1683 + }
  1684 + function destroyDomStructure($node) {
  1685 + var $input = $node.find(".tt-input");
  1686 + _.each($input.data(attrsKey), function(val, key) {
  1687 + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
  1688 + });
  1689 + $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node);
  1690 + $node.remove();
  1691 + }
  1692 + }();
  1693 + (function() {
  1694 + "use strict";
  1695 + var old, typeaheadKey, methods;
  1696 + old = $.fn.typeahead;
  1697 + typeaheadKey = "ttTypeahead";
  1698 + methods = {
  1699 + initialize: function initialize(o, datasets) {
  1700 + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
  1701 + o = o || {};
  1702 + return this.each(attach);
  1703 + function attach() {
  1704 + var $input = $(this), eventBus, typeahead;
  1705 + _.each(datasets, function(d) {
  1706 + d.highlight = !!o.highlight;
  1707 + });
  1708 + typeahead = new Typeahead({
  1709 + input: $input,
  1710 + eventBus: eventBus = new EventBus({
  1711 + el: $input
  1712 + }),
  1713 + withHint: _.isUndefined(o.hint) ? true : !!o.hint,
  1714 + minLength: o.minLength,
  1715 + autoselect: o.autoselect,
  1716 + datasets: datasets
  1717 + });
  1718 + $input.data(typeaheadKey, typeahead);
  1719 + }
  1720 + },
  1721 + open: function open() {
  1722 + return this.each(openTypeahead);
  1723 + function openTypeahead() {
  1724 + var $input = $(this), typeahead;
  1725 + if (typeahead = $input.data(typeaheadKey)) {
  1726 + typeahead.open();
  1727 + }
  1728 + }
  1729 + },
  1730 + close: function close() {
  1731 + return this.each(closeTypeahead);
  1732 + function closeTypeahead() {
  1733 + var $input = $(this), typeahead;
  1734 + if (typeahead = $input.data(typeaheadKey)) {
  1735 + typeahead.close();
  1736 + }
  1737 + }
  1738 + },
  1739 + val: function val(newVal) {
  1740 + return !arguments.length ? getVal(this.first()) : this.each(setVal);
  1741 + function setVal() {
  1742 + var $input = $(this), typeahead;
  1743 + if (typeahead = $input.data(typeaheadKey)) {
  1744 + typeahead.setVal(newVal);
  1745 + }
  1746 + }
  1747 + function getVal($input) {
  1748 + var typeahead, query;
  1749 + if (typeahead = $input.data(typeaheadKey)) {
  1750 + query = typeahead.getVal();
  1751 + }
  1752 + return query;
  1753 + }
  1754 + },
  1755 + destroy: function destroy() {
  1756 + return this.each(unattach);
  1757 + function unattach() {
  1758 + var $input = $(this), typeahead;
  1759 + if (typeahead = $input.data(typeaheadKey)) {
  1760 + typeahead.destroy();
  1761 + $input.removeData(typeaheadKey);
  1762 + }
  1763 + }
  1764 + }
  1765 + };
  1766 + $.fn.typeahead = function(method) {
  1767 + var tts;
  1768 + if (methods[method] && method !== "initialize") {
  1769 + tts = this.filter(function() {
  1770 + return !!$(this).data(typeaheadKey);
  1771 + });
  1772 + return methods[method].apply(tts, [].slice.call(arguments, 1));
  1773 + } else {
  1774 + return methods.initialize.apply(this, arguments);
  1775 + }
  1776 + };
  1777 + $.fn.typeahead.noConflict = function noConflict() {
  1778 + $.fn.typeahead = old;
  1779 + return this;
  1780 + };
  1781 + })();
  1782 +})(window.jQuery);
0 \ No newline at end of file 1783 \ No newline at end of file
public/stylesheets/typeahead.css 0 → 100644
@@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
  1 +.tt-hint {
  2 + display: block;
  3 + width: 100%;
  4 + height: 34px;
  5 + padding: 6px 12px;
  6 + font-size: 14px;
  7 + line-height: 1.428571429;
  8 + color: blue;
  9 +}
  10 +
  11 +.tt-dropdown-menu {
  12 + text-align: left;
  13 + width: 100%;
  14 + margin-top: 12px;
  15 + padding: 6px 0;
  16 + background-color: #fff;
  17 + border: 1px solid #ccc;
  18 + border: 1px solid rgba(0, 0, 0, 0.2);
  19 + -webkit-border-radius: 6px;
  20 + -moz-border-radius: 6px;
  21 + border-radius: 6px;
  22 + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
  23 + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
  24 + box-shadow: 0 5px 10px rgba(0,0,0,.2);
  25 +}
  26 +
  27 +.tt-suggestion {
  28 + padding: 6px 12px;
  29 + font-size: 14px;
  30 + line-height: 1.428571429;
  31 + cursor: pointer;
  32 +}
  33 +
  34 +.tt-suggestion.tt-cursor {
  35 + color: #fff;
  36 + background-color: #0097cf;
  37 +}
  38 +.tt-suggestion p {
  39 + margin: 0;
  40 +}