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