Using bootstrap-collapse.js
diff --git a/js/bootstrap-typeahead.js b/js/bootstrap-typeahead.js
new file mode 100644
index 0000000000..52ced3fef8
--- /dev/null
+++ b/js/bootstrap-typeahead.js
@@ -0,0 +1,190 @@
+/* =============================================================
+ * bootstrap-typeahead.js v2.0.0
+ * http://twitter.github.com/bootstrap/javascript.html#collapsible
+ * =============================================================
+ * Copyright 2011 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+!function( $ ){
+
+ "use strict"
+
+ var Typeahead = function ( element, options ) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.typeahead.defaults, options)
+ this.$menu = $(this.options.menu).appendTo('body')
+ this.data = this.options.data
+ this.shown = false
+ this.listen()
+ }
+
+ Typeahead.prototype = {
+
+ constructor: Typeahead
+
+ , matcher: function(item, query) {
+ return ~item.indexOf(query)
+ }
+
+ , select: function(event) {
+ this.$element.val($(event.target).attr('data-value'))
+ this.hide()
+ }
+
+ , show: function () {
+ this.shown = true
+ this.$menu.show()
+ return this
+ }
+
+ , hide: function () {
+ this.shown = false
+ this.$menu.hide()
+ return this
+ }
+
+ , lookup: function (event) {
+ var query = this.$element.val()
+ , that = this
+
+ var items = this.data.filter(function (item) {
+ if (that.matcher(item, query)) {
+ return item
+ }
+ })
+
+ if (!items.length) {
+ return this.shown ? this.hide() : this
+ }
+
+ return this.render(items).show()
+ }
+
+ , render: function(items) {
+ var that = this
+
+ items = $(items).map(function (i, item) {
+ return $(that.options.item)
+ .text(item)
+ .attr('data-value', item)[0]
+ })
+
+ items.first().addClass('active')
+
+ this.$menu.append(items)
+
+ return this
+ }
+
+ , next: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.next() || $(this.$menu.find('li')[0])
+
+ next.addClass('active')
+ }
+
+ , prev: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.prev() || this.$menu.find('li').last()
+
+ next.addClass('active')
+ }
+
+ , keyup: function () {
+ event
+ .stopPropagation()
+ .preventDefault()
+
+ switch(event.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ this.select()
+ break
+
+ case 27: // escape
+ this.hide()
+ break
+
+ default:
+ this.lookup()
+ }
+ }
+
+ , keypress: function (event) {
+ event.stopPropagation()
+ switch(event.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ case 27: // escape
+ event.preventDefault()
+ break
+
+ case 38: // up arrow
+ this.prev()
+ event.preventDefault()
+ break
+
+ case 40: // down arrow
+ this.next()
+ event.preventDefault()
+ break
+ }
+ }
+
+ , listen: function () {
+ this.$element
+ .on('focus', this.show)
+ .on('blur', $.proxy(this.hide, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', this.keyup)
+ .on('change', $.proxy(this.lookup, this))
+
+ if ($.browser.webkit || $.browser.msie) {
+ this.$element.on('keydown', this.keypress)
+ }
+
+ this.$menu
+ .on('click', '* > *', $.proxy(this.select, this))
+ .on('mouseenter', function () {
+ /* remove selected class, add to mouseover */
+ })
+ }
+ }
+
+ /* TYPEAHEAD PLUGIN DEFINITION
+ * ============================== */
+
+ $.fn.typeahead = function ( option ) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('typeahead')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.typeahead.defaults = {
+ data: null
+ , items: 8
+ , empty: false
+ , noresults: false
+ , menu: ''
+ , item: '
'
+ }
+
+ $.fn.typeahead.Constructor = Typeahead
+
+}( window.jQuery )
\ No newline at end of file
diff --git a/js/tests/index.html b/js/tests/index.html
index e8ed2c5feb..27c2b34125 100644
--- a/js/tests/index.html
+++ b/js/tests/index.html
@@ -22,6 +22,7 @@
+
@@ -34,6 +35,7 @@
+
diff --git a/js/tests/unit/bootstrap-typeahead.js b/js/tests/unit/bootstrap-typeahead.js
new file mode 100644
index 0000000000..dc46a79909
--- /dev/null
+++ b/js/tests/unit/bootstrap-typeahead.js
@@ -0,0 +1,122 @@
+$(function () {
+
+ module("bootstrap-typeahead")
+
+ test("should be defined on jquery object", function () {
+ ok($(document.body).typeahead, 'alert method is defined')
+ })
+
+ test("should return element", function () {
+ ok($(document.body).typeahead()[0] == document.body, 'document.body returned')
+ })
+
+ test("should listen to an input", function () {
+ var $input = $('')
+ $input.typeahead()
+ ok($input.data('events').focus, 'has a focus event')
+ ok($input.data('events').blur, 'has a blur event')
+ ok($input.data('events').keypress, 'has a keypress event')
+ ok($input.data('events').keyup, 'has a keyup event')
+ ok($input.data('events').change, 'has a change event')
+ if ($.browser.webkit || $.browser.msie) {
+ ok($input.data('events').keydown, 'has a keydown event')
+ } else {
+ ok($input.data('events').keydown, 'does not have a keydown event')
+ }
+ })
+
+ test("should create a menu", function () {
+ var $input = $('')
+ ok($input.typeahead().data('typeahead').$menu, 'has a menu')
+ })
+
+ test("should listen to the menu", function () {
+ var $input = $('')
+ , $menu = $input.typeahead().data('typeahead').$menu
+
+ ok($menu.data('events').mouseover, 'has a mouseover(pseudo: mouseenter)')
+ ok($menu.data('events').click, 'has a click')
+ })
+
+ test("should show menu when query entered", function () {
+ var $input = $('').typeahead({
+ data: ['aa', 'ab', 'ac']
+ })
+ , typeahead = $input.data('typeahead')
+
+ $input.val('a').change()
+
+ ok(typeahead.$menu.is(":visible"), 'typeahead is visible')
+ equals(typeahead.$menu.find('li').length, 3, 'has 3 items in menu')
+ equals(typeahead.$menu.find('.active').length, 1, 'one item is active')
+
+ typeahead.$menu.remove()
+ })
+
+ test("should hide menu when query entered", function () {
+ var $input = $('').typeahead({
+ data: ['aa', 'ab', 'ac']
+ })
+ , typeahead = $input.data('typeahead')
+
+ $input.val('a').change()
+
+ ok(typeahead.$menu.is(":visible"), 'typeahead is visible')
+ equals(typeahead.$menu.find('li').length, 3, 'has 3 items in menu')
+ equals(typeahead.$menu.find('.active').length, 1, 'one item is active')
+
+ $input.blur()
+
+ ok(!typeahead.$menu.is(":visible"), "typeahead is no longer visible")
+
+ typeahead.$menu.remove()
+ })
+
+ test("should set next item when down arrow is pressed", function () {
+ var $input = $('').typeahead({
+ data: ['aa', 'ab', 'ac']
+ })
+ , typeahead = $input.data('typeahead')
+
+ $input.val('a').change()
+
+ ok(typeahead.$menu.is(":visible"), 'typeahead is visible')
+ equals(typeahead.$menu.find('li').length, 3, 'has 3 items in menu')
+ equals(typeahead.$menu.find('.active').length, 1, 'one item is active')
+ ok(typeahead.$menu.find('li').first().hasClass('active'), "first item is active")
+
+ $input.trigger({
+ type: 'keypress'
+ , keyCode: 40
+ })
+
+ ok(typeahead.$menu.find('li').first().next().hasClass('active'), "second item is active")
+
+
+ $input.trigger({
+ type: 'keypress'
+ , keyCode: 38
+ })
+
+ ok(typeahead.$menu.find('li').first().hasClass('active'), "first item is active")
+
+ typeahead.$menu.remove()
+ })
+
+
+ test("should set input value to selected item", function () {
+ var $input = $('').typeahead({
+ data: ['aa', 'ab', 'ac']
+ })
+ , typeahead = $input.data('typeahead')
+
+ $input.val('a').change()
+
+ $(typeahead.$menu.find('li')[2]).trigger('click')
+
+ equals($input.val(), 'ac', 'input value was correctly set')
+ ok(!typeahead.$menu.is(':visible'), 'the menu was hidden')
+
+ typeahead.$menu.remove()
+ })
+})
\ No newline at end of file