1/* ============================================================= 2 * bootstrap-typeahead.js v2.3.1 3 * http://twitter.github.com/bootstrap/javascript.html#typeahead 4 * ============================================================= 5 * Copyright 2012 Twitter, Inc. 6 * 7 * Licensed under the Apache License, Version 2.0 (the "License"); 8 * you may not use this file except in compliance with the License. 9 * You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 * ============================================================ */ 19 20 21!function($){ 22 23 "use strict"; // jshint ;_; 24 25 26 /* TYPEAHEAD PUBLIC CLASS DEFINITION 27 * ================================= */ 28 29 var Typeahead = function (element, options) { 30 this.$element = $(element) 31 this.options = $.extend({}, $.fn.typeahead.defaults, options) 32 this.matcher = this.options.matcher || this.matcher 33 this.sorter = this.options.sorter || this.sorter 34 this.highlighter = this.options.highlighter || this.highlighter 35 this.updater = this.options.updater || this.updater 36 this.source = this.options.source 37 this.$menu = $(this.options.menu) 38 this.shown = false 39 this.listen() 40 } 41 42 Typeahead.prototype = { 43 44 constructor: Typeahead 45 46 , select: function () { 47 var val = this.$menu.find('.active').attr('data-value') 48 this.$element 49 .val(this.updater(val)) 50 .change() 51 return this.hide() 52 } 53 54 , updater: function (item) { 55 return item 56 } 57 58 , show: function () { 59 var pos = $.extend({}, this.$element.position(), { 60 height: this.$element[0].offsetHeight 61 }) 62 63 this.$menu 64 .insertAfter(this.$element) 65 .css({ 66 top: pos.top + pos.height 67 , left: pos.left 68 }) 69 .show() 70 71 this.shown = true 72 return this 73 } 74 75 , hide: function () { 76 this.$menu.hide() 77 this.shown = false 78 return this 79 } 80 81 , lookup: function (event) { 82 var items 83 84 this.query = this.$element.val() 85 86 if (!this.query || this.query.length < this.options.minLength) { 87 return this.shown ? this.hide() : this 88 } 89 90 items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source 91 92 return items ? this.process(items) : this 93 } 94 95 , process: function (items) { 96 var that = this 97 98 items = $.grep(items, function (item) { 99 return that.matcher(item) 100 }) 101 102 items = this.sorter(items) 103 104 if (!items.length) { 105 return this.shown ? this.hide() : this 106 } 107 108 return this.render(items.slice(0, this.options.items)).show() 109 } 110 111 , matcher: function (item) { 112 return ~item.toLowerCase().indexOf(this.query.toLowerCase()) 113 } 114 115 , sorter: function (items) { 116 var beginswith = [] 117 , caseSensitive = [] 118 , caseInsensitive = [] 119 , item 120 121 while (item = items.shift()) { 122 if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) 123 else if (~item.indexOf(this.query)) caseSensitive.push(item) 124 else caseInsensitive.push(item) 125 } 126 127 return beginswith.concat(caseSensitive, caseInsensitive) 128 } 129 130 , highlighter: function (item) { 131 var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') 132 return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { 133 return '<strong>' + match + '</strong>' 134 }) 135 } 136 137 , render: function (items) { 138 var that = this 139 140 items = $(items).map(function (i, item) { 141 i = $(that.options.item).attr('data-value', item) 142 i.find('a').html(that.highlighter(item)) 143 return i[0] 144 }) 145 146 items.first().addClass('active') 147 this.$menu.html(items) 148 return this 149 } 150 151 , next: function (event) { 152 var active = this.$menu.find('.active').removeClass('active') 153 , next = active.next() 154 155 if (!next.length) { 156 next = $(this.$menu.find('li')[0]) 157 } 158 159 next.addClass('active') 160 } 161 162 , prev: function (event) { 163 var active = this.$menu.find('.active').removeClass('active') 164 , prev = active.prev() 165 166 if (!prev.length) { 167 prev = this.$menu.find('li').last() 168 } 169 170 prev.addClass('active') 171 } 172 173 , listen: function () { 174 this.$element 175 .on('focus', $.proxy(this.focus, this)) 176 .on('blur', $.proxy(this.blur, this)) 177 .on('keypress', $.proxy(this.keypress, this)) 178 .on('keyup', $.proxy(this.keyup, this)) 179 180 if (this.eventSupported('keydown')) { 181 this.$element.on('keydown', $.proxy(this.keydown, this)) 182 } 183 184 this.$menu 185 .on('click', $.proxy(this.click, this)) 186 .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) 187 .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) 188 } 189 190 , eventSupported: function(eventName) { 191 var isSupported = eventName in this.$element 192 if (!isSupported) { 193 this.$element.setAttribute(eventName, 'return;') 194 isSupported = typeof this.$element[eventName] === 'function' 195 } 196 return isSupported 197 } 198 199 , move: function (e) { 200 if (!this.shown) return 201 202 switch(e.keyCode) { 203 case 9: // tab 204 case 13: // enter 205 case 27: // escape 206 e.preventDefault() 207 break 208 209 case 38: // up arrow 210 e.preventDefault() 211 this.prev() 212 break 213 214 case 40: // down arrow 215 e.preventDefault() 216 this.next() 217 break 218 } 219 220 e.stopPropagation() 221 } 222 223 , keydown: function (e) { 224 this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) 225 this.move(e) 226 } 227 228 , keypress: function (e) { 229 if (this.suppressKeyPressRepeat) return 230 this.move(e) 231 } 232 233 , keyup: function (e) { 234 switch(e.keyCode) { 235 case 40: // down arrow 236 case 38: // up arrow 237 case 16: // shift 238 case 17: // ctrl 239 case 18: // alt 240 break 241 242 case 9: // tab 243 case 13: // enter 244 if (!this.shown) return 245 this.select() 246 break 247 248 case 27: // escape 249 if (!this.shown) return 250 this.hide() 251 break 252 253 default: 254 this.lookup() 255 } 256 257 e.stopPropagation() 258 e.preventDefault() 259 } 260 261 , focus: function (e) { 262 this.focused = true 263 } 264 265 , blur: function (e) { 266 this.focused = false 267 if (!this.mousedover && this.shown) this.hide() 268 } 269 270 , click: function (e) { 271 e.stopPropagation() 272 e.preventDefault() 273 this.select() 274 this.$element.focus() 275 } 276 277 , mouseenter: function (e) { 278 this.mousedover = true 279 this.$menu.find('.active').removeClass('active') 280 $(e.currentTarget).addClass('active') 281 } 282 283 , mouseleave: function (e) { 284 this.mousedover = false 285 if (!this.focused && this.shown) this.hide() 286 } 287 288 } 289 290 291 /* TYPEAHEAD PLUGIN DEFINITION 292 * =========================== */ 293 294 var old = $.fn.typeahead 295 296 $.fn.typeahead = function (option) { 297 return this.each(function () { 298 var $this = $(this) 299 , data = $this.data('typeahead') 300 , options = typeof option == 'object' && option 301 if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) 302 if (typeof option == 'string') data[option]() 303 }) 304 } 305 306 $.fn.typeahead.defaults = { 307 source: [] 308 , items: 8 309 , menu: '<ul class="typeahead dropdown-menu"></ul>' 310 , item: '<li><a href="#"></a></li>' 311 , minLength: 1 312 } 313 314 $.fn.typeahead.Constructor = Typeahead 315 316 317 /* TYPEAHEAD NO CONFLICT 318 * =================== */ 319 320 $.fn.typeahead.noConflict = function () { 321 $.fn.typeahead = old 322 return this 323 } 324 325 326 /* TYPEAHEAD DATA-API 327 * ================== */ 328 329 $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 330 var $this = $(this) 331 if ($this.data('typeahead')) return 332 $this.typeahead($this.data()) 333 }) 334 335}(window.jQuery); 336