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