typeahead-rails 0.9.3.4 → 0.10.1

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