twitter-typeahead-rails 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in twitter-typeahead-rails.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Yousef Ourabi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Twitter typeahead.js jquery plugin
2
+
3
+ This asset gem packages the [twitter typeahead.js](https://github.com/twitter/typeahead.js) jquery plugin for the Rails asset pipeline.
4
+
5
+ To learn more about typeahead.js read the post [Twitter's engineering blog](http://engineering.twitter.com/2013/02/twitter-typeaheadjs-you-autocomplete-me.html).
6
+
7
+ This gem includes the standard and minified versions of the assets.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'twitter-typeahead-rails'
14
+
15
+ or
16
+
17
+ gem 'twitter-typeahead-rails', :git => "git@github.com:yourabi/twitter-typeahead-rails.git"
18
+
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install twitter-typeahead-rails
27
+
28
+ ## Usage
29
+
30
+ To start using the twitter typeahead.js plugin in your rails app enable it via the asset pipeline (app/assets/javascripts/application.js).
31
+
32
+ Add one the folllwing to your application.js mainifest:
33
+
34
+ ```js
35
+
36
+ //= require twitter/typeahead
37
+ //= require twitter/typeahead.min
38
+
39
+ ```
40
+
41
+
42
+ ```js
43
+
44
+ // Twitter typeahead exmaple.
45
+ $(document).ready(function() {
46
+ $('.typeahead').typeahead( {name: 'planets', local: [ "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" ] });
47
+ });
48
+
49
+ ```
50
+
51
+ You'll also probably want to enable include the CSS in your application.css manifest. Include one of the following:
52
+
53
+ ```js
54
+
55
+ *= require twitter/typeahead
56
+
57
+ *= require twitter/typeahead.min
58
+
59
+ ```
60
+
61
+ Currently this version tracks version v0.8.0.
62
+
63
+ ## Contributing
64
+
65
+ 1. Fork it
66
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
67
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
68
+ 4. Push to the branch (`git push origin my-new-feature`)
69
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ require "twitter-typeahead-rails/version"
2
+
3
+ module Twitter
4
+ module Typeahead
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Twitter
2
+ module Typeahead
3
+ module Rails
4
+ VERSION = "0.8.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'twitter-typeahead-rails/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "twitter-typeahead-rails"
8
+ gem.version = Twitter::Typeahead::Rails::VERSION
9
+ gem.authors = ["Yousef Ourabi"]
10
+ gem.email = ["yourabi@gmail.com"]
11
+ gem.description = %q{twitter-typeahead-rails packages the typeahead.js jquery plugin for rails}
12
+ gem.summary = %q{twitter-typeahead-rails packages the typeahead.js jquery plugin for rails}
13
+ gem.homepage = "https://github.com/yourabi/twitter-typeahead-rails"
14
+
15
+ gem.add_dependency 'railties', '>= 3.1'
16
+ gem.add_dependency 'actionpack', '>= 3.1'
17
+ gem.add_dependency 'jquery-rails'
18
+
19
+ gem.add_development_dependency 'rails', '>= 3.1'
20
+
21
+ gem.files = `git ls-files`.split($/)
22
+ gem.require_paths = ["lib"]
23
+ end
@@ -0,0 +1,4 @@
1
+ //=require twitter/typeahead/typeahead.js
2
+
3
+
4
+
@@ -0,0 +1,4 @@
1
+ //=require twitter/typeahead/typeahead.min.js
2
+
3
+
4
+
@@ -0,0 +1,984 @@
1
+ /*!
2
+ * Twitter Typeahead 0.8.0
3
+ * https://github.com/twitter/typeahead
4
+ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
5
+ */
6
+
7
+ (function() {
8
+ var VERSION = "0.8.0";
9
+ var utils = {
10
+ isMsie: function() {
11
+ return /msie [\w.]+/i.test(navigator.userAgent);
12
+ },
13
+ isString: function(obj) {
14
+ return typeof obj === "string";
15
+ },
16
+ isNumber: function(obj) {
17
+ return typeof obj === "number";
18
+ },
19
+ isArray: $.isArray,
20
+ isFunction: $.isFunction,
21
+ isObject: function(obj) {
22
+ return obj !== Object(obj);
23
+ },
24
+ isUndefined: function(obj) {
25
+ return typeof obj === "undefined";
26
+ },
27
+ bind: $.proxy,
28
+ bindAll: function(obj) {
29
+ var val;
30
+ for (var key in obj) {
31
+ utils.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj));
32
+ }
33
+ },
34
+ indexOf: function(haystack, needle) {
35
+ for (var i = 0; i < haystack.length; i++) {
36
+ if (haystack[i] === needle) {
37
+ return i;
38
+ }
39
+ }
40
+ return -1;
41
+ },
42
+ each: $.each,
43
+ map: $.map,
44
+ filter: function(obj, test) {
45
+ var results = [];
46
+ $.each(obj, function(key, val) {
47
+ if (test(val, key, obj)) {
48
+ results.push(val);
49
+ }
50
+ });
51
+ return results;
52
+ },
53
+ every: function(obj, test) {
54
+ var result = true;
55
+ if (!obj) {
56
+ return result;
57
+ }
58
+ $.each(obj, function(key, val) {
59
+ if (!(result = test.call(null, val, key, obj))) {
60
+ return false;
61
+ }
62
+ });
63
+ return !!result;
64
+ },
65
+ keys: function(obj) {
66
+ if (!utils.isObject(obj)) {
67
+ throw new TypeError("invalid object");
68
+ }
69
+ return $.map(obj, function(val, key) {
70
+ return key;
71
+ });
72
+ },
73
+ mixin: $.extend,
74
+ getUniqueId: function() {
75
+ var counter = 0;
76
+ return function() {
77
+ return counter++;
78
+ };
79
+ }(),
80
+ debounce: function(func, wait, immediate) {
81
+ var timeout, result;
82
+ return function() {
83
+ var context = this, args = arguments, later, callNow;
84
+ later = function() {
85
+ timeout = null;
86
+ if (!immediate) {
87
+ result = func.apply(context, args);
88
+ }
89
+ };
90
+ callNow = immediate && !timeout;
91
+ clearTimeout(timeout);
92
+ timeout = setTimeout(later, wait);
93
+ if (callNow) {
94
+ result = func.apply(context, args);
95
+ }
96
+ return result;
97
+ };
98
+ },
99
+ throttle: function(func, wait) {
100
+ var context, args, timeout, result, previous, later;
101
+ previous = 0;
102
+ later = function() {
103
+ previous = new Date();
104
+ timeout = null;
105
+ result = func.apply(context, args);
106
+ };
107
+ return function() {
108
+ var now = new Date(), remaining = wait - (now - previous);
109
+ context = this;
110
+ args = arguments;
111
+ if (remaining <= 0) {
112
+ clearTimeout(timeout);
113
+ timeout = null;
114
+ previous = now;
115
+ result = func.apply(context, args);
116
+ } else if (!timeout) {
117
+ timeout = setTimeout(later, remaining);
118
+ }
119
+ return result;
120
+ };
121
+ },
122
+ uniqueArray: function(array) {
123
+ var u = {}, a = [];
124
+ for (var i = 0, l = array.length; i < l; ++i) {
125
+ if (u.hasOwnProperty(array[i])) {
126
+ continue;
127
+ }
128
+ a.push(array[i]);
129
+ u[array[i]] = 1;
130
+ }
131
+ return a;
132
+ },
133
+ tokenizeText: function(str) {
134
+ return $.trim(str).toLowerCase().split(/[\s\-_]+/);
135
+ },
136
+ getProtocol: function() {
137
+ return location.protocol;
138
+ },
139
+ noop: function() {}
140
+ };
141
+ var EventTarget = function() {
142
+ var eventSplitter = /\s+/;
143
+ return {
144
+ on: function(events, callback) {
145
+ var event;
146
+ if (!callback) {
147
+ return this;
148
+ }
149
+ this._callbacks = this._callbacks || {};
150
+ events = events.split(eventSplitter);
151
+ while (event = events.shift()) {
152
+ this._callbacks[event] = this._callbacks[event] || [];
153
+ this._callbacks[event].push(callback);
154
+ }
155
+ return this;
156
+ },
157
+ trigger: function(events, data) {
158
+ var event, callbacks;
159
+ if (!this._callbacks) {
160
+ return this;
161
+ }
162
+ events = events.split(eventSplitter);
163
+ while (event = events.shift()) {
164
+ if (callbacks = this._callbacks[event]) {
165
+ for (var i = 0; i < callbacks.length; i += 1) {
166
+ callbacks[i].call(this, {
167
+ type: event,
168
+ data: data
169
+ });
170
+ }
171
+ }
172
+ }
173
+ return this;
174
+ }
175
+ };
176
+ }();
177
+ var PersistentStorage = function() {
178
+ var ls = window.localStorage, methods;
179
+ function PersistentStorage(namespace) {
180
+ this.prefix = [ "__", namespace, "__" ].join("");
181
+ this.ttlKey = "__ttl__";
182
+ this.keyMatcher = new RegExp("^" + this.prefix);
183
+ }
184
+ if (window.localStorage && window.JSON) {
185
+ methods = {
186
+ get: function(key) {
187
+ var ttl = decode(ls.getItem(this.prefix + key));
188
+ if (utils.isNumber(ttl) && now() > ttl) {
189
+ ls.removeItem(this.prefix + key + this.ttlKey);
190
+ }
191
+ return decode(ls.getItem(this.prefix + key));
192
+ },
193
+ set: function(key, val, ttl) {
194
+ if (utils.isNumber(ttl)) {
195
+ ls.setItem(this.prefix + key + this.ttlKey, encode(now() + ttl));
196
+ } else {
197
+ ls.removeItem(this.prefix + key + this.ttlKey);
198
+ }
199
+ return ls.setItem(this.prefix + key, encode(val));
200
+ },
201
+ remove: function(key) {
202
+ ls.removeItem(this.prefix + key + this.ttlKey);
203
+ ls.removeItem(this.prefix + key);
204
+ return this;
205
+ },
206
+ clear: function() {
207
+ var i, key, len = ls.length;
208
+ for (i = 0; i < len; i += 1) {
209
+ key = ls.key(i);
210
+ if (key.match(this.keyMatcher)) {
211
+ i -= 1;
212
+ len -= 1;
213
+ this.remove(key.replace(this.keyMatcher, ""));
214
+ }
215
+ }
216
+ return this;
217
+ },
218
+ isExpired: function(key) {
219
+ var ttl = decode(ls.getItem(this.prefix + key + this.ttlKey));
220
+ return utils.isNumber(ttl) && now() > ttl ? true : false;
221
+ }
222
+ };
223
+ } else {
224
+ methods = {
225
+ get: utils.noop,
226
+ set: utils.noop,
227
+ remove: utils.noop,
228
+ clear: utils.noop,
229
+ isExpired: utils.noop
230
+ };
231
+ }
232
+ utils.mixin(PersistentStorage.prototype, methods);
233
+ return PersistentStorage;
234
+ function now() {
235
+ return new Date().getTime();
236
+ }
237
+ function encode(val) {
238
+ return JSON.stringify(val);
239
+ }
240
+ function decode(val) {
241
+ return utils.isUndefined(val) ? undefined : JSON.parse(val);
242
+ }
243
+ }();
244
+ var RequestCache = function() {
245
+ function RequestCache(o) {
246
+ utils.bindAll(this);
247
+ o = o || {};
248
+ this.sizeLimit = o.sizeLimit || 10;
249
+ this.cache = {};
250
+ this.cachedKeysByAge = [];
251
+ }
252
+ utils.mixin(RequestCache.prototype, {
253
+ get: function(url) {
254
+ return this.cache[url];
255
+ },
256
+ set: function(url, resp) {
257
+ var requestToEvict;
258
+ if (this.cachedKeysByAge.length === this.sizeLimit) {
259
+ requestToEvict = this.cachedKeysByAge.shift();
260
+ delete this.cache[requestToEvict];
261
+ }
262
+ this.cache[url] = resp;
263
+ this.cachedKeysByAge.push(url);
264
+ }
265
+ });
266
+ return RequestCache;
267
+ }();
268
+ var Transport = function() {
269
+ function Transport(o) {
270
+ var rateLimitFn;
271
+ utils.bindAll(this);
272
+ o = o || {};
273
+ rateLimitFn = /^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce;
274
+ this.wait = o.wait || 300;
275
+ this.wildcard = o.wildcard || "%QUERY";
276
+ this.maxConcurrentRequests = o.maxConcurrentRequests || 6;
277
+ this.concurrentRequests = 0;
278
+ this.onDeckRequestArgs = null;
279
+ this.cache = new RequestCache();
280
+ this.get = rateLimitFn(this.get, this.wait);
281
+ }
282
+ utils.mixin(Transport.prototype, {
283
+ _incrementConcurrentRequests: function() {
284
+ this.concurrentRequests++;
285
+ },
286
+ _decrementConcurrentRequests: function() {
287
+ this.concurrentRequests--;
288
+ },
289
+ _belowConcurrentRequestsThreshold: function() {
290
+ return this.concurrentRequests < this.maxConcurrentRequests;
291
+ },
292
+ get: function(url, query, cb) {
293
+ var that = this, resp;
294
+ url = url.replace(this.wildcard, encodeURIComponent(query || ""));
295
+ if (resp = this.cache.get(url)) {
296
+ cb && cb(resp);
297
+ } else if (this._belowConcurrentRequestsThreshold()) {
298
+ $.ajax({
299
+ url: url,
300
+ type: "GET",
301
+ dataType: "json",
302
+ beforeSend: function() {
303
+ that._incrementConcurrentRequests();
304
+ },
305
+ success: function(resp) {
306
+ cb && cb(resp);
307
+ that.cache.set(url, resp);
308
+ },
309
+ complete: function() {
310
+ that._decrementConcurrentRequests();
311
+ if (that.onDeckRequestArgs) {
312
+ that.get.apply(that, that.onDeckRequestArgs);
313
+ that.onDeckRequestArgs = null;
314
+ }
315
+ }
316
+ });
317
+ } else {
318
+ this.onDeckRequestArgs = [].slice.call(arguments, 0);
319
+ }
320
+ }
321
+ });
322
+ return Transport;
323
+ }();
324
+ var Dataset = function() {
325
+ function Dataset(o) {
326
+ utils.bindAll(this);
327
+ this.storage = new PersistentStorage(o.name);
328
+ this.adjacencyList = {};
329
+ this.itemHash = {};
330
+ this.name = o.name;
331
+ this.resetDataOnProtocolSwitch = o.resetDataOnProtocolSwitch || false;
332
+ this.prefetchUrl = o.prefetch;
333
+ this.queryUrl = o.remote;
334
+ this.rawData = o.local;
335
+ this.transport = o.transport;
336
+ this.limit = o.limit || 10;
337
+ this._customMatcher = o.matcher || null;
338
+ this._customRanker = o.ranker || null;
339
+ this._ttl_ms = o.ttl_ms || 3 * 24 * 60 * 60 * 1e3;
340
+ this.storageAdjacencyList = "adjacencyList";
341
+ this.storageHash = "itemHash";
342
+ this.storageProtocol = "protocol";
343
+ this.storageVersion = "version";
344
+ this._loadData();
345
+ }
346
+ utils.mixin(Dataset.prototype, {
347
+ _isMetadataExpired: function() {
348
+ var isExpired = this.storage.isExpired(this.storageProtocol);
349
+ var isCacheStale = this.storage.isExpired(this.storageAdjacencyList) || this.storage.isExpired(this.storageHash);
350
+ var resetForProtocolSwitch = this.resetDataOnProtocolSwitch && this.storage.get(this.storageProtocol) != utils.getProtocol();
351
+ if (VERSION == this.storage.get(this.storageVersion) && !resetForProtocolSwitch && !isExpired && !isCacheStale) {
352
+ return false;
353
+ }
354
+ return true;
355
+ },
356
+ _loadData: function() {
357
+ this.rawData && this._processRawData(this.rawData);
358
+ this._getDataFromLocalStorage();
359
+ if (this._isMetadataExpired() || this.itemHash === {}) {
360
+ this.prefetchUrl && this._prefetch(this.prefetchUrl);
361
+ }
362
+ },
363
+ _getDataFromLocalStorage: function() {
364
+ this.itemHash = this.storage.get(this.storageHash) || this.itemHash;
365
+ this.adjacencyList = this.storage.get(this.storageAdjacencyList) || this.adjacencyList;
366
+ },
367
+ _getPotentiallyMatchingIds: function(terms) {
368
+ var potentiallyMatchingIds = [];
369
+ var lists = [];
370
+ utils.map(terms, utils.bind(function(term) {
371
+ var list = this.adjacencyList[term.charAt(0)];
372
+ if (!list) {
373
+ return;
374
+ }
375
+ lists.push(list);
376
+ }, this));
377
+ if (lists.length === 1) {
378
+ return lists[0];
379
+ }
380
+ var listLengths = [];
381
+ $.each(lists, function(i, list) {
382
+ listLengths.push(list.length);
383
+ });
384
+ var shortestListIndex = utils.indexOf(listLengths, Math.min.apply(null, listLengths)) || 0;
385
+ var shortestList = lists[shortestListIndex] || [];
386
+ potentiallyMatchingIds = utils.map(shortestList, function(item) {
387
+ var idInEveryList = utils.every(lists, function(list) {
388
+ return utils.indexOf(list, item) > -1;
389
+ });
390
+ if (idInEveryList) {
391
+ return item;
392
+ }
393
+ });
394
+ return potentiallyMatchingIds;
395
+ },
396
+ _getItemsFromIds: function(ids) {
397
+ var items = [];
398
+ utils.map(ids, utils.bind(function(id) {
399
+ var item = this.itemHash[id];
400
+ if (item) {
401
+ items.push(item);
402
+ }
403
+ }, this));
404
+ return items;
405
+ },
406
+ _matcher: function(terms) {
407
+ if (this._customMatcher) {
408
+ var customMatcher = this._customMatcher;
409
+ return function(item) {
410
+ return customMatcher(item);
411
+ };
412
+ } else {
413
+ return function(item) {
414
+ var tokens = item.tokens;
415
+ var allTermsMatched = utils.every(terms, function(term) {
416
+ var tokensMatched = utils.filter(tokens, function(token) {
417
+ return token.indexOf(term) === 0;
418
+ });
419
+ return tokensMatched.length;
420
+ });
421
+ if (allTermsMatched) {
422
+ return item;
423
+ }
424
+ };
425
+ }
426
+ },
427
+ _compareItems: function(a, b, areLocalItems) {
428
+ var aScoreBoost = !a.score_boost ? 0 : a.score_boost, bScoreBoost = !b.score_boost ? 0 : b.score_boost, aScore = !a.score ? 0 : a.score, bScore = !b.score ? 0 : b.score;
429
+ if (areLocalItems) {
430
+ return b.weight + bScoreBoost - (a.weight + aScoreBoost);
431
+ } else {
432
+ return bScore + bScoreBoost - (aScore + aScoreBoost);
433
+ }
434
+ },
435
+ _ranker: function(a, b) {
436
+ if (this._customRanker) {
437
+ return this._customRanker(a, b);
438
+ } else {
439
+ var aIsLocal = a.weight && a.weight !== 0;
440
+ var bIsLocal = b.weight && b.weight !== 0;
441
+ if (aIsLocal && !bIsLocal) {
442
+ return -1;
443
+ } else if (bIsLocal && !aIsLocal) {
444
+ return 1;
445
+ } else {
446
+ return aIsLocal && bIsLocal ? this._compareItems(a, b, true) : this._compareItems(a, b, false);
447
+ }
448
+ }
449
+ },
450
+ _processRawData: function(data) {
451
+ this.itemHash = {};
452
+ this.adjacencyList = {};
453
+ utils.map(data, utils.bind(function(item) {
454
+ var tokens;
455
+ if (item.tokens) {
456
+ tokens = item.tokens;
457
+ } else {
458
+ item = {
459
+ tokens: utils.tokenizeText(item),
460
+ value: item
461
+ };
462
+ tokens = item.tokens;
463
+ }
464
+ item.id = utils.getUniqueId(item.value);
465
+ utils.map(tokens, utils.bind(function(token) {
466
+ var firstChar = token.charAt(0);
467
+ if (!this.adjacencyList[firstChar]) {
468
+ this.adjacencyList[firstChar] = [ item.id ];
469
+ } else {
470
+ if (utils.indexOf(this.adjacencyList[firstChar], item.id) === -1) {
471
+ this.adjacencyList[firstChar].push(item.id);
472
+ }
473
+ }
474
+ }, this));
475
+ this.itemHash[item.id] = item;
476
+ }, this));
477
+ this.storage.set(this.storageHash, this.itemHash, this._ttl_ms);
478
+ this.storage.set(this.storageAdjacencyList, this.adjacencyList, this._ttl_ms);
479
+ this.storage.set(this.storageVersion, VERSION, this._ttl_ms);
480
+ this.storage.set(this.storageProtocol, utils.getProtocol(), this._ttl_ms);
481
+ },
482
+ _prefetch: function(url) {
483
+ var processPrefetchSuccess = function(data) {
484
+ if (!data) {
485
+ return;
486
+ }
487
+ utils.map(data, function(item) {
488
+ if (utils.isString(item)) {
489
+ return {
490
+ value: item,
491
+ tokens: utils.tokenizeText(item.toLowerCase())
492
+ };
493
+ } else {
494
+ utils.map(item.tokens, function(token, i) {
495
+ item.tokens[i] = token.toLowerCase();
496
+ });
497
+ return item;
498
+ }
499
+ });
500
+ this._processRawData(data);
501
+ };
502
+ var processPrefetchError = function() {
503
+ this._getDataFromLocalStorage();
504
+ };
505
+ $.ajax({
506
+ url: url,
507
+ success: utils.bind(processPrefetchSuccess, this),
508
+ error: utils.bind(processPrefetchError, this)
509
+ });
510
+ },
511
+ _processRemoteSuggestions: function(callback, matchedItems) {
512
+ return function(data) {
513
+ var remoteAndLocalSuggestions = {}, dedupedSuggestions = [];
514
+ utils.each(data, function(index, item) {
515
+ if (utils.isString(item)) {
516
+ remoteAndLocalSuggestions[item] = {
517
+ value: item
518
+ };
519
+ } else {
520
+ remoteAndLocalSuggestions[item.value] = item;
521
+ }
522
+ });
523
+ utils.each(matchedItems, function(index, item) {
524
+ if (remoteAndLocalSuggestions[item.value]) {
525
+ return true;
526
+ }
527
+ if (utils.isString(item)) {
528
+ remoteAndLocalSuggestions[item] = {
529
+ value: item
530
+ };
531
+ } else {
532
+ remoteAndLocalSuggestions[item.value] = item;
533
+ }
534
+ });
535
+ utils.each(remoteAndLocalSuggestions, function(index, item) {
536
+ dedupedSuggestions.push(item);
537
+ });
538
+ callback && callback(dedupedSuggestions);
539
+ };
540
+ },
541
+ getSuggestions: function(query, callback) {
542
+ var terms = utils.tokenizeText(query);
543
+ var potentiallyMatchingIds = this._getPotentiallyMatchingIds(terms);
544
+ var potentiallyMatchingItems = this._getItemsFromIds(potentiallyMatchingIds);
545
+ var matchedItems = utils.filter(potentiallyMatchingItems, this._matcher(terms));
546
+ matchedItems.sort(this._ranker);
547
+ callback && callback(matchedItems);
548
+ if (matchedItems.length < this.limit && this.queryUrl) {
549
+ this.transport.get(this.queryUrl, query, this._processRemoteSuggestions(callback, matchedItems));
550
+ }
551
+ }
552
+ });
553
+ return Dataset;
554
+ }();
555
+ var InputView = function() {
556
+ function InputView(o) {
557
+ var that = this;
558
+ utils.bindAll(this);
559
+ this.specialKeyCodeMap = {
560
+ 9: {
561
+ event: "tab"
562
+ },
563
+ 27: {
564
+ event: "esc"
565
+ },
566
+ 37: {
567
+ event: "left"
568
+ },
569
+ 39: {
570
+ event: "right"
571
+ },
572
+ 13: {
573
+ event: "enter"
574
+ },
575
+ 38: {
576
+ event: "up",
577
+ preventDefault: true
578
+ },
579
+ 40: {
580
+ event: "down",
581
+ preventDefault: true
582
+ }
583
+ };
584
+ this.query = "";
585
+ this.$hint = $(o.hint);
586
+ this.$input = $(o.input).on("blur", this._handleBlur).on("focus", this._handleFocus).on("keydown", this._handleSpecialKeyEvent);
587
+ if (!utils.isMsie()) {
588
+ this.$input.on("input", this._compareQueryToInputValue);
589
+ } else {
590
+ this.$input.on("keydown keypress cut paste", function(e) {
591
+ if (that.specialKeyCodeMap[e.which || e.keyCode]) {
592
+ return;
593
+ }
594
+ setTimeout(that._compareQueryToInputValue, 0);
595
+ });
596
+ }
597
+ }
598
+ utils.mixin(InputView.prototype, EventTarget, {
599
+ _handleFocus: function() {
600
+ this.trigger("focus");
601
+ },
602
+ _handleBlur: function() {
603
+ this.trigger("blur");
604
+ },
605
+ _handleSpecialKeyEvent: function(e) {
606
+ var keyCode = this.specialKeyCodeMap[e.which || e.keyCode];
607
+ if (keyCode) {
608
+ this.trigger(keyCode.event, e);
609
+ keyCode.preventDefault && e.preventDefault();
610
+ }
611
+ },
612
+ _compareQueryToInputValue: function() {
613
+ var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false;
614
+ if (isSameQueryExceptWhitespace) {
615
+ this.trigger("whitespaceChange", {
616
+ value: this.query
617
+ });
618
+ } else if (!isSameQuery) {
619
+ this.trigger("queryChange", {
620
+ value: this.query = inputValue
621
+ });
622
+ }
623
+ },
624
+ focus: function() {
625
+ this.$input.focus();
626
+ },
627
+ blur: function() {
628
+ this.$input.blur();
629
+ },
630
+ setPreventDefaultValueForKey: function(key, value) {
631
+ this.specialKeyCodeMap[key].preventDefault = !!value;
632
+ },
633
+ getQuery: function() {
634
+ return this.query;
635
+ },
636
+ getInputValue: function() {
637
+ return this.$input.val();
638
+ },
639
+ setInputValue: function(value, silent) {
640
+ this.$input.val(value);
641
+ if (silent !== true) {
642
+ this._compareQueryToInputValue();
643
+ }
644
+ },
645
+ getHintValue: function() {
646
+ return this.$hint.val();
647
+ },
648
+ setHintValue: function(value) {
649
+ this.$hint.val(value);
650
+ },
651
+ getLanguageDirection: function() {
652
+ return (this.$input.css("direction") || "ltr").toLowerCase();
653
+ },
654
+ isCursorAtEnd: function() {
655
+ var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range;
656
+ if (selectionStart) {
657
+ return selectionStart === valueLength;
658
+ } else if (document.selection) {
659
+ range = document.selection.createRange();
660
+ range.moveStart("character", -valueLength);
661
+ return valueLength === range.text.length;
662
+ }
663
+ return true;
664
+ }
665
+ });
666
+ return InputView;
667
+ function compareQueries(a, b) {
668
+ a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ").toLowerCase();
669
+ b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ").toLowerCase();
670
+ return a === b;
671
+ }
672
+ }();
673
+ var DropdownView = function() {
674
+ function DropdownView(o) {
675
+ utils.bindAll(this);
676
+ this.isMouseOverDropdown;
677
+ this.$menu = $(o.menu).on("mouseenter", this._handleMouseenter).on("mouseleave", this._handleMouseleave).on("mouseover", ".tt-suggestions > .tt-suggestion", this._handleMouseover).on("click", ".tt-suggestions > .tt-suggestion", this._handleSelection);
678
+ }
679
+ utils.mixin(DropdownView.prototype, EventTarget, {
680
+ _handleMouseenter: function() {
681
+ this.isMouseOverDropdown = true;
682
+ },
683
+ _handleMouseleave: function() {
684
+ this.isMouseOverDropdown = false;
685
+ },
686
+ _handleMouseover: function(e) {
687
+ this._getSuggestions().removeClass("tt-is-under-cursor");
688
+ $(e.currentTarget).addClass("tt-is-under-cursor");
689
+ },
690
+ _handleSelection: function(e) {
691
+ this.trigger("select", formatDataForSuggestion($(e.currentTarget)));
692
+ },
693
+ _moveCursor: function(increment) {
694
+ var $suggestions, $cur, nextIndex, $underCursor;
695
+ if (!this.$menu.hasClass("tt-is-open")) {
696
+ return;
697
+ }
698
+ $suggestions = this._getSuggestions();
699
+ $cur = $suggestions.filter(".tt-is-under-cursor");
700
+ $cur.removeClass("tt-is-under-cursor");
701
+ nextIndex = $suggestions.index($cur) + increment;
702
+ nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;
703
+ if (nextIndex === -1) {
704
+ this.trigger("cursorOff");
705
+ return;
706
+ } else if (nextIndex < -1) {
707
+ nextIndex = $suggestions.length - 1;
708
+ }
709
+ $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor");
710
+ this.trigger("cursorOn", {
711
+ value: $underCursor.data("value")
712
+ });
713
+ },
714
+ _getSuggestions: function() {
715
+ return this.$menu.find(".tt-suggestions > .tt-suggestion");
716
+ },
717
+ hideUnlessMouseIsOverDropdown: function() {
718
+ if (!this.isMouseOverDropdown) {
719
+ this.hide();
720
+ }
721
+ },
722
+ hide: function() {
723
+ if (this.$menu.hasClass("tt-is-open")) {
724
+ this.$menu.removeClass("tt-is-open").find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor");
725
+ this.trigger("hide");
726
+ }
727
+ },
728
+ show: function() {
729
+ if (!this.$menu.hasClass("tt-is-open")) {
730
+ this.$menu.addClass("tt-is-open");
731
+ this.trigger("show");
732
+ }
733
+ },
734
+ isOpen: function() {
735
+ return this.$menu.hasClass("tt-is-open");
736
+ },
737
+ moveCursorUp: function() {
738
+ this._moveCursor(-1);
739
+ },
740
+ moveCursorDown: function() {
741
+ this._moveCursor(+1);
742
+ },
743
+ getSuggestionUnderCursor: function() {
744
+ var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first();
745
+ return $suggestion.length > 0 ? formatDataForSuggestion($suggestion) : null;
746
+ },
747
+ getFirstSuggestion: function() {
748
+ var $suggestion = this._getSuggestions().first();
749
+ return $suggestion.length > 0 ? formatDataForSuggestion($suggestion) : null;
750
+ },
751
+ renderSuggestions: function(query, dataset, suggestions) {
752
+ var datasetClassName = "tt-dataset-" + dataset.name, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, el;
753
+ if ($dataset.length === 0) {
754
+ $dataset = $('<li><ol class="tt-suggestions"></ol></li>').addClass(datasetClassName).appendTo(this.$menu);
755
+ }
756
+ elBuilder = document.createElement("div");
757
+ fragment = document.createDocumentFragment();
758
+ this.clearSuggestions(dataset.name);
759
+ if (suggestions.length > 0) {
760
+ this.$menu.removeClass("tt-is-empty");
761
+ utils.each(suggestions, function(i, suggestion) {
762
+ elBuilder.innerHTML = dataset.template.render(suggestion);
763
+ el = elBuilder.firstChild;
764
+ el.setAttribute("data-value", suggestion.value);
765
+ fragment.appendChild(el);
766
+ });
767
+ }
768
+ $dataset.find("> .tt-suggestions").data({
769
+ query: query,
770
+ dataset: dataset.name
771
+ }).append(fragment);
772
+ this.trigger("suggestionsRender");
773
+ },
774
+ clearSuggestions: function(datasetName) {
775
+ var $suggestions = datasetName ? this.$menu.find(".tt-dataset-" + datasetName + " .tt-suggestions") : this.$menu.find(".tt-suggestions");
776
+ $suggestions.empty();
777
+ this._getSuggestions().length === 0 && this.$menu.addClass("tt-is-empty");
778
+ }
779
+ });
780
+ return DropdownView;
781
+ function formatDataForSuggestion($suggestion) {
782
+ var $suggestions = $suggestion.parents(".tt-suggestions").first();
783
+ return {
784
+ value: $suggestion.data("value"),
785
+ query: $suggestions.data("query"),
786
+ dataset: $suggestions.data("dataset")
787
+ };
788
+ }
789
+ }();
790
+ var TypeaheadView = function() {
791
+ var html = {
792
+ wrapper: '<span class="twitter-typeahead"></span>',
793
+ hint: '<input class="tt-hint" type="text" autocomplete="false" spellcheck="false" disabled>',
794
+ dropdown: '<ol class="tt-dropdown-menu tt-is-empty"></ol>'
795
+ };
796
+ function TypeaheadView(o) {
797
+ utils.bindAll(this);
798
+ this.$node = wrapInput(o.input);
799
+ this.datasets = o.datasets;
800
+ utils.each(this.datasets, function(key, dataset) {
801
+ var parentTemplate = '<li class="tt-suggestion">%body</li>';
802
+ if (dataset.template) {
803
+ dataset.template = dataset.engine.compile(parentTemplate.replace("%body", dataset.template));
804
+ } else {
805
+ dataset.template = {
806
+ render: function(context) {
807
+ return parentTemplate.replace("%body", "<p>" + context.value + "</p>");
808
+ }
809
+ };
810
+ }
811
+ });
812
+ this.inputView = new InputView({
813
+ input: this.$node.find(".tt-query"),
814
+ hint: this.$node.find(".tt-hint")
815
+ });
816
+ this.dropdownView = new DropdownView({
817
+ menu: this.$node.find(".tt-dropdown-menu")
818
+ });
819
+ this.dropdownView.on("select", this._handleSelection).on("cursorOn", this._clearHint).on("cursorOn", this._setInputValueToSuggestionUnderCursor).on("cursorOff", this._setInputValueToQuery).on("cursorOff", this._updateHint).on("suggestionsRender", this._updateHint).on("show", this._updateHint).on("hide", this._clearHint);
820
+ this.inputView.on("focus", this._showDropdown).on("blur", this._hideDropdown).on("blur", this._setInputValueToQuery).on("enter", this._handleSelection).on("queryChange", this._clearHint).on("queryChange", this._clearSuggestions).on("queryChange", this._getSuggestions).on("whitespaceChange", this._updateHint).on("queryChange whitespaceChange", this._showDropdown).on("queryChange whitespaceChange", this._setLanguageDirection).on("esc", this._hideDropdown).on("esc", this._setInputValueToQuery).on("up down", this._moveDropdownCursor).on("up down", this._showDropdown).on("tab", this._setPreventDefaultValueForTab).on("tab left right", this._autocomplete);
821
+ }
822
+ utils.mixin(TypeaheadView.prototype, EventTarget, {
823
+ _setPreventDefaultValueForTab: function(e) {
824
+ var hint = this.inputView.getHintValue(), inputValue = this.inputView.getInputValue(), preventDefault = hint && hint !== inputValue;
825
+ this.inputView.setPreventDefaultValueForKey("9", preventDefault);
826
+ },
827
+ _setLanguageDirection: function() {
828
+ var dirClassName = "tt-" + this.inputView.getLanguageDirection();
829
+ if (!this.$node.hasClass(dirClassName)) {
830
+ this.$node.removeClass("tt-ltr tt-rtl").addClass(dirClassName);
831
+ }
832
+ },
833
+ _updateHint: function() {
834
+ var dataForFirstSuggestion = this.dropdownView.getFirstSuggestion(), hint = dataForFirstSuggestion ? dataForFirstSuggestion.value : null, inputValue, query, beginsWithQuery, match;
835
+ if (hint && this.dropdownView.isOpen()) {
836
+ inputValue = this.inputView.getInputValue();
837
+ query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, "");
838
+ beginsWithQuery = new RegExp("^(?:" + query + ")(.*$)", "i");
839
+ match = beginsWithQuery.exec(hint);
840
+ this.inputView.setHintValue(inputValue + (match ? match[1] : ""));
841
+ }
842
+ },
843
+ _clearHint: function() {
844
+ this.inputView.setHintValue("");
845
+ },
846
+ _clearSuggestions: function() {
847
+ this.dropdownView.clearSuggestions();
848
+ },
849
+ _setInputValueToQuery: function() {
850
+ this.inputView.setInputValue(this.inputView.getQuery());
851
+ },
852
+ _setInputValueToSuggestionUnderCursor: function(e) {
853
+ this.inputView.setInputValue(e.data.value, true);
854
+ },
855
+ _showDropdown: function() {
856
+ this.dropdownView.show();
857
+ },
858
+ _hideDropdown: function(e) {
859
+ this.dropdownView[e.type === "blur" ? "hideUnlessMouseIsOverDropdown" : "hide"]();
860
+ },
861
+ _moveDropdownCursor: function(e) {
862
+ this.dropdownView[e.type === "up" ? "moveCursorUp" : "moveCursorDown"]();
863
+ },
864
+ _handleSelection: function(e) {
865
+ var byClick = e.type === "select", suggestionData = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor();
866
+ if (suggestionData) {
867
+ this.inputView.setInputValue(suggestionData.value);
868
+ byClick ? this.inputView.focus() : e.data.preventDefault();
869
+ byClick && utils.isMsie() ? setTimeout(this.dropdownView.hide, 0) : this.dropdownView.hide();
870
+ }
871
+ },
872
+ _getSuggestions: function() {
873
+ var that = this, query = this.inputView.getQuery();
874
+ utils.each(this.datasets, function(i, dataset) {
875
+ dataset.getSuggestions(query, function(suggestions) {
876
+ that._renderSuggestions(query, dataset, suggestions);
877
+ });
878
+ });
879
+ },
880
+ _renderSuggestions: function(query, dataset, suggestions) {
881
+ if (query !== this.inputView.getQuery()) {
882
+ return;
883
+ }
884
+ suggestions = suggestions.slice(0, dataset.limit);
885
+ this.dropdownView.renderSuggestions(query, dataset, suggestions);
886
+ },
887
+ _autocomplete: function(e) {
888
+ var isCursorAtEnd, ignoreEvent, query, hint;
889
+ if (e.type === "right" || e.type === "left") {
890
+ isCursorAtEnd = this.inputView.isCursorAtEnd();
891
+ ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "left" : e.type === "right";
892
+ if (!isCursorAtEnd || ignoreEvent) {
893
+ return;
894
+ }
895
+ }
896
+ query = this.inputView.getQuery();
897
+ hint = this.inputView.getHintValue();
898
+ if (hint !== "" && query !== hint) {
899
+ this.inputView.setInputValue(hint);
900
+ }
901
+ }
902
+ });
903
+ return TypeaheadView;
904
+ function wrapInput(input) {
905
+ var $input = $(input), $hint = $(html.hint).css({
906
+ "background-color": $input.css("background-color")
907
+ });
908
+ if ($input.length === 0) {
909
+ return null;
910
+ }
911
+ try {
912
+ !$input.attr("dir") && $input.attr("dir", "auto");
913
+ } catch (e) {}
914
+ return $input.attr({
915
+ autocomplete: false,
916
+ spellcheck: false
917
+ }).addClass("tt-query").wrap(html.wrapper).parent().prepend($hint).append(html.dropdown);
918
+ }
919
+ }();
920
+ (function() {
921
+ var initializedDatasets = {}, transportOptions = {}, transport, methods;
922
+ jQuery.fn.typeahead = typeahead;
923
+ typeahead.configureTransport = configureTransport;
924
+ methods = {
925
+ initialize: function(datasetDefs) {
926
+ var datasets = {};
927
+ datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ];
928
+ if (datasetDefs.length === 0) {
929
+ throw new Error("no datasets provided");
930
+ }
931
+ delete typeahead.configureTransport;
932
+ transport = transport || new Transport(transportOptions);
933
+ utils.each(datasetDefs, function(i, datasetDef) {
934
+ var dataset, name = datasetDef.name = datasetDef.name || utils.getUniqueId();
935
+ if (initializedDatasets[name]) {
936
+ dataset = initializedDatasets[name];
937
+ } else {
938
+ datasetDef.limit = datasetDef.limit || 5;
939
+ datasetDef.template = datasetDef.template;
940
+ datasetDef.engine = datasetDef.engine;
941
+ if (datasetDef.template && !datasetDef.engine) {
942
+ throw new Error("no template engine specified for " + name);
943
+ }
944
+ dataset = initializedDatasets[name] = new Dataset({
945
+ name: datasetDef.name,
946
+ limit: datasetDef.limit,
947
+ local: datasetDef.local,
948
+ prefetch: datasetDef.prefetch,
949
+ remote: datasetDef.remote,
950
+ matcher: datasetDef.matcher,
951
+ ranker: datasetDef.ranker,
952
+ transport: transport
953
+ });
954
+ }
955
+ datasets[name] = {
956
+ name: datasetDef.name,
957
+ limit: datasetDef.limit,
958
+ template: datasetDef.template,
959
+ engine: datasetDef.engine,
960
+ getSuggestions: dataset.getSuggestions
961
+ };
962
+ });
963
+ return this.each(function() {
964
+ $(this).data({
965
+ typeahead: new TypeaheadView({
966
+ input: this,
967
+ datasets: datasets
968
+ })
969
+ });
970
+ });
971
+ }
972
+ };
973
+ function typeahead(method) {
974
+ if (methods[method]) {
975
+ methods[method].apply(this, [].slice.call(arguments, 1));
976
+ } else {
977
+ methods.initialize.apply(this, arguments);
978
+ }
979
+ }
980
+ function configureTransport(o) {
981
+ transportOptions = o;
982
+ }
983
+ })();
984
+ })();