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