1// ---------------------------------------------------------------------------- 2// 3// bootstrap-typeahead.js 4// 5// Twitter Bootstrap Typeahead Plugin 6// v1.2.2 7// https://github.com/tcrosen/twitter-bootstrap-typeahead 8// 9// 10// Author 11// ---------- 12// Terry Rosen 13// [email protected] | @rerrify | github.com/tcrosen/ 14// 15// 16// Description 17// ---------- 18// Custom implementation of Twitter's Bootstrap Typeahead Plugin 19// http://twitter.github.com/bootstrap/javascript.html#typeahead 20// 21// 22// Requirements 23// ---------- 24// jQuery 1.7+ 25// Twitter Bootstrap 2.0+ 26// 27// ---------------------------------------------------------------------------- 28 29! 30function ($) { 31 32 "use strict"; 33 34 //------------------------------------------------------------------ 35 // 36 // Constructor 37 // 38 var Typeahead = function (element, options) { 39 this.$element = $(element); 40 this.options = $.extend(true, {}, $.fn.typeahead.defaults, options); 41 this.$menu = $(this.options.menu).appendTo('body'); 42 this.shown = false; 43 44 // Method overrides 45 this.eventSupported = this.options.eventSupported || this.eventSupported; 46 this.grepper = this.options.grepper || this.grepper; 47 this.highlighter = this.options.highlighter || this.highlighter; 48 this.lookup = this.options.lookup || this.lookup; 49 this.matcher = this.options.matcher || this.matcher; 50 this.render = this.options.render || this.render; 51 this.select = this.options.select || this.select; 52 this.sorter = this.options.sorter || this.sorter; 53 this.source = this.options.source || this.source; 54 55 if (!this.source.length) { 56 var ajax = this.options.ajax; 57 58 if (typeof ajax === 'string') { 59 this.ajax = $.extend({}, $.fn.typeahead.defaults.ajax, { url: ajax }); 60 } else { 61 this.ajax = $.extend({}, $.fn.typeahead.defaults.ajax, ajax); 62 } 63 64 if (!this.ajax.url) { 65 this.ajax = null; 66 } 67 } 68 69 this.listen(); 70 } 71 72 Typeahead.prototype = { 73 74 constructor: Typeahead, 75 76 //============================================================================================================= 77 // 78 // Utils 79 // 80 //============================================================================================================= 81 82 //------------------------------------------------------------------ 83 // 84 // Check if an event is supported by the browser eg. 'keypress' 85 // * This was included to handle the "exhaustive deprecation" of jQuery.browser in jQuery 1.8 86 // 87 eventSupported: function(eventName) { 88 var isSupported = (eventName in this.$element); 89 90 if (!isSupported) { 91 this.$element.setAttribute(eventName, 'return;'); 92 isSupported = typeof this.$element[eventName] === 'function'; 93 } 94 95 return isSupported; 96 }, 97 98 //============================================================================================================= 99 // 100 // AJAX 101 // 102 //============================================================================================================= 103 104 //------------------------------------------------------------------ 105 // 106 // Handle AJAX source 107 // 108 ajaxer: function () { 109 var that = this, 110 query = that.$element.val(); 111 112 if (query === that.query) { 113 return that; 114 } 115 116 // Query changed 117 that.query = query; 118 119 // Cancel last timer if set 120 if (that.ajax.timerId) { 121 clearTimeout(that.ajax.timerId); 122 that.ajax.timerId = null; 123 } 124 125 if (!query || query.length < that.ajax.triggerLength) { 126 // Cancel the ajax callback if in progress 127 if (that.ajax.xhr) { 128 that.ajax.xhr.abort(); 129 that.ajax.xhr = null; 130 that.ajaxToggleLoadClass(false); 131 } 132 133 return that.shown ? that.hide() : that; 134 } 135 136 // Query is good to send, set a timer 137 that.ajax.timerId = setTimeout(function() { 138 $.proxy(that.ajaxExecute(query), that) 139 }, that.ajax.timeout); 140 141 return that; 142 }, 143 144 //------------------------------------------------------------------ 145 // 146 // Execute an AJAX request 147 // 148 ajaxExecute: function(query) { 149 this.ajaxToggleLoadClass(true); 150 151 // Cancel last call if already in progress 152 if (this.ajax.xhr) this.ajax.xhr.abort(); 153 154 var params = this.ajax.preDispatch ? this.ajax.preDispatch(query) : { query : query }; 155 var jAjax = (this.ajax.method === "post") ? $.post : $.get; 156 this.ajax.xhr = jAjax(this.ajax.url, params, $.proxy(this.ajaxLookup, this)); 157 this.ajax.timerId = null; 158 }, 159 160 //------------------------------------------------------------------ 161 // 162 // Perform a lookup in the AJAX results 163 // 164 ajaxLookup: function (data) { 165 var items; 166 167 this.ajaxToggleLoadClass(false); 168 169 if (!this.ajax.xhr) return; 170 171 if (this.ajax.preProcess) { 172 data = this.ajax.preProcess(data); 173 } 174 175 // Save for selection retreival 176 this.ajax.data = data; 177 178 items = this.grepper(this.ajax.data); 179 180 if (!items || !items.length) { 181 return this.shown ? this.hide() : this; 182 } 183 184 this.ajax.xhr = null; 185 186 return this.render(items.slice(0, this.options.items)).show(); 187 }, 188 189 //------------------------------------------------------------------ 190 // 191 // Toggle the loading class 192 // 193 ajaxToggleLoadClass: function (enable) { 194 if (!this.ajax.loadingClass) return; 195 this.$element.toggleClass(this.ajax.loadingClass, enable); 196 }, 197 198 //============================================================================================================= 199 // 200 // Data manipulation 201 // 202 //============================================================================================================= 203 204 //------------------------------------------------------------------ 205 // 206 // Search source 207 // 208 lookup: function (event) { 209 var that = this, 210 items; 211 212 if (that.ajax) { 213 that.ajaxer(); 214 } 215 else { 216 that.query = that.$element.val(); 217 218 if (!that.query) { 219 return that.shown ? that.hide() : that; 220 } 221 222 items = that.grepper(that.source); 223 224 if (!items || !items.length) { 225 return that.shown ? that.hide() : that; 226 } 227 228 return that.render(items.slice(0, that.options.items)).show(); 229 } 230 }, 231 232 //------------------------------------------------------------------ 233 // 234 // Filters relevent results 235 // 236 grepper: function(data) { 237 var that = this, 238 items; 239 240 if (data && data.length && !data[0].hasOwnProperty(that.options.display)) { 241 return null; 242 } 243 244 items = $.grep(data, function (item) { 245 return that.matcher(item[that.options.display], item); 246 }); 247 248 return this.sorter(items); 249 }, 250 251 //------------------------------------------------------------------ 252 // 253 // Looks for a match in the source 254 // 255 matcher: function (item) { 256 return ~item.toLowerCase().indexOf(this.query.toLowerCase()); 257 }, 258 259 //------------------------------------------------------------------ 260 // 261 // Sorts the results 262 // 263 sorter: function (items) { 264 var that = this, 265 beginswith = [], 266 caseSensitive = [], 267 caseInsensitive = [], 268 item; 269 270 while (item = items.shift()) { 271 if (!item[that.options.display].toLowerCase().indexOf(this.query.toLowerCase())) { 272 beginswith.push(item); 273 } 274 else if (~item[that.options.display].indexOf(this.query)) { 275 caseSensitive.push(item); 276 } 277 else { 278 caseInsensitive.push(item); 279 } 280 } 281 282 return beginswith.concat(caseSensitive, caseInsensitive); 283 }, 284 285 //============================================================================================================= 286 // 287 // DOM manipulation 288 // 289 //============================================================================================================= 290 291 //------------------------------------------------------------------ 292 // 293 // Shows the results list 294 // 295 show: function () { 296 var pos = $.extend({}, this.$element.offset(), { 297 height: this.$element[0].offsetHeight 298 }); 299 300 this.$menu.css({ 301 top: pos.top + pos.height, 302 left: pos.left 303 }); 304 305 this.$menu.show(); 306 this.shown = true; 307 308 return this; 309 }, 310 311 //------------------------------------------------------------------ 312 // 313 // Hides the results list 314 // 315 hide: function () { 316 this.$menu.hide(); 317 this.shown = false;
318 return this; 319 }, 320 321 //------------------------------------------------------------------ 322 // 323 // Highlights the match(es) within the results 324 // 325 highlighter: function (item) { 326 var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); 327 return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { 328 return '<strong>' + match + '</strong>'; 329 }); 330 }, 331 332 //------------------------------------------------------------------ 333 // 334 // Renders the results list 335 // 336 render: function (items) { 337 var that = this; 338 339 items = $(items).map(function (i, item) { 340 i = $(that.options.item).attr('data-value', item[that.options.val]); 341 i.find('a').html(that.highlighter(item[that.options.display], item)); 342 return i[0]; 343 }); 344 345 items.first().addClass('active'); 346 this.$menu.html(items); 347 return this; 348 }, 349 350 //------------------------------------------------------------------ 351 // 352 // Item is selected 353 // 354 select: function () { 355 var $selectedItem = this.$menu.find('.active'); 356 this.$element.val($selectedItem.text()).change(); 357 this.options.itemSelected($selectedItem, $selectedItem.attr('data-value'), $selectedItem.text()); 358 return this.hide(); 359 }, 360 361 //------------------------------------------------------------------ 362 // 363 // Selects the next result 364 // 365 next: function (event) { 366 var active = this.$menu.find('.active').removeClass('active'); 367 var next = active.next(); 368 369 if (!next.length) { 370 next = $(this.$menu.find('li')[0]); 371 } 372 373 next.addClass('active'); 374 }, 375 376 //------------------------------------------------------------------ 377 // 378 // Selects the previous result 379 // 380 prev: function (event) { 381 var active = this.$menu.find('.active').removeClass('active'); 382 var prev = active.prev(); 383 384 if (!prev.length) { 385 prev = this.$menu.find('li').last(); 386 } 387 388 prev.addClass('active'); 389 }, 390 391 //============================================================================================================= 392 // 393 // Events 394 // 395 //============================================================================================================= 396 397 //------------------------------------------------------------------ 398 // 399 // Listens for user events 400 // 401 listen: function () { 402 this.$element.on('blur', $.proxy(this.blur, this)) 403 .on('keyup', $.proxy(this.keyup, this)); 404 405 if (this.eventSupported('keydown')) { 406 this.$element.on('keydown', $.proxy(this.keypress, this)); 407 } else { 408 this.$element.on('keypress', $.proxy(this.keypress, this)); 409 } 410 411 this.$menu.on('click', $.proxy(this.click, this)) 412 .on('mouseenter', 'li', $.proxy(this.mouseenter, this)); 413 }, 414 415 //------------------------------------------------------------------ 416 // 417 // Handles a key being raised up 418 // 419 keyup: function (e) { 420 e.stopPropagation(); 421 e.preventDefault(); 422 423 switch (e.keyCode) { 424 case 40: 425 // down arrow 426 case 38: 427 // up arrow 428 break; 429 case 9: 430 // tab 431 case 13: 432 // enter 433 if (!this.shown) { 434 return; 435 } 436 this.select(); 437 break; 438 case 27: 439 // escape 440 this.hide(); 441 break; 442 default: 443 this.lookup(); 444 } 445 }, 446 447 //------------------------------------------------------------------ 448 // 449 // Handles a key being pressed 450 // 451 keypress: function (e) { 452 e.stopPropagation(); 453 if (!this.shown) { 454 return; 455 } 456 457 switch (e.keyCode) { 458 case 9: 459 // tab 460 case 13: 461 // enter 462 case 27: 463 // escape 464 e.preventDefault(); 465 break; 466 case 38: 467 // up arrow 468 e.preventDefault(); 469 this.prev(); 470 break; 471 case 40: 472 // down arrow 473 e.preventDefault(); 474 this.next(); 475 break; 476 } 477 }, 478 479 //------------------------------------------------------------------ 480 // 481 // Handles cursor exiting the textbox 482 // 483 blur: function (e) { 484 var that = this; 485 e.stopPropagation(); 486 e.preventDefault(); 487 setTimeout(function () { 488 if (!that.$menu.is(':focus')) { 489 that.hide(); 490 } 491 }, 150) 492 }, 493 494 //------------------------------------------------------------------ 495 // 496 // Handles clicking on the results list 497 // 498 click: function (e) { 499 e.stopPropagation(); 500 e.preventDefault(); 501 this.select(); 502 }, 503 504 //------------------------------------------------------------------ 505 // 506 // Handles the mouse entering the results list 507 // 508 mouseenter: function (e) { 509 this.$menu.find('.active').removeClass('active'); 510 $(e.currentTarget).addClass('active'); 511 } 512 } 513 514 //------------------------------------------------------------------ 515 // 516 // Plugin definition 517 // 518 $.fn.typeahead = function (option) { 519 return this.each(function () { 520 var $this = $(this), 521 data = $this.data('typeahead'), 522 options = typeof option === 'object' && option; 523 524 if (!data) { 525 $this.data('typeahead', (data = new Typeahead(this, options))); 526 } 527 528 if (typeof option === 'string') { 529 data[option](); 530 } 531 }); 532 } 533 534 //------------------------------------------------------------------ 535 // 536 // Defaults 537 // 538 $.fn.typeahead.defaults = { 539 source: [], 540 items: 8, 541 menu: '<ul class="typeahead dropdown-menu"></ul>', 542 item: '<li><a href="#"></a></li>', 543 display: 'name', 544 val: 'id', 545 itemSelected: function () { }, 546 ajax: { 547 url: null, 548 timeout: 300, 549 method: 'post', 550 triggerLength: 3, 551 loadingClass: null, 552 displayField: null, 553 preDispatch: null, 554 preProcess: null 555 } 556 } 557 558 $.fn.typeahead.Constructor = Typeahead; 559 560 //------------------------------------------------------------------ 561 // 562 // DOM-ready call for the Data API (no-JS implementation) 563 // 564 // Note: As of Bootstrap v2.0 this feature may be disabled using $('body').off('.data-api') 565 // More info here: https://github.com/twitter/bootstrap/tree/master/js 566 // 567 $(function () {
568 $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 569 var $this = $(this); 570 571 if ($this.data('typeahead')) { 572 return; 573 } 574 575 e.preventDefault(); 576 $this.typeahead($this.data()); 577 }) 578 }); 579 580} (window.jQuery);
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.