1 /* =============================================================
2 * bootstrap-typeahead.js v2.3.2
3 * http://twbs.github.com/bootstrap/javascript.html#typeahead
4 * =============================================================
5 * Copyright 2013 Twitter, Inc.
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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 * ============================================================ */
23 "use strict"; // jshint ;_;
26 /* TYPEAHEAD PUBLIC CLASS DEFINITION
27 * ================================= */
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)
42 Typeahead.prototype = {
44 constructor: Typeahead
46 , select: function () {
47 var val = this.$menu.find('.active').attr('data-value')
49 .val(this.updater(val))
54 , updater: function (item) {
59 var pos = $.extend({}, this.$element.position(), {
60 height: this.$element[0].offsetHeight
64 .insertAfter(this.$element)
66 top: pos.top + pos.height
81 , lookup: function (event) {
84 this.query = this.$element.val()
86 if (!this.query || this.query.length < this.options.minLength) {
87 return this.shown ? this.hide() : this
90 items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
92 return items ? this.process(items) : this
95 , process: function (items) {
98 items = $.grep(items, function (item) {
99 return that.matcher(item)
102 items = this.sorter(items)
105 return this.shown ? this.hide() : this
108 return this.render(items.slice(0, this.options.items)).show()
111 , matcher: function (item) {
112 return ~item.toLowerCase().indexOf(this.query.toLowerCase())
115 , sorter: function (items) {
118 , caseInsensitive = []
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)
127 return beginswith.concat(caseSensitive, caseInsensitive)
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>'
137 , render: function (items) {
140 items = $(items).map(function (i, item) {
141 i = $(that.options.item).attr('data-value', item)
142 i.find('a').html(that.highlighter(item))
146 items.first().addClass('active')
147 this.$menu.html(items)
151 , next: function (event) {
152 var active = this.$menu.find('.active').removeClass('active')
153 , next = active.next()
156 next = $(this.$menu.find('li')[0])
159 next.addClass('active')
162 , prev: function (event) {
163 var active = this.$menu.find('.active').removeClass('active')
164 , prev = active.prev()
167 prev = this.$menu.find('li').last()
170 prev.addClass('active')
173 , listen: function () {
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))
180 if (this.eventSupported('keydown')) {
181 this.$element.on('keydown', $.proxy(this.keydown, this))
185 .on('click', $.proxy(this.click, this))
186 .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
187 .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
190 , eventSupported: function(eventName) {
191 var isSupported = eventName in this.$element
193 this.$element.setAttribute(eventName, 'return;')
194 isSupported = typeof this.$element[eventName] === 'function'
199 , move: function (e) {
200 if (!this.shown) return
214 case 40: // down arrow
223 , keydown: function (e) {
224 this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
228 , keypress: function (e) {
229 if (this.suppressKeyPressRepeat) return
233 , keyup: function (e) {
235 case 40: // down arrow
244 if (!this.shown) return
249 if (!this.shown) return
261 , focus: function (e) {
265 , blur: function (e) {
267 if (!this.mousedover && this.shown) this.hide()
270 , click: function (e) {
274 this.$element.focus()
277 , mouseenter: function (e) {
278 this.mousedover = true
279 this.$menu.find('.active').removeClass('active')
280 $(e.currentTarget).addClass('active')
283 , mouseleave: function (e) {
284 this.mousedover = false
285 if (!this.focused && this.shown) this.hide()
291 /* TYPEAHEAD PLUGIN DEFINITION
292 * =========================== */
294 var old = $.fn.typeahead
296 $.fn.typeahead = function (option) {
297 return this.each(function () {
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]()
306 $.fn.typeahead.defaults = {
309 , menu: '<ul class="typeahead dropdown-menu"></ul>'
310 , item: '<li><a href="#"></a></li>'
314 $.fn.typeahead.Constructor = Typeahead
317 /* TYPEAHEAD NO CONFLICT
318 * =================== */
320 $.fn.typeahead.noConflict = function () {
326 /* TYPEAHEAD DATA-API
327 * ================== */
329 $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
331 if ($this.data('typeahead')) return
332 $this.typeahead($this.data())