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