bootstrap-typeahead-rails 0.9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3bd02c5dc0c136d597f689c7bfa8063b8395e495
4
+ data.tar.gz: 5c51339e7d30ae0cb1941de1c56e66a0aa11017d
5
+ SHA512:
6
+ metadata.gz: 4c3fb96704d25ca51696474f4f8aeefb113b4807cf95a974b21b79b2ae2ce35a88fa245726e2f9edaa3ce12c5f2fe72656e0c8fb4e90128cfef848cf8280f9dc
7
+ data.tar.gz: 3a639a47cc5de6efcc5961917957010dd43b63c2b3d2445e92bb51b29176f54186bb06c588e6b458f87b9028c251c48dff43a531a333ab639560518308f438ea
@@ -0,0 +1,3 @@
1
+ bootstrap-typeahead-src
2
+ *.gem
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bootstrap-rails.gemspec
4
+ gemspec
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'json'
4
+ require File.expand_path('../lib/bootstrap-typeahead-rails/version', __FILE__)
5
+
6
+ desc "Update assets"
7
+ task :update do
8
+ if Dir.exist?('bootstrap-typeahead-src')
9
+ system("cd bootstrap-typeahead-src && git pull && cd ..")
10
+ else
11
+ system("git clone git@github.com:twitter/typeahead.js.git bootstrap-typeahead-src")
12
+ end
13
+ system("cp bootstrap-typeahead-src/dist/typeahead.js vendor/assets/javascripts/bootstrap-typeahead-rails/bootstrap-typeahead.js")
14
+
15
+ system("git status")
16
+
17
+ puts "\n"
18
+ puts "bootstrap-typeahead version: #{JSON.parse(File.read('./bootstrap-typeahead-src/component.json'))['version']}"
19
+ puts "bootstrap-typeahead-rails version: #{BootstrapTypeaheadRails::Rails::VERSION}"
20
+ end
21
+
22
+ desc "Build"
23
+ task "build" do
24
+ system("gem build bootstrap-typeahead-rails.gemspec")
25
+ end
26
+
27
+ desc "Build and publish the gem"
28
+ task :publish => :build do
29
+ tags = `git tag`.split
30
+ current_version = BootstrapTypeaheadRails::Rails::VERSION
31
+ system("git tag -a #{current_version} -m 'Release #{current_version}'") unless tags.include?(current_version)
32
+ system("gem push bootstrap-typeahead-rails-#{current_version}.gem")
33
+ system("git push --follow-tags")
34
+ end
35
+
36
+ task :release => :publish do
37
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/bootstrap-typeahead-rails/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Gonzalo Rodríguez-Baltanás Díaz"]
6
+ gem.email = ["siotopo@gmail.com"]
7
+ gem.description = %q{The official Typeahead plugin for Twitter Bootstrap}
8
+ gem.homepage = "https://github.com/Nerian/bootstrap-typeahead-rails"
9
+ gem.summary = gem.description
10
+ gem.license = 'MIT'
11
+
12
+ gem.name = "bootstrap-typeahead-rails"
13
+ gem.require_paths = ["lib"]
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.version = BootstrapTypeaheadRails::Rails::VERSION
16
+
17
+ gem.add_dependency "railties", ">= 3.0"
18
+ gem.add_development_dependency "bundler", ">= 1.0"
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "pry"
21
+ gem.add_development_dependency "json"
22
+ end
@@ -0,0 +1,12 @@
1
+ require "rails"
2
+ require "bootstrap-typeahead-rails/version"
3
+
4
+ module BootstrapTypeaheadRails
5
+ module Rails
6
+ if ::Rails.version.to_s < "3.1"
7
+ require "bootstrap-typeahead-rails/railtie"
8
+ else
9
+ require "bootstrap-typeahead-rails/engine"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ bomodule BootstrapTypeaheadRails
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module BootstrapTypeaheadRails
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module BootstrapTypeaheadRails
2
+ module Rails
3
+ VERSION = "0.9.3.0"
4
+ end
5
+ end
@@ -0,0 +1,1139 @@
1
+ /*!
2
+ * typeahead.js 0.9.3
3
+ * https://github.com/twitter/typeahead
4
+ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
5
+ */
6
+
7
+ (function($) {
8
+ var VERSION = "0.9.3";
9
+ var utils = {
10
+ isMsie: function() {
11
+ var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent);
12
+ return match ? parseInt(match[2], 10) : 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
+ bind: $.proxy,
33
+ bindAll: function(obj) {
34
+ var val;
35
+ for (var key in obj) {
36
+ $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj));
37
+ }
38
+ },
39
+ indexOf: function(haystack, needle) {
40
+ for (var i = 0; i < haystack.length; i++) {
41
+ if (haystack[i] === needle) {
42
+ return i;
43
+ }
44
+ }
45
+ return -1;
46
+ },
47
+ each: $.each,
48
+ map: $.map,
49
+ filter: $.grep,
50
+ every: function(obj, test) {
51
+ var result = true;
52
+ if (!obj) {
53
+ return result;
54
+ }
55
+ $.each(obj, function(key, val) {
56
+ if (!(result = test.call(null, val, key, obj))) {
57
+ return false;
58
+ }
59
+ });
60
+ return !!result;
61
+ },
62
+ some: function(obj, test) {
63
+ var result = false;
64
+ if (!obj) {
65
+ return result;
66
+ }
67
+ $.each(obj, function(key, val) {
68
+ if (result = test.call(null, val, key, obj)) {
69
+ return false;
70
+ }
71
+ });
72
+ return !!result;
73
+ },
74
+ mixin: $.extend,
75
+ getUniqueId: function() {
76
+ var counter = 0;
77
+ return function() {
78
+ return counter++;
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
+ tokenizeQuery: function(str) {
127
+ return $.trim(str).toLowerCase().split(/[\s]+/);
128
+ },
129
+ tokenizeText: function(str) {
130
+ return $.trim(str).toLowerCase().split(/[\s\-_]+/);
131
+ },
132
+ getProtocol: function() {
133
+ return location.protocol;
134
+ },
135
+ noop: function() {}
136
+ };
137
+ var EventTarget = function() {
138
+ var eventSplitter = /\s+/;
139
+ return {
140
+ on: function(events, callback) {
141
+ var event;
142
+ if (!callback) {
143
+ return this;
144
+ }
145
+ this._callbacks = this._callbacks || {};
146
+ events = events.split(eventSplitter);
147
+ while (event = events.shift()) {
148
+ this._callbacks[event] = this._callbacks[event] || [];
149
+ this._callbacks[event].push(callback);
150
+ }
151
+ return this;
152
+ },
153
+ trigger: function(events, data) {
154
+ var event, callbacks;
155
+ if (!this._callbacks) {
156
+ return this;
157
+ }
158
+ events = events.split(eventSplitter);
159
+ while (event = events.shift()) {
160
+ if (callbacks = this._callbacks[event]) {
161
+ for (var i = 0; i < callbacks.length; i += 1) {
162
+ callbacks[i].call(this, {
163
+ type: event,
164
+ data: data
165
+ });
166
+ }
167
+ }
168
+ }
169
+ return this;
170
+ }
171
+ };
172
+ }();
173
+ var EventBus = function() {
174
+ var namespace = "typeahead:";
175
+ function EventBus(o) {
176
+ if (!o || !o.el) {
177
+ $.error("EventBus initialized without el");
178
+ }
179
+ this.$el = $(o.el);
180
+ }
181
+ utils.mixin(EventBus.prototype, {
182
+ trigger: function(type) {
183
+ var args = [].slice.call(arguments, 1);
184
+ this.$el.trigger(namespace + type, args);
185
+ }
186
+ });
187
+ return EventBus;
188
+ }();
189
+ var PersistentStorage = function() {
190
+ var ls, methods;
191
+ try {
192
+ ls = window.localStorage;
193
+ ls.setItem("~~~", "!");
194
+ ls.removeItem("~~~");
195
+ } catch (err) {
196
+ ls = null;
197
+ }
198
+ function PersistentStorage(namespace) {
199
+ this.prefix = [ "__", namespace, "__" ].join("");
200
+ this.ttlKey = "__ttl__";
201
+ this.keyMatcher = new RegExp("^" + this.prefix);
202
+ }
203
+ if (ls && window.JSON) {
204
+ methods = {
205
+ _prefix: function(key) {
206
+ return this.prefix + key;
207
+ },
208
+ _ttlKey: function(key) {
209
+ return this._prefix(key) + this.ttlKey;
210
+ },
211
+ get: function(key) {
212
+ if (this.isExpired(key)) {
213
+ this.remove(key);
214
+ }
215
+ return decode(ls.getItem(this._prefix(key)));
216
+ },
217
+ set: function(key, val, ttl) {
218
+ if (utils.isNumber(ttl)) {
219
+ ls.setItem(this._ttlKey(key), encode(now() + ttl));
220
+ } else {
221
+ ls.removeItem(this._ttlKey(key));
222
+ }
223
+ return ls.setItem(this._prefix(key), encode(val));
224
+ },
225
+ remove: function(key) {
226
+ ls.removeItem(this._ttlKey(key));
227
+ ls.removeItem(this._prefix(key));
228
+ return this;
229
+ },
230
+ clear: function() {
231
+ var i, key, keys = [], len = ls.length;
232
+ for (i = 0; i < len; i++) {
233
+ if ((key = ls.key(i)).match(this.keyMatcher)) {
234
+ keys.push(key.replace(this.keyMatcher, ""));
235
+ }
236
+ }
237
+ for (i = keys.length; i--; ) {
238
+ this.remove(keys[i]);
239
+ }
240
+ return this;
241
+ },
242
+ isExpired: function(key) {
243
+ var ttl = decode(ls.getItem(this._ttlKey(key)));
244
+ return utils.isNumber(ttl) && now() > ttl ? true : false;
245
+ }
246
+ };
247
+ } else {
248
+ methods = {
249
+ get: utils.noop,
250
+ set: utils.noop,
251
+ remove: utils.noop,
252
+ clear: utils.noop,
253
+ isExpired: utils.noop
254
+ };
255
+ }
256
+ utils.mixin(PersistentStorage.prototype, methods);
257
+ return PersistentStorage;
258
+ function now() {
259
+ return new Date().getTime();
260
+ }
261
+ function encode(val) {
262
+ return JSON.stringify(utils.isUndefined(val) ? null : val);
263
+ }
264
+ function decode(val) {
265
+ return JSON.parse(val);
266
+ }
267
+ }();
268
+ var RequestCache = function() {
269
+ function RequestCache(o) {
270
+ utils.bindAll(this);
271
+ o = o || {};
272
+ this.sizeLimit = o.sizeLimit || 10;
273
+ this.cache = {};
274
+ this.cachedKeysByAge = [];
275
+ }
276
+ utils.mixin(RequestCache.prototype, {
277
+ get: function(url) {
278
+ return this.cache[url];
279
+ },
280
+ set: function(url, resp) {
281
+ var requestToEvict;
282
+ if (this.cachedKeysByAge.length === this.sizeLimit) {
283
+ requestToEvict = this.cachedKeysByAge.shift();
284
+ delete this.cache[requestToEvict];
285
+ }
286
+ this.cache[url] = resp;
287
+ this.cachedKeysByAge.push(url);
288
+ }
289
+ });
290
+ return RequestCache;
291
+ }();
292
+ var Transport = function() {
293
+ var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache;
294
+ function Transport(o) {
295
+ utils.bindAll(this);
296
+ o = utils.isString(o) ? {
297
+ url: o
298
+ } : o;
299
+ requestCache = requestCache || new RequestCache();
300
+ maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6;
301
+ this.url = o.url;
302
+ this.wildcard = o.wildcard || "%QUERY";
303
+ this.filter = o.filter;
304
+ this.replace = o.replace;
305
+ this.ajaxSettings = {
306
+ type: "get",
307
+ cache: o.cache,
308
+ timeout: o.timeout,
309
+ dataType: o.dataType || "json",
310
+ beforeSend: o.beforeSend
311
+ };
312
+ this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300);
313
+ }
314
+ utils.mixin(Transport.prototype, {
315
+ _get: function(url, cb) {
316
+ var that = this;
317
+ if (belowPendingRequestsThreshold()) {
318
+ this._sendRequest(url).done(done);
319
+ } else {
320
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
321
+ }
322
+ function done(resp) {
323
+ var data = that.filter ? that.filter(resp) : resp;
324
+ cb && cb(data);
325
+ requestCache.set(url, resp);
326
+ }
327
+ },
328
+ _sendRequest: function(url) {
329
+ var that = this, jqXhr = pendingRequests[url];
330
+ if (!jqXhr) {
331
+ incrementPendingRequests();
332
+ jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always);
333
+ }
334
+ return jqXhr;
335
+ function always() {
336
+ decrementPendingRequests();
337
+ pendingRequests[url] = null;
338
+ if (that.onDeckRequestArgs) {
339
+ that._get.apply(that, that.onDeckRequestArgs);
340
+ that.onDeckRequestArgs = null;
341
+ }
342
+ }
343
+ },
344
+ get: function(query, cb) {
345
+ var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp;
346
+ cb = cb || utils.noop;
347
+ url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery);
348
+ if (resp = requestCache.get(url)) {
349
+ utils.defer(function() {
350
+ cb(that.filter ? that.filter(resp) : resp);
351
+ });
352
+ } else {
353
+ this._get(url, cb);
354
+ }
355
+ return !!resp;
356
+ }
357
+ });
358
+ return Transport;
359
+ function incrementPendingRequests() {
360
+ pendingRequestsCount++;
361
+ }
362
+ function decrementPendingRequests() {
363
+ pendingRequestsCount--;
364
+ }
365
+ function belowPendingRequestsThreshold() {
366
+ return pendingRequestsCount < maxPendingRequests;
367
+ }
368
+ }();
369
+ var Dataset = function() {
370
+ var keys = {
371
+ thumbprint: "thumbprint",
372
+ protocol: "protocol",
373
+ itemHash: "itemHash",
374
+ adjacencyList: "adjacencyList"
375
+ };
376
+ function Dataset(o) {
377
+ utils.bindAll(this);
378
+ if (utils.isString(o.template) && !o.engine) {
379
+ $.error("no template engine specified");
380
+ }
381
+ if (!o.local && !o.prefetch && !o.remote) {
382
+ $.error("one of local, prefetch, or remote is required");
383
+ }
384
+ this.name = o.name || utils.getUniqueId();
385
+ this.limit = o.limit || 5;
386
+ this.minLength = o.minLength || 1;
387
+ this.header = o.header;
388
+ this.footer = o.footer;
389
+ this.valueKey = o.valueKey || "value";
390
+ this.template = compileTemplate(o.template, o.engine, this.valueKey);
391
+ this.local = o.local;
392
+ this.prefetch = o.prefetch;
393
+ this.remote = o.remote;
394
+ this.itemHash = {};
395
+ this.adjacencyList = {};
396
+ this.storage = o.name ? new PersistentStorage(o.name) : null;
397
+ }
398
+ utils.mixin(Dataset.prototype, {
399
+ _processLocalData: function(data) {
400
+ this._mergeProcessedData(this._processData(data));
401
+ },
402
+ _loadPrefetchData: function(o) {
403
+ var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred;
404
+ if (this.storage) {
405
+ storedThumbprint = this.storage.get(keys.thumbprint);
406
+ storedProtocol = this.storage.get(keys.protocol);
407
+ storedItemHash = this.storage.get(keys.itemHash);
408
+ storedAdjacencyList = this.storage.get(keys.adjacencyList);
409
+ }
410
+ isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol();
411
+ o = utils.isString(o) ? {
412
+ url: o
413
+ } : o;
414
+ o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3;
415
+ if (storedItemHash && storedAdjacencyList && !isExpired) {
416
+ this._mergeProcessedData({
417
+ itemHash: storedItemHash,
418
+ adjacencyList: storedAdjacencyList
419
+ });
420
+ deferred = $.Deferred().resolve();
421
+ } else {
422
+ deferred = $.getJSON(o.url).done(processPrefetchData);
423
+ }
424
+ return deferred;
425
+ function processPrefetchData(data) {
426
+ var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList;
427
+ if (that.storage) {
428
+ that.storage.set(keys.itemHash, itemHash, o.ttl);
429
+ that.storage.set(keys.adjacencyList, adjacencyList, o.ttl);
430
+ that.storage.set(keys.thumbprint, thumbprint, o.ttl);
431
+ that.storage.set(keys.protocol, utils.getProtocol(), o.ttl);
432
+ }
433
+ that._mergeProcessedData(processedData);
434
+ }
435
+ },
436
+ _transformDatum: function(datum) {
437
+ var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = {
438
+ value: value,
439
+ tokens: tokens
440
+ };
441
+ if (utils.isString(datum)) {
442
+ item.datum = {};
443
+ item.datum[this.valueKey] = datum;
444
+ } else {
445
+ item.datum = datum;
446
+ }
447
+ item.tokens = utils.filter(item.tokens, function(token) {
448
+ return !utils.isBlankString(token);
449
+ });
450
+ item.tokens = utils.map(item.tokens, function(token) {
451
+ return token.toLowerCase();
452
+ });
453
+ return item;
454
+ },
455
+ _processData: function(data) {
456
+ var that = this, itemHash = {}, adjacencyList = {};
457
+ utils.each(data, function(i, datum) {
458
+ var item = that._transformDatum(datum), id = utils.getUniqueId(item.value);
459
+ itemHash[id] = item;
460
+ utils.each(item.tokens, function(i, token) {
461
+ var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]);
462
+ !~utils.indexOf(adjacency, id) && adjacency.push(id);
463
+ });
464
+ });
465
+ return {
466
+ itemHash: itemHash,
467
+ adjacencyList: adjacencyList
468
+ };
469
+ },
470
+ _mergeProcessedData: function(processedData) {
471
+ var that = this;
472
+ utils.mixin(this.itemHash, processedData.itemHash);
473
+ utils.each(processedData.adjacencyList, function(character, adjacency) {
474
+ var masterAdjacency = that.adjacencyList[character];
475
+ that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency;
476
+ });
477
+ },
478
+ _getLocalSuggestions: function(terms) {
479
+ var that = this, firstChars = [], lists = [], shortestList, suggestions = [];
480
+ utils.each(terms, function(i, term) {
481
+ var firstChar = term.charAt(0);
482
+ !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar);
483
+ });
484
+ utils.each(firstChars, function(i, firstChar) {
485
+ var list = that.adjacencyList[firstChar];
486
+ if (!list) {
487
+ return false;
488
+ }
489
+ lists.push(list);
490
+ if (!shortestList || list.length < shortestList.length) {
491
+ shortestList = list;
492
+ }
493
+ });
494
+ if (lists.length < firstChars.length) {
495
+ return [];
496
+ }
497
+ utils.each(shortestList, function(i, id) {
498
+ var item = that.itemHash[id], isCandidate, isMatch;
499
+ isCandidate = utils.every(lists, function(list) {
500
+ return ~utils.indexOf(list, id);
501
+ });
502
+ isMatch = isCandidate && utils.every(terms, function(term) {
503
+ return utils.some(item.tokens, function(token) {
504
+ return token.indexOf(term) === 0;
505
+ });
506
+ });
507
+ isMatch && suggestions.push(item);
508
+ });
509
+ return suggestions;
510
+ },
511
+ initialize: function() {
512
+ var deferred;
513
+ this.local && this._processLocalData(this.local);
514
+ this.transport = this.remote ? new Transport(this.remote) : null;
515
+ deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve();
516
+ this.local = this.prefetch = this.remote = null;
517
+ this.initialize = function() {
518
+ return deferred;
519
+ };
520
+ return deferred;
521
+ },
522
+ getSuggestions: function(query, cb) {
523
+ var that = this, terms, suggestions, cacheHit = false;
524
+ if (query.length < this.minLength) {
525
+ return;
526
+ }
527
+ terms = utils.tokenizeQuery(query);
528
+ suggestions = this._getLocalSuggestions(terms).slice(0, this.limit);
529
+ if (suggestions.length < this.limit && this.transport) {
530
+ cacheHit = this.transport.get(query, processRemoteData);
531
+ }
532
+ !cacheHit && cb && cb(suggestions);
533
+ function processRemoteData(data) {
534
+ suggestions = suggestions.slice(0);
535
+ utils.each(data, function(i, datum) {
536
+ var item = that._transformDatum(datum), isDuplicate;
537
+ isDuplicate = utils.some(suggestions, function(suggestion) {
538
+ return item.value === suggestion.value;
539
+ });
540
+ !isDuplicate && suggestions.push(item);
541
+ return suggestions.length < that.limit;
542
+ });
543
+ cb && cb(suggestions);
544
+ }
545
+ }
546
+ });
547
+ return Dataset;
548
+ function compileTemplate(template, engine, valueKey) {
549
+ var renderFn, compiledTemplate;
550
+ if (utils.isFunction(template)) {
551
+ renderFn = template;
552
+ } else if (utils.isString(template)) {
553
+ compiledTemplate = engine.compile(template);
554
+ renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
555
+ } else {
556
+ renderFn = function(context) {
557
+ return "<p>" + context[valueKey] + "</p>";
558
+ };
559
+ }
560
+ return renderFn;
561
+ }
562
+ }();
563
+ var InputView = function() {
564
+ function InputView(o) {
565
+ var that = this;
566
+ utils.bindAll(this);
567
+ this.specialKeyCodeMap = {
568
+ 9: "tab",
569
+ 27: "esc",
570
+ 37: "left",
571
+ 39: "right",
572
+ 13: "enter",
573
+ 38: "up",
574
+ 40: "down"
575
+ };
576
+ this.$hint = $(o.hint);
577
+ this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent);
578
+ if (!utils.isMsie()) {
579
+ this.$input.on("input.tt", this._compareQueryToInputValue);
580
+ } else {
581
+ this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
582
+ if (that.specialKeyCodeMap[$e.which || $e.keyCode]) {
583
+ return;
584
+ }
585
+ utils.defer(that._compareQueryToInputValue);
586
+ });
587
+ }
588
+ this.query = this.$input.val();
589
+ this.$overflowHelper = buildOverflowHelper(this.$input);
590
+ }
591
+ utils.mixin(InputView.prototype, EventTarget, {
592
+ _handleFocus: function() {
593
+ this.trigger("focused");
594
+ },
595
+ _handleBlur: function() {
596
+ this.trigger("blured");
597
+ },
598
+ _handleSpecialKeyEvent: function($e) {
599
+ var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode];
600
+ keyName && this.trigger(keyName + "Keyed", $e);
601
+ },
602
+ _compareQueryToInputValue: function() {
603
+ var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false;
604
+ if (isSameQueryExceptWhitespace) {
605
+ this.trigger("whitespaceChanged", {
606
+ value: this.query
607
+ });
608
+ } else if (!isSameQuery) {
609
+ this.trigger("queryChanged", {
610
+ value: this.query = inputValue
611
+ });
612
+ }
613
+ },
614
+ destroy: function() {
615
+ this.$hint.off(".tt");
616
+ this.$input.off(".tt");
617
+ this.$hint = this.$input = this.$overflowHelper = null;
618
+ },
619
+ focus: function() {
620
+ this.$input.focus();
621
+ },
622
+ blur: function() {
623
+ this.$input.blur();
624
+ },
625
+ getQuery: function() {
626
+ return this.query;
627
+ },
628
+ setQuery: function(query) {
629
+ this.query = query;
630
+ },
631
+ getInputValue: function() {
632
+ return this.$input.val();
633
+ },
634
+ setInputValue: function(value, silent) {
635
+ this.$input.val(value);
636
+ !silent && this._compareQueryToInputValue();
637
+ },
638
+ getHintValue: function() {
639
+ return this.$hint.val();
640
+ },
641
+ setHintValue: function(value) {
642
+ this.$hint.val(value);
643
+ },
644
+ getLanguageDirection: function() {
645
+ return (this.$input.css("direction") || "ltr").toLowerCase();
646
+ },
647
+ isOverflow: function() {
648
+ this.$overflowHelper.text(this.getInputValue());
649
+ return this.$overflowHelper.width() > this.$input.width();
650
+ },
651
+ isCursorAtEnd: function() {
652
+ var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range;
653
+ if (utils.isNumber(selectionStart)) {
654
+ return selectionStart === valueLength;
655
+ } else if (document.selection) {
656
+ range = document.selection.createRange();
657
+ range.moveStart("character", -valueLength);
658
+ return valueLength === range.text.length;
659
+ }
660
+ return true;
661
+ }
662
+ });
663
+ return InputView;
664
+ function buildOverflowHelper($input) {
665
+ return $("<span></span>").css({
666
+ position: "absolute",
667
+ left: "-9999px",
668
+ visibility: "hidden",
669
+ whiteSpace: "nowrap",
670
+ fontFamily: $input.css("font-family"),
671
+ fontSize: $input.css("font-size"),
672
+ fontStyle: $input.css("font-style"),
673
+ fontVariant: $input.css("font-variant"),
674
+ fontWeight: $input.css("font-weight"),
675
+ wordSpacing: $input.css("word-spacing"),
676
+ letterSpacing: $input.css("letter-spacing"),
677
+ textIndent: $input.css("text-indent"),
678
+ textRendering: $input.css("text-rendering"),
679
+ textTransform: $input.css("text-transform")
680
+ }).insertAfter($input);
681
+ }
682
+ function compareQueries(a, b) {
683
+ a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
684
+ b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
685
+ return a === b;
686
+ }
687
+ }();
688
+ var DropdownView = function() {
689
+ var html = {
690
+ suggestionsList: '<span class="tt-suggestions"></span>'
691
+ }, css = {
692
+ suggestionsList: {
693
+ display: "block"
694
+ },
695
+ suggestion: {
696
+ whiteSpace: "nowrap",
697
+ cursor: "pointer"
698
+ },
699
+ suggestionChild: {
700
+ whiteSpace: "normal"
701
+ }
702
+ };
703
+ function DropdownView(o) {
704
+ utils.bindAll(this);
705
+ this.isOpen = false;
706
+ this.isEmpty = true;
707
+ this.isMouseOverDropdown = false;
708
+ this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover);
709
+ }
710
+ utils.mixin(DropdownView.prototype, EventTarget, {
711
+ _handleMouseenter: function() {
712
+ this.isMouseOverDropdown = true;
713
+ },
714
+ _handleMouseleave: function() {
715
+ this.isMouseOverDropdown = false;
716
+ },
717
+ _handleMouseover: function($e) {
718
+ var $suggestion = $($e.currentTarget);
719
+ this._getSuggestions().removeClass("tt-is-under-cursor");
720
+ $suggestion.addClass("tt-is-under-cursor");
721
+ },
722
+ _handleSelection: function($e) {
723
+ var $suggestion = $($e.currentTarget);
724
+ this.trigger("suggestionSelected", extractSuggestion($suggestion));
725
+ },
726
+ _show: function() {
727
+ this.$menu.css("display", "block");
728
+ },
729
+ _hide: function() {
730
+ this.$menu.hide();
731
+ },
732
+ _moveCursor: function(increment) {
733
+ var $suggestions, $cur, nextIndex, $underCursor;
734
+ if (!this.isVisible()) {
735
+ return;
736
+ }
737
+ $suggestions = this._getSuggestions();
738
+ $cur = $suggestions.filter(".tt-is-under-cursor");
739
+ $cur.removeClass("tt-is-under-cursor");
740
+ nextIndex = $suggestions.index($cur) + increment;
741
+ nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;
742
+ if (nextIndex === -1) {
743
+ this.trigger("cursorRemoved");
744
+ return;
745
+ } else if (nextIndex < -1) {
746
+ nextIndex = $suggestions.length - 1;
747
+ }
748
+ $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor");
749
+ this._ensureVisibility($underCursor);
750
+ this.trigger("cursorMoved", extractSuggestion($underCursor));
751
+ },
752
+ _getSuggestions: function() {
753
+ return this.$menu.find(".tt-suggestions > .tt-suggestion");
754
+ },
755
+ _ensureVisibility: function($el) {
756
+ var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true);
757
+ if (elTop < 0) {
758
+ this.$menu.scrollTop(menuScrollTop + elTop);
759
+ } else if (menuHeight < elBottom) {
760
+ this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
761
+ }
762
+ },
763
+ destroy: function() {
764
+ this.$menu.off(".tt");
765
+ this.$menu = null;
766
+ },
767
+ isVisible: function() {
768
+ return this.isOpen && !this.isEmpty;
769
+ },
770
+ closeUnlessMouseIsOverDropdown: function() {
771
+ if (!this.isMouseOverDropdown) {
772
+ this.close();
773
+ }
774
+ },
775
+ close: function() {
776
+ if (this.isOpen) {
777
+ this.isOpen = false;
778
+ this.isMouseOverDropdown = false;
779
+ this._hide();
780
+ this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor");
781
+ this.trigger("closed");
782
+ }
783
+ },
784
+ open: function() {
785
+ if (!this.isOpen) {
786
+ this.isOpen = true;
787
+ !this.isEmpty && this._show();
788
+ this.trigger("opened");
789
+ }
790
+ },
791
+ setLanguageDirection: function(dir) {
792
+ var ltrCss = {
793
+ left: "0",
794
+ right: "auto"
795
+ }, rtlCss = {
796
+ left: "auto",
797
+ right: " 0"
798
+ };
799
+ dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
800
+ },
801
+ moveCursorUp: function() {
802
+ this._moveCursor(-1);
803
+ },
804
+ moveCursorDown: function() {
805
+ this._moveCursor(+1);
806
+ },
807
+ getSuggestionUnderCursor: function() {
808
+ var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first();
809
+ return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
810
+ },
811
+ getFirstSuggestion: function() {
812
+ var $suggestion = this._getSuggestions().first();
813
+ return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
814
+ },
815
+ renderSuggestions: function(dataset, suggestions) {
816
+ var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '<div class="tt-suggestion">%body</div>', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el;
817
+ if ($dataset.length === 0) {
818
+ $suggestionsList = $(html.suggestionsList).css(css.suggestionsList);
819
+ $dataset = $("<div></div>").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu);
820
+ }
821
+ if (suggestions.length > 0) {
822
+ this.isEmpty = false;
823
+ this.isOpen && this._show();
824
+ elBuilder = document.createElement("div");
825
+ fragment = document.createDocumentFragment();
826
+ utils.each(suggestions, function(i, suggestion) {
827
+ suggestion.dataset = dataset.name;
828
+ compiledHtml = dataset.template(suggestion.datum);
829
+ elBuilder.innerHTML = wrapper.replace("%body", compiledHtml);
830
+ $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion);
831
+ $el.children().each(function() {
832
+ $(this).css(css.suggestionChild);
833
+ });
834
+ fragment.appendChild($el[0]);
835
+ });
836
+ $dataset.show().find(".tt-suggestions").html(fragment);
837
+ } else {
838
+ this.clearSuggestions(dataset.name);
839
+ }
840
+ this.trigger("suggestionsRendered");
841
+ },
842
+ clearSuggestions: function(datasetName) {
843
+ var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions");
844
+ $datasets.hide();
845
+ $suggestions.empty();
846
+ if (this._getSuggestions().length === 0) {
847
+ this.isEmpty = true;
848
+ this._hide();
849
+ }
850
+ }
851
+ });
852
+ return DropdownView;
853
+ function extractSuggestion($el) {
854
+ return $el.data("suggestion");
855
+ }
856
+ }();
857
+ var TypeaheadView = function() {
858
+ var html = {
859
+ wrapper: '<span class="twitter-typeahead"></span>',
860
+ hint: '<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>',
861
+ dropdown: '<span class="tt-dropdown-menu"></span>'
862
+ }, css = {
863
+ wrapper: {
864
+ position: "relative",
865
+ display: "inline-block"
866
+ },
867
+ hint: {
868
+ position: "absolute",
869
+ top: "0",
870
+ left: "0",
871
+ borderColor: "transparent",
872
+ boxShadow: "none"
873
+ },
874
+ query: {
875
+ position: "relative",
876
+ verticalAlign: "top",
877
+ backgroundColor: "transparent"
878
+ },
879
+ dropdown: {
880
+ position: "absolute",
881
+ top: "100%",
882
+ left: "0",
883
+ zIndex: "100",
884
+ display: "none"
885
+ }
886
+ };
887
+ if (utils.isMsie()) {
888
+ utils.mixin(css.query, {
889
+ backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
890
+ });
891
+ }
892
+ if (utils.isMsie() && utils.isMsie() <= 7) {
893
+ utils.mixin(css.wrapper, {
894
+ display: "inline",
895
+ zoom: "1"
896
+ });
897
+ utils.mixin(css.query, {
898
+ marginTop: "-1px"
899
+ });
900
+ }
901
+ function TypeaheadView(o) {
902
+ var $menu, $input, $hint;
903
+ utils.bindAll(this);
904
+ this.$node = buildDomStructure(o.input);
905
+ this.datasets = o.datasets;
906
+ this.dir = null;
907
+ this.eventBus = o.eventBus;
908
+ $menu = this.$node.find(".tt-dropdown-menu");
909
+ $input = this.$node.find(".tt-query");
910
+ $hint = this.$node.find(".tt-hint");
911
+ this.dropdownView = new DropdownView({
912
+ menu: $menu
913
+ }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent);
914
+ this.inputView = new InputView({
915
+ input: $input,
916
+ hint: $hint
917
+ }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete);
918
+ }
919
+ utils.mixin(TypeaheadView.prototype, EventTarget, {
920
+ _managePreventDefault: function(e) {
921
+ var $e = e.data, hint, inputValue, preventDefault = false;
922
+ switch (e.type) {
923
+ case "tabKeyed":
924
+ hint = this.inputView.getHintValue();
925
+ inputValue = this.inputView.getInputValue();
926
+ preventDefault = hint && hint !== inputValue;
927
+ break;
928
+
929
+ case "upKeyed":
930
+ case "downKeyed":
931
+ preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey;
932
+ break;
933
+ }
934
+ preventDefault && $e.preventDefault();
935
+ },
936
+ _setLanguageDirection: function() {
937
+ var dir = this.inputView.getLanguageDirection();
938
+ if (dir !== this.dir) {
939
+ this.dir = dir;
940
+ this.$node.css("direction", dir);
941
+ this.dropdownView.setLanguageDirection(dir);
942
+ }
943
+ },
944
+ _updateHint: function() {
945
+ var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match;
946
+ if (hint && dropdownIsVisible && !inputHasOverflow) {
947
+ inputValue = this.inputView.getInputValue();
948
+ query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, "");
949
+ escapedQuery = utils.escapeRegExChars(query);
950
+ beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i");
951
+ match = beginsWithQuery.exec(hint);
952
+ this.inputView.setHintValue(inputValue + (match ? match[1] : ""));
953
+ }
954
+ },
955
+ _clearHint: function() {
956
+ this.inputView.setHintValue("");
957
+ },
958
+ _clearSuggestions: function() {
959
+ this.dropdownView.clearSuggestions();
960
+ },
961
+ _setInputValueToQuery: function() {
962
+ this.inputView.setInputValue(this.inputView.getQuery());
963
+ },
964
+ _setInputValueToSuggestionUnderCursor: function(e) {
965
+ var suggestion = e.data;
966
+ this.inputView.setInputValue(suggestion.value, true);
967
+ },
968
+ _openDropdown: function() {
969
+ this.dropdownView.open();
970
+ },
971
+ _closeDropdown: function(e) {
972
+ this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"]();
973
+ },
974
+ _moveDropdownCursor: function(e) {
975
+ var $e = e.data;
976
+ if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) {
977
+ this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"]();
978
+ }
979
+ },
980
+ _handleSelection: function(e) {
981
+ var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor();
982
+ if (suggestion) {
983
+ this.inputView.setInputValue(suggestion.value);
984
+ byClick ? this.inputView.focus() : e.data.preventDefault();
985
+ byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close();
986
+ this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset);
987
+ }
988
+ },
989
+ _getSuggestions: function() {
990
+ var that = this, query = this.inputView.getQuery();
991
+ if (utils.isBlankString(query)) {
992
+ return;
993
+ }
994
+ utils.each(this.datasets, function(i, dataset) {
995
+ dataset.getSuggestions(query, function(suggestions) {
996
+ if (query === that.inputView.getQuery()) {
997
+ that.dropdownView.renderSuggestions(dataset, suggestions);
998
+ }
999
+ });
1000
+ });
1001
+ },
1002
+ _autocomplete: function(e) {
1003
+ var isCursorAtEnd, ignoreEvent, query, hint, suggestion;
1004
+ if (e.type === "rightKeyed" || e.type === "leftKeyed") {
1005
+ isCursorAtEnd = this.inputView.isCursorAtEnd();
1006
+ ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed";
1007
+ if (!isCursorAtEnd || ignoreEvent) {
1008
+ return;
1009
+ }
1010
+ }
1011
+ query = this.inputView.getQuery();
1012
+ hint = this.inputView.getHintValue();
1013
+ if (hint !== "" && query !== hint) {
1014
+ suggestion = this.dropdownView.getFirstSuggestion();
1015
+ this.inputView.setInputValue(suggestion.value);
1016
+ this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset);
1017
+ }
1018
+ },
1019
+ _propagateEvent: function(e) {
1020
+ this.eventBus.trigger(e.type);
1021
+ },
1022
+ destroy: function() {
1023
+ this.inputView.destroy();
1024
+ this.dropdownView.destroy();
1025
+ destroyDomStructure(this.$node);
1026
+ this.$node = null;
1027
+ },
1028
+ setQuery: function(query) {
1029
+ this.inputView.setQuery(query);
1030
+ this.inputView.setInputValue(query);
1031
+ this._clearHint();
1032
+ this._clearSuggestions();
1033
+ this._getSuggestions();
1034
+ }
1035
+ });
1036
+ return TypeaheadView;
1037
+ function buildDomStructure(input) {
1038
+ var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint);
1039
+ $wrapper = $wrapper.css(css.wrapper);
1040
+ $dropdown = $dropdown.css(css.dropdown);
1041
+ $hint.css(css.hint).css({
1042
+ backgroundAttachment: $input.css("background-attachment"),
1043
+ backgroundClip: $input.css("background-clip"),
1044
+ backgroundColor: $input.css("background-color"),
1045
+ backgroundImage: $input.css("background-image"),
1046
+ backgroundOrigin: $input.css("background-origin"),
1047
+ backgroundPosition: $input.css("background-position"),
1048
+ backgroundRepeat: $input.css("background-repeat"),
1049
+ backgroundSize: $input.css("background-size")
1050
+ });
1051
+ $input.data("ttAttrs", {
1052
+ dir: $input.attr("dir"),
1053
+ autocomplete: $input.attr("autocomplete"),
1054
+ spellcheck: $input.attr("spellcheck"),
1055
+ style: $input.attr("style")
1056
+ });
1057
+ $input.addClass("tt-query").attr({
1058
+ autocomplete: "off",
1059
+ spellcheck: false
1060
+ }).css(css.query);
1061
+ try {
1062
+ !$input.attr("dir") && $input.attr("dir", "auto");
1063
+ } catch (e) {}
1064
+ return $input.wrap($wrapper).parent().prepend($hint).append($dropdown);
1065
+ }
1066
+ function destroyDomStructure($node) {
1067
+ var $input = $node.find(".tt-query");
1068
+ utils.each($input.data("ttAttrs"), function(key, val) {
1069
+ utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
1070
+ });
1071
+ $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node);
1072
+ $node.remove();
1073
+ }
1074
+ }();
1075
+ (function() {
1076
+ var cache = {}, viewKey = "ttView", methods;
1077
+ methods = {
1078
+ initialize: function(datasetDefs) {
1079
+ var datasets;
1080
+ datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ];
1081
+ if (datasetDefs.length === 0) {
1082
+ $.error("no datasets provided");
1083
+ }
1084
+ datasets = utils.map(datasetDefs, function(o) {
1085
+ var dataset = cache[o.name] ? cache[o.name] : new Dataset(o);
1086
+ if (o.name) {
1087
+ cache[o.name] = dataset;
1088
+ }
1089
+ return dataset;
1090
+ });
1091
+ return this.each(initialize);
1092
+ function initialize() {
1093
+ var $input = $(this), deferreds, eventBus = new EventBus({
1094
+ el: $input
1095
+ });
1096
+ deferreds = utils.map(datasets, function(dataset) {
1097
+ return dataset.initialize();
1098
+ });
1099
+ $input.data(viewKey, new TypeaheadView({
1100
+ input: $input,
1101
+ eventBus: eventBus = new EventBus({
1102
+ el: $input
1103
+ }),
1104
+ datasets: datasets
1105
+ }));
1106
+ $.when.apply($, deferreds).always(function() {
1107
+ utils.defer(function() {
1108
+ eventBus.trigger("initialized");
1109
+ });
1110
+ });
1111
+ }
1112
+ },
1113
+ destroy: function() {
1114
+ return this.each(destroy);
1115
+ function destroy() {
1116
+ var $this = $(this), view = $this.data(viewKey);
1117
+ if (view) {
1118
+ view.destroy();
1119
+ $this.removeData(viewKey);
1120
+ }
1121
+ }
1122
+ },
1123
+ setQuery: function(query) {
1124
+ return this.each(setQuery);
1125
+ function setQuery() {
1126
+ var view = $(this).data(viewKey);
1127
+ view && view.setQuery(query);
1128
+ }
1129
+ }
1130
+ };
1131
+ jQuery.fn.typeahead = function(method) {
1132
+ if (methods[method]) {
1133
+ return methods[method].apply(this, [].slice.call(arguments, 1));
1134
+ } else {
1135
+ return methods.initialize.apply(this, arguments);
1136
+ }
1137
+ };
1138
+ })();
1139
+ })(window.jQuery);