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