/html/js/bootstrap-typeahead.js
JavaScript | 285 lines | 200 code | 61 blank | 24 comment | 28 complexity | a71319e43efd22bf29161d0e75b892b7 MD5 | raw file
1/* =============================================================
2 * bootstrap-typeahead.js v2.0.4
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.$menu = $(this.options.menu).appendTo('body')
37 this.source = this.options.source
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.offset(), {
60 height: this.$element[0].offsetHeight
61 })
62
63 this.$menu.css({
64 top: pos.top + pos.height
65 , left: pos.left
66 })
67
68 this.$menu.show()
69 this.shown = true
70 return this
71 }
72
73 , hide: function () {
74 this.$menu.hide()
75 this.shown = false
76 return this
77 }
78
79 , lookup: function (event) {
80 var that = this
81 , items
82 , q
83
84 this.query = this.$element.val()
85
86 if (!this.query) {
87 return this.shown ? this.hide() : this
88 }
89
90 items = $.grep(this.source, function (item) {
91 return that.matcher(item)
92 })
93
94 items = this.sorter(items)
95
96 if (!items.length) {
97 return this.shown ? this.hide() : this
98 }
99
100 return this.render(items.slice(0, this.options.items)).show()
101 }
102
103 , matcher: function (item) {
104 return ~item.toLowerCase().indexOf(this.query.toLowerCase())
105 }
106
107 , sorter: function (items) {
108 var beginswith = []
109 , caseSensitive = []
110 , caseInsensitive = []
111 , item
112
113 while (item = items.shift()) {
114 if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
115 else if (~item.indexOf(this.query)) caseSensitive.push(item)
116 else caseInsensitive.push(item)
117 }
118
119 return beginswith.concat(caseSensitive, caseInsensitive)
120 }
121
122 , highlighter: function (item) {
123 var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
124 return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
125 return '<strong>' + match + '</strong>'
126 })
127 }
128
129 , render: function (items) {
130 var that = this
131
132 items = $(items).map(function (i, item) {
133 i = $(that.options.item).attr('data-value', item)
134 i.find('a').html(that.highlighter(item))
135 return i[0]
136 })
137
138 items.first().addClass('active')
139 this.$menu.html(items)
140 return this
141 }
142
143 , next: function (event) {
144 var active = this.$menu.find('.active').removeClass('active')
145 , next = active.next()
146
147 if (!next.length) {
148 next = $(this.$menu.find('li')[0])
149 }
150
151 next.addClass('active')
152 }
153
154 , prev: function (event) {
155 var active = this.$menu.find('.active').removeClass('active')
156 , prev = active.prev()
157
158 if (!prev.length) {
159 prev = this.$menu.find('li').last()
160 }
161
162 prev.addClass('active')
163 }
164
165 , listen: function () {
166 this.$element
167 .on('blur', $.proxy(this.blur, this))
168 .on('keypress', $.proxy(this.keypress, this))
169 .on('keyup', $.proxy(this.keyup, this))
170
171 if ($.browser.webkit || $.browser.msie) {
172 this.$element.on('keydown', $.proxy(this.keypress, this))
173 }
174
175 this.$menu
176 .on('click', $.proxy(this.click, this))
177 .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
178 }
179
180 , keyup: function (e) {
181 switch(e.keyCode) {
182 case 40: // down arrow
183 case 38: // up arrow
184 break
185
186 case 9: // tab
187 case 13: // enter
188 if (!this.shown) return
189 this.select()
190 break
191
192 case 27: // escape
193 if (!this.shown) return
194 this.hide()
195 break
196
197 default:
198 this.lookup()
199 }
200
201 e.stopPropagation()
202 e.preventDefault()
203 }
204
205 , keypress: function (e) {
206 if (!this.shown) return
207
208 switch(e.keyCode) {
209 case 9: // tab
210 case 13: // enter
211 case 27: // escape
212 e.preventDefault()
213 break
214
215 case 38: // up arrow
216 if (e.type != 'keydown') break
217 e.preventDefault()
218 this.prev()
219 break
220
221 case 40: // down arrow
222 if (e.type != 'keydown') break
223 e.preventDefault()
224 this.next()
225 break
226 }
227
228 e.stopPropagation()
229 }
230
231 , blur: function (e) {
232 var that = this
233 setTimeout(function () { that.hide() }, 150)
234 }
235
236 , click: function (e) {
237 e.stopPropagation()
238 e.preventDefault()
239 this.select()
240 }
241
242 , mouseenter: function (e) {
243 this.$menu.find('.active').removeClass('active')
244 $(e.currentTarget).addClass('active')
245 }
246
247 }
248
249
250 /* TYPEAHEAD PLUGIN DEFINITION
251 * =========================== */
252
253 $.fn.typeahead = function (option) {
254 return this.each(function () {
255 var $this = $(this)
256 , data = $this.data('typeahead')
257 , options = typeof option == 'object' && option
258 if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
259 if (typeof option == 'string') data[option]()
260 })
261 }
262
263 $.fn.typeahead.defaults = {
264 source: []
265 , items: 8
266 , menu: '<ul class="typeahead dropdown-menu"></ul>'
267 , item: '<li><a href="#"></a></li>'
268 }
269
270 $.fn.typeahead.Constructor = Typeahead
271
272
273 /* TYPEAHEAD DATA-API
274 * ================== */
275
276 $(function () {
277 $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
278 var $this = $(this)
279 if ($this.data('typeahead')) return
280 e.preventDefault()
281 $this.typeahead($this.data())
282 })
283 })
284
285}(window.jQuery);