Skip to content

Instantly share code, notes, and snippets.

@jrust
Forked from pezholio/README.markdown
Created March 13, 2012 17:57

Revisions

  1. Jason Rust revised this gist Mar 13, 2012. 1 changed file with 188 additions and 111 deletions.
    299 changes: 188 additions & 111 deletions gistfile1.diff
    Original file line number Diff line number Diff line change
    @@ -1,111 +1,188 @@
    2c2
    < * bootstrap-typeahead.js v2.0.2
    ---
    > * bootstrap-typeahead.js v2.0.0
    31a32,35
    > this.onselect = this.options.onselect
    > this.autoselect = this.options.autoselect
    > this.autowidth = this.options.autowidth
    > this.strings = true
    41,43c45,61
    < var val = this.$menu.find('.active').attr('data-value')
    < this.$element.val(val)
    < this.$element.change();
    ---
    > var text, original_text;
    > if (this.$menu.find('.active').length == 0) {
    > var val = this.$element.val();
    > }
    > else {
    > var val = JSON.parse(this.$menu.find('.active').attr('data-value'));
    > }
    >
    > if (!this.strings) text = val[this.options.property]
    > else text = val
    >
    > original_text = this.$element.val();
    > this.$element.val(text)
    >
    > if (typeof this.onselect == "function")
    > this.onselect(text, original_text)
    >
    71a90,108
    > , value
    >
    > this.query = this.$element.val()
    >
    > if (typeof this.source == "function") {
    > value = this.source(this, this.query)
    > if (value) this.process(value)
    > } else {
    > this.process(this.source)
    > }
    > }
    >
    > , process: function (results) {
    > var that = this
    > , items
    > , q
    >
    > if (results.length && typeof results[0] != "string")
    > this.strings = false
    79c116,118
    < items = $.grep(this.source, function (item) {
    ---
    > items = $.grep(results, function (item) {
    > if (!that.strings)
    > item = item[that.options.property]
    100a140
    > , sortby
    103,104c143,147
    < if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    < else if (~item.indexOf(this.query)) caseSensitive.push(item)
    ---
    > if (this.strings) sortby = item
    > else sortby = item[this.options.property]
    >
    > if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    > else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    121c164,166
    < i = $(that.options.item).attr('data-value', item)
    ---
    > i = $(that.options.item).attr('data-value', JSON.stringify(item))
    > if (!that.strings)
    > item = item[that.options.property]
    126c171,172
    < items.first().addClass('active')
    ---
    > if (that.autoselect) items.first().addClass('active')
    > if (that.autowidth) this.$menu.width(this.$element.width());
    168a215,217
    > e.stopPropagation()
    > e.preventDefault()
    >
    174d222
    < case 9: // tab
    179a228,234
    > case 9: // tab
    > var that = this
    > e.stopPropagation()
    > e.preventDefault()
    > setTimeout(function () { that.hide() }, 150)
    > break
    >
    181d235
    < if (!this.shown) return
    189,190d242
    < e.stopPropagation()
    < e.preventDefault()
    193a246
    > e.stopPropagation()
    213,214d265
    <
    < e.stopPropagation()
    218a270,271
    > e.stopPropagation()
    > e.preventDefault()
    253a307,310
    > , onselect: null
    > , autoselect: true
    > , autowidth: true
    > , property: 'value'
    --- bootstrap-typeahead.js.1 2012-03-12 14:13:12.000000000 -0700
    +++ bootstrap-typeahead.js 2012-03-13 12:05:09.000000000 -0700
    @@ -1,5 +1,5 @@
    /* =============================================================
    - * bootstrap-typeahead.js v2.0.2
    + * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 Twitter, Inc.
    @@ -29,6 +29,10 @@
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    + this.onselect = this.options.onselect
    + this.autoselect = this.options.autoselect
    + this.autowidth = this.options.autowidth
    + this.strings = true
    this.shown = false
    this.listen()
    }
    @@ -38,9 +42,23 @@
    constructor: Typeahead

    , select: function () {
    - var val = this.$menu.find('.active').attr('data-value')
    - this.$element.val(val)
    - this.$element.change();
    + var text, original_text;
    + if (this.$menu.find('.active').length == 0) {
    + var val = this.$element.val();
    + }
    + else {
    + var val = JSON.parse(this.$menu.find('.active').attr('data-value'));
    + }
    +
    + if (!this.strings) text = val[this.options.property]
    + else text = val
    +
    + original_text = this.$element.val();
    + this.$element.val(text)
    +
    + if (typeof this.onselect == "function")
    + this.onselect(text, original_text)
    +
    return this.hide()
    }

    @@ -69,6 +87,25 @@
    var that = this
    , items
    , q
    + , value
    +
    + this.query = this.$element.val()
    +
    + if (typeof this.source == "function") {
    + value = this.source(this, this.query)
    + if (value) this.process(value)
    + } else {
    + this.process(this.source)
    + }
    + }
    +
    + , process: function (results) {
    + var that = this
    + , items
    + , q
    +
    + if (results.length && typeof results[0] != "string")
    + this.strings = false

    this.query = this.$element.val()

    @@ -76,7 +113,9 @@
    return this.shown ? this.hide() : this
    }

    - items = $.grep(this.source, function (item) {
    + items = $.grep(results, function (item) {
    + if (!that.strings)
    + item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    @@ -98,10 +137,14 @@
    , caseSensitive = []
    , caseInsensitive = []
    , item
    + , sortby

    while (item = items.shift()) {
    - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    - else if (~item.indexOf(this.query)) caseSensitive.push(item)
    + if (this.strings) sortby = item
    + else sortby = item[this.options.property]
    +
    + if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    + else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    @@ -118,12 +161,15 @@
    var that = this

    items = $(items).map(function (i, item) {
    - i = $(that.options.item).attr('data-value', item)
    + i = $(that.options.item).attr('data-value', JSON.stringify(item))
    + if (!that.strings)
    + item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })

    - items.first().addClass('active')
    + if (that.autoselect) items.first().addClass('active')
    + if (that.autowidth) this.$menu.width(this.$element.width());
    this.$menu.html(items)
    return this
    }
    @@ -166,19 +212,27 @@
    }

    , keyup: function (e) {
    + e.stopPropagation()
    + e.preventDefault()
    +
    switch(e.keyCode) {
    case 40: // down arrow
    case 38: // up arrow
    break

    - case 9: // tab
    case 13: // enter
    if (!this.shown) return
    this.select()
    break

    + case 9: // tab
    + var that = this
    + e.stopPropagation()
    + e.preventDefault()
    + setTimeout(function () { that.hide() }, 150)
    + break
    +
    case 27: // escape
    - if (!this.shown) return
    this.hide()
    break

    @@ -186,11 +240,10 @@
    this.lookup()
    }

    - e.stopPropagation()
    - e.preventDefault()
    }

    , keypress: function (e) {
    + e.stopPropagation()
    if (!this.shown) return

    switch(e.keyCode) {
    @@ -210,12 +263,12 @@
    this.next()
    break
    }
    -
    - e.stopPropagation()
    }

    , blur: function (e) {
    var that = this
    + e.stopPropagation()
    + e.preventDefault()
    setTimeout(function () { that.hide() }, 150)
    }

    @@ -251,6 +304,10 @@
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    + , onselect: null
    + , autoselect: true
    + , autowidth: true
    + , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead
  2. Jason Rust revised this gist Mar 13, 2012. 3 changed files with 154 additions and 374 deletions.
    80 changes: 28 additions & 52 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,59 +1,35 @@
    # This is a fork of a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

    I've added a couple of lines to the original fork to stop the default behaviour when tabbing. For the proper source, please see [the original gist](https://gist.github.com/1866577).
    * Ability to disable autoselect of first matched element.
    * Ability to automatically set the width of the dropdown to that of the text input.
    * Ability to fetch source element via AJAX
    * Ability to have a comma separated list of tags.

    ### For example, process typeahead list asynchronously and return objects
    For the proper source, and other examples, please see [the original gist](https://gist.github.com/1866577).

    ```coffeescript
    # This example does an AJAX lookup and is in CoffeeScript
    $('.typeahead').typeahead(
    # source can be a function
    source: (typeahead, query) ->
    # this function receives the typeahead object and the query string
    $.ajax(
    url: "/lookup/?q="+query
    # i'm binding the function here using CoffeeScript syntactic sugar,
    # you can use for example Underscore's bind function instead.
    success: (data) =>
    # data must be a list of either strings or objects
    # data = [{'name': 'Joe', }, {'name': 'Henry'}, ...]
    typeahead.process(data)
    )
    # if we return objects to typeahead.process we must specify the property
    # that typeahead uses to look up the display value
    property: "name"
    )
    ```

    ### For example, process typeahead list synchronously and fire a callback on selection
    ### Example showing off all the above features

    ```javascript
    // This example is in Javascript, collects html in some li's and returns it
    $('.typeahead').typeahead({
    source: function (typeahead, query) {
    var return_list = []
    $("li").each(function(i,v){
    return_list.push($(v).html())
    })
    // here I'm just returning a list of strings
    return return_list
    },
    // typeahead calls this function when a object is selected, and
    // passes an object or string depending on what you processed, in this case a string
    onselect: function (obj) {
    alert('Selected '+obj)
    // This example is in Javascript, fetches a tag via AJAX and appends it to the comma separated list of tags
    $('#tags').typeahead({
    source: function(typeahead, query) {
    var term = $.trim(query.split(',').pop());
    if (term == '') return [];
    $.getJSON('/tags/typeahead.json' + '?', { term: term }, function(data) {
    typeahead.process(data);
    });
    }

    })
    ```

    ### and a very simple example, showing you can pass list of objects as source, and get that object via onselect
    ```javascript
    $('.typeahead').typeahead({
    // note that "value" is the default setting for the property option
    source: [{value: 'Charlie'}, {value: 'Gudbergur'}, ...],
    onselect: function(obj) { console.log(obj) }
    })
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.
    , onselect: function(item, previous_items) {
    terms = previous_items.split(',');
    terms.pop();
    terms.push(item);
    terms.push('');
    $.each(terms, function(idx, val) { terms[idx] = $.trim(val); });
    $('#tags').val(terms.join(', '));
    }
    // Matcher always returns true since there are multiple comma-seperated terms in the input box and the server ensures only matching terms are returned
    , matcher: function() { return true; }
    // Autoselect is disabled so that users can enter new tags
    , autoselect: false
    });
    ```
    19 changes: 15 additions & 4 deletions bootstrap-typeahead.js
    Original file line number Diff line number Diff line change
    @@ -30,6 +30,8 @@
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    this.onselect = this.options.onselect
    this.autoselect = this.options.autoselect
    this.autowidth = this.options.autowidth
    this.strings = true
    this.shown = false
    this.listen()
    @@ -40,16 +42,22 @@
    constructor: Typeahead

    , select: function () {
    var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    , text
    var text, original_text;
    if (this.$menu.find('.active').length == 0) {
    var val = this.$element.val();
    }
    else {
    var val = JSON.parse(this.$menu.find('.active').attr('data-value'));
    }

    if (!this.strings) text = val[this.options.property]
    else text = val

    original_text = this.$element.val();
    this.$element.val(text)

    if (typeof this.onselect == "function")
    this.onselect(val)
    this.onselect(text, original_text)

    return this.hide()
    }
    @@ -160,7 +168,8 @@
    return i[0]
    })

    items.first().addClass('active')
    if (that.autoselect) items.first().addClass('active')
    if (that.autowidth) this.$menu.width(this.$element.width());
    this.$menu.html(items)
    return this
    }
    @@ -296,6 +305,8 @@
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    , onselect: null
    , autoselect: true
    , autowidth: true
    , property: 'value'
    }

    429 changes: 111 additions & 318 deletions gistfile1.diff
    Original file line number Diff line number Diff line change
    @@ -1,318 +1,111 @@
    /* =============================================================
    * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 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.matcher = this.options.matcher || this.matcher
    this.sorter = this.options.sorter || this.sorter
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    this.onselect = this.options.onselect
    this.strings = true
    this.shown = false
    this.listen()
    }

    Typeahead.prototype = {

    constructor: Typeahead

    , select: function () {
    var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    , text

    if (!this.strings) text = val[this.options.property]
    else text = val

    this.$element.val(text)

    if (typeof this.onselect == "function")
    this.onselect(val)

    return this.hide()
    }

    , show: function () {
    var pos = $.extend({}, this.$element.offset(), {
    height: this.$element[0].offsetHeight
    })

    this.$menu.css({
    top: pos.top + pos.height
    , left: pos.left
    })

    this.$menu.show()
    this.shown = true
    return this
    }

    , hide: function () {
    this.$menu.hide()
    this.shown = false
    return this
    }

    , lookup: function (event) {
    var that = this
    , items
    , q
    , value

    this.query = this.$element.val()

    if (typeof this.source == "function") {
    value = this.source(this, this.query)
    if (value) this.process(value)
    } else {
    this.process(this.source)
    }
    }

    , process: function (results) {
    var that = this
    , items
    , q

    if (results.length && typeof results[0] != "string")
    this.strings = false

    this.query = this.$element.val()

    if (!this.query) {
    return this.shown ? this.hide() : this
    }

    items = $.grep(results, function (item) {
    if (!that.strings)
    item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    items = this.sorter(items)

    if (!items.length) {
    return this.shown ? this.hide() : this
    }

    return this.render(items.slice(0, this.options.items)).show()
    }

    , matcher: function (item) {
    return ~item.toLowerCase().indexOf(this.query.toLowerCase())
    }

    , sorter: function (items) {
    var beginswith = []
    , caseSensitive = []
    , caseInsensitive = []
    , item
    , sortby

    while (item = items.shift()) {
    if (this.strings) sortby = item
    else sortby = item[this.options.property]

    if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    return beginswith.concat(caseSensitive, caseInsensitive)
    }

    , highlighter: function (item) {
    return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
    return '<strong>' + match + '</strong>'
    })
    }

    , render: function (items) {
    var that = this

    items = $(items).map(function (i, item) {
    i = $(that.options.item).attr('data-value', JSON.stringify(item))
    if (!that.strings)
    item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })

    items.first().addClass('active')
    this.$menu.html(items)
    return this
    }

    , next: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , next = active.next()

    if (!next.length) {
    next = $(this.$menu.find('li')[0])
    }

    next.addClass('active')
    }

    , prev: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , prev = active.prev()

    if (!prev.length) {
    prev = this.$menu.find('li').last()
    }

    prev.addClass('active')
    }

    , listen: function () {
    this.$element
    .on('blur', $.proxy(this.blur, this))
    .on('keypress', $.proxy(this.keypress, this))
    .on('keyup', $.proxy(this.keyup, this))

    if ($.browser.webkit || $.browser.msie) {
    this.$element.on('keydown', $.proxy(this.keypress, this))
    }

    this.$menu
    .on('click', $.proxy(this.click, this))
    .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
    }

    , keyup: function (e) {
    e.stopPropagation()
    e.preventDefault()

    switch(e.keyCode) {
    case 40: // down arrow
    case 38: // up arrow
    break

    - case 9: // tab
    case 13: // enter
    if (!this.shown) return
    this.select()
    break

    + case 9: // tab
    + var that = this
    + e.stopPropagation()
    + e.preventDefault()
    + setTimeout(function () { that.hide() }, 150)
    + break
    +
    case 27: // escape
    this.hide()
    break

    default:
    this.lookup()
    }

    }

    , keypress: function (e) {
    e.stopPropagation()
    if (!this.shown) return

    switch(e.keyCode) {
    case 9: // tab
    case 13: // enter
    case 27: // escape
    e.preventDefault()
    break

    case 38: // up arrow
    e.preventDefault()
    this.prev()
    break

    case 40: // down arrow
    e.preventDefault()
    this.next()
    break
    }
    }

    , blur: function (e) {
    var that = this
    e.stopPropagation()
    e.preventDefault()
    setTimeout(function () { that.hide() }, 150)
    }

    , click: function (e) {
    e.stopPropagation()
    e.preventDefault()
    this.select()
    }

    , mouseenter: function (e) {
    this.$menu.find('.active').removeClass('active')
    $(e.currentTarget).addClass('active')
    }

    }


    /* 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 = {
    source: []
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    , onselect: null
    , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead


    /* TYPEAHEAD DATA-API
    * ================== */

    $(function () {
    $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
    var $this = $(this)
    if ($this.data('typeahead')) return
    e.preventDefault()
    $this.typeahead($this.data())
    })
    })

    }( window.jQuery );
    2c2
    < * bootstrap-typeahead.js v2.0.2
    ---
    > * bootstrap-typeahead.js v2.0.0
    31a32,35
    > this.onselect = this.options.onselect
    > this.autoselect = this.options.autoselect
    > this.autowidth = this.options.autowidth
    > this.strings = true
    41,43c45,61
    < var val = this.$menu.find('.active').attr('data-value')
    < this.$element.val(val)
    < this.$element.change();
    ---
    > var text, original_text;
    > if (this.$menu.find('.active').length == 0) {
    > var val = this.$element.val();
    > }
    > else {
    > var val = JSON.parse(this.$menu.find('.active').attr('data-value'));
    > }
    >
    > if (!this.strings) text = val[this.options.property]
    > else text = val
    >
    > original_text = this.$element.val();
    > this.$element.val(text)
    >
    > if (typeof this.onselect == "function")
    > this.onselect(text, original_text)
    >
    71a90,108
    > , value
    >
    > this.query = this.$element.val()
    >
    > if (typeof this.source == "function") {
    > value = this.source(this, this.query)
    > if (value) this.process(value)
    > } else {
    > this.process(this.source)
    > }
    > }
    >
    > , process: function (results) {
    > var that = this
    > , items
    > , q
    >
    > if (results.length && typeof results[0] != "string")
    > this.strings = false
    79c116,118
    < items = $.grep(this.source, function (item) {
    ---
    > items = $.grep(results, function (item) {
    > if (!that.strings)
    > item = item[that.options.property]
    100a140
    > , sortby
    103,104c143,147
    < if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    < else if (~item.indexOf(this.query)) caseSensitive.push(item)
    ---
    > if (this.strings) sortby = item
    > else sortby = item[this.options.property]
    >
    > if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    > else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    121c164,166
    < i = $(that.options.item).attr('data-value', item)
    ---
    > i = $(that.options.item).attr('data-value', JSON.stringify(item))
    > if (!that.strings)
    > item = item[that.options.property]
    126c171,172
    < items.first().addClass('active')
    ---
    > if (that.autoselect) items.first().addClass('active')
    > if (that.autowidth) this.$menu.width(this.$element.width());
    168a215,217
    > e.stopPropagation()
    > e.preventDefault()
    >
    174d222
    < case 9: // tab
    179a228,234
    > case 9: // tab
    > var that = this
    > e.stopPropagation()
    > e.preventDefault()
    > setTimeout(function () { that.hide() }, 150)
    > break
    >
    181d235
    < if (!this.shown) return
    189,190d242
    < e.stopPropagation()
    < e.preventDefault()
    193a246
    > e.stopPropagation()
    213,214d265
    <
    < e.stopPropagation()
    218a270,271
    > e.stopPropagation()
    > e.preventDefault()
    253a307,310
    > , onselect: null
    > , autoselect: true
    > , autowidth: true
    > , property: 'value'
  3. @pezholio pezholio revised this gist Mar 5, 2012. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,6 @@
    # This is a fork of a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

    I've added a couple of lines to the original fork to stop the default behaviour when tabbing. For the proper source, please see [the original gist][1].

    [1] https://gist.github.com/1866577
    I've added a couple of lines to the original fork to stop the default behaviour when tabbing. For the proper source, please see [the original gist](https://gist.github.com/1866577).

    ### For example, process typeahead list asynchronously and return objects

  4. @pezholio pezholio revised this gist Mar 5, 2012. 2 changed files with 323 additions and 113 deletions.
    12 changes: 6 additions & 6 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,8 @@
    # This is a fork of Bootstrap Typeahead that adds minimal but powerful extensions.
    # This is a fork of a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

    I've added a couple of lines to the original fork to stop the default behaviour when tabbing. For the proper source, please see [the original gist][1].

    [1] https://gist.github.com/1866577

    ### For example, process typeahead list asynchronously and return objects

    @@ -54,8 +58,4 @@
    })
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    **Update 02/23/2012: Fixed a bug**

    ### Gudbergur Erlendsson, reach me here or gudbergur at gmail
    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.
    424 changes: 317 additions & 107 deletions gistfile1.diff
    Original file line number Diff line number Diff line change
    @@ -1,108 +1,318 @@
    /* =============================================================
    * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 Twitter, Inc.
    @@ -29,6 +29,8 @@
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    + this.onselect = this.options.onselect
    + this.strings = true
    this.shown = false
    this.listen()
    }
    @@ -38,8 +40,17 @@
    constructor: Typeahead

    , select: function () {
    - var val = this.$menu.find('.active').attr('data-value')
    - this.$element.val(val)
    + var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    + , text
    /* =============================================================
    * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 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.matcher = this.options.matcher || this.matcher
    this.sorter = this.options.sorter || this.sorter
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    this.onselect = this.options.onselect
    this.strings = true
    this.shown = false
    this.listen()
    }

    Typeahead.prototype = {

    constructor: Typeahead

    , select: function () {
    var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    , text

    if (!this.strings) text = val[this.options.property]
    else text = val

    this.$element.val(text)

    if (typeof this.onselect == "function")
    this.onselect(val)

    return this.hide()
    }

    , show: function () {
    var pos = $.extend({}, this.$element.offset(), {
    height: this.$element[0].offsetHeight
    })

    this.$menu.css({
    top: pos.top + pos.height
    , left: pos.left
    })

    this.$menu.show()
    this.shown = true
    return this
    }

    , hide: function () {
    this.$menu.hide()
    this.shown = false
    return this
    }

    , lookup: function (event) {
    var that = this
    , items
    , q
    , value

    this.query = this.$element.val()

    if (typeof this.source == "function") {
    value = this.source(this, this.query)
    if (value) this.process(value)
    } else {
    this.process(this.source)
    }
    }

    , process: function (results) {
    var that = this
    , items
    , q

    if (results.length && typeof results[0] != "string")
    this.strings = false

    this.query = this.$element.val()

    if (!this.query) {
    return this.shown ? this.hide() : this
    }

    items = $.grep(results, function (item) {
    if (!that.strings)
    item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    items = this.sorter(items)

    if (!items.length) {
    return this.shown ? this.hide() : this
    }

    return this.render(items.slice(0, this.options.items)).show()
    }

    , matcher: function (item) {
    return ~item.toLowerCase().indexOf(this.query.toLowerCase())
    }

    , sorter: function (items) {
    var beginswith = []
    , caseSensitive = []
    , caseInsensitive = []
    , item
    , sortby

    while (item = items.shift()) {
    if (this.strings) sortby = item
    else sortby = item[this.options.property]

    if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    return beginswith.concat(caseSensitive, caseInsensitive)
    }

    , highlighter: function (item) {
    return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
    return '<strong>' + match + '</strong>'
    })
    }

    , render: function (items) {
    var that = this

    items = $(items).map(function (i, item) {
    i = $(that.options.item).attr('data-value', JSON.stringify(item))
    if (!that.strings)
    item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })

    items.first().addClass('active')
    this.$menu.html(items)
    return this
    }

    , next: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , next = active.next()

    if (!next.length) {
    next = $(this.$menu.find('li')[0])
    }

    next.addClass('active')
    }

    , prev: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , prev = active.prev()

    if (!prev.length) {
    prev = this.$menu.find('li').last()
    }

    prev.addClass('active')
    }

    , listen: function () {
    this.$element
    .on('blur', $.proxy(this.blur, this))
    .on('keypress', $.proxy(this.keypress, this))
    .on('keyup', $.proxy(this.keyup, this))

    if ($.browser.webkit || $.browser.msie) {
    this.$element.on('keydown', $.proxy(this.keypress, this))
    }

    this.$menu
    .on('click', $.proxy(this.click, this))
    .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
    }

    , keyup: function (e) {
    e.stopPropagation()
    e.preventDefault()

    switch(e.keyCode) {
    case 40: // down arrow
    case 38: // up arrow
    break

    - case 9: // tab
    case 13: // enter
    if (!this.shown) return
    this.select()
    break

    + case 9: // tab
    + var that = this
    + e.stopPropagation()
    + e.preventDefault()
    + setTimeout(function () { that.hide() }, 150)
    + break
    +
    + if (!this.strings) text = val[this.options.property]
    + else text = val
    +
    + this.$element.val(text)
    +
    + if (typeof this.onselect == "function")
    + this.onselect(val)
    +
    return this.hide()
    }

    @@ -68,6 +79,25 @@
    var that = this
    , items
    , q
    + , value
    +
    + this.query = this.$element.val()
    +
    + if (typeof this.source == "function")
    + value = this.source(this, this.query)
    + if (value)
    + this.process(value)
    + else
    + this.process(this.source)
    + }
    +
    + , process: function (results) {
    + var that = this
    + , items
    + , q
    +
    + if (results.length && typeof results[0] != "string")
    + this.strings = false

    this.query = this.$element.val()

    @@ -75,7 +105,9 @@
    return this.shown ? this.hide() : this
    }

    - items = $.grep(this.source, function (item) {
    + items = $.grep(results, function (item) {
    + if (!that.strings)
    + item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    @@ -97,10 +129,14 @@
    , caseSensitive = []
    , caseInsensitive = []
    , item
    + , sortby

    while (item = items.shift()) {
    - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    - else if (~item.indexOf(this.query)) caseSensitive.push(item)
    + if (this.strings) sortby = item
    + else sortby = item[this.options.property]
    +
    + if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    + else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    @@ -117,7 +153,9 @@
    var that = this

    items = $(items).map(function (i, item) {
    - i = $(that.options.item).attr('data-value', item)
    + i = $(that.options.item).attr('data-value', JSON.stringify(item))
    + if (!that.strings)
    + item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })
    @@ -251,6 +289,8 @@
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    + , onselect: null
    + , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead
    case 27: // escape
    this.hide()
    break

    default:
    this.lookup()
    }

    }

    , keypress: function (e) {
    e.stopPropagation()
    if (!this.shown) return

    switch(e.keyCode) {
    case 9: // tab
    case 13: // enter
    case 27: // escape
    e.preventDefault()
    break

    case 38: // up arrow
    e.preventDefault()
    this.prev()
    break

    case 40: // down arrow
    e.preventDefault()
    this.next()
    break
    }
    }

    , blur: function (e) {
    var that = this
    e.stopPropagation()
    e.preventDefault()
    setTimeout(function () { that.hide() }, 150)
    }

    , click: function (e) {
    e.stopPropagation()
    e.preventDefault()
    this.select()
    }

    , mouseenter: function (e) {
    this.$menu.find('.active').removeClass('active')
    $(e.currentTarget).addClass('active')
    }

    }


    /* 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 = {
    source: []
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    , onselect: null
    , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead


    /* TYPEAHEAD DATA-API
    * ================== */

    $(function () {
    $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
    var $this = $(this)
    if ($this.data('typeahead')) return
    e.preventDefault()
    $this.typeahead($this.data())
    })
    })

    }( window.jQuery );
  5. @pezholio pezholio revised this gist Mar 5, 2012. 1 changed file with 8 additions and 2 deletions.
    10 changes: 8 additions & 2 deletions bootstrap-typeahead.js
    Original file line number Diff line number Diff line change
    @@ -211,12 +211,18 @@
    case 38: // up arrow
    break

    case 9: // tab
    case 13: // enter
    if (!this.shown) return
    this.select()
    break

    case 9: // tab
    var that = this
    e.stopPropagation()
    e.preventDefault()
    setTimeout(function () { that.hide() }, 150)
    break

    case 27: // escape
    this.hide()
    break
    @@ -308,4 +314,4 @@
    })
    })

    }( window.jQuery );
    }( window.jQuery );
  6. @gudber gudber revised this gist Feb 23, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # This is an extension to Bootstrap Typeahead that adds minimal but powerful extensions.
    # This is a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

    ### For example, process typeahead list asynchronously and return objects

  7. @gudber gudber revised this gist Feb 23, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -56,6 +56,6 @@

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    Update 02/23/2012: Fixed a bug
    **Update 02/23/2012: Fixed a bug**

    ### Gudbergur Erlendsson, reach me here or gudbergur at gmail
  8. @gudber gudber revised this gist Feb 23, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -56,6 +56,6 @@

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    02/23/2012: Fixed a bug
    Update 02/23/2012: Fixed a bug

    ### Gudbergur Erlendsson, reach me here or gudbergur at gmail
  9. @gudber gudber revised this gist Feb 23, 2012. 2 changed files with 6 additions and 4 deletions.
    2 changes: 2 additions & 0 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -56,4 +56,6 @@

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    02/23/2012: Fixed a bug

    ### Gudbergur Erlendsson, reach me here or gudbergur at gmail
    8 changes: 4 additions & 4 deletions bootstrap-typeahead.js
    Original file line number Diff line number Diff line change
    @@ -83,12 +83,12 @@

    this.query = this.$element.val()

    if (typeof this.source == "function")
    if (typeof this.source == "function") {
    value = this.source(this, this.query)
    if (value)
    this.process(value)
    else
    if (value) this.process(value)
    } else {
    this.process(this.source)
    }
    }

    , process: function (results) {
  10. @gudber gudber revised this gist Feb 20, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -56,4 +56,4 @@

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    - Gudbergur Erlendsson
    ### Gudbergur Erlendsson, reach me here or gudbergur at gmail
  11. @gudber gudber revised this gist Feb 20, 2012. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -55,3 +55,5 @@
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

    - Gudbergur Erlendsson
  12. @gudber gudber revised this gist Feb 20, 2012. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # This is an extension to Bootstrap Typeahead that adds minimal but powerful extensions.

    ### For example, process typeahead list asynchronously, return objects and then fire a callback when one is selected
    ### For example, process typeahead list asynchronously and return objects

    ```coffeescript
    # This example does an AJAX lookup and is in CoffeeScript
    @@ -23,7 +23,7 @@
    )
    ```

    ### For example, process typeahead list synchronously and call a function on selection
    ### For example, process typeahead list synchronously and fire a callback on selection

    ```javascript
    // This example is in Javascript, collects html in some li's and returns it
  13. @gudber gudber revised this gist Feb 20, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -54,4 +54,4 @@
    })
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I think these additions are very helpful.
    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.
  14. @gudber gudber revised this gist Feb 20, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.markdown
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # This is an extension to Bootstrap Typeahead that adds three minimal but powerful extensions.
    # This is an extension to Bootstrap Typeahead that adds minimal but powerful extensions.

    ### For example, process typeahead list asynchronously, return objects and then fire a callback when one is selected

  15. @gudber gudber revised this gist Feb 19, 2012. 1 changed file with 9 additions and 0 deletions.
    9 changes: 9 additions & 0 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -45,4 +45,13 @@
    })
    ```

    ### and a very simple example, showing you can pass list of objects as source, and get that object via onselect
    ```javascript
    $('.typeahead').typeahead({
    // note that "value" is the default setting for the property option
    source: [{value: 'Charlie'}, {value: 'Gudbergur'}, ...],
    onselect: function(obj) { console.log(obj) }
    })
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I think these additions are very helpful.
  16. @gudber gudber created this gist Feb 19, 2012.
    48 changes: 48 additions & 0 deletions README.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    # This is an extension to Bootstrap Typeahead that adds three minimal but powerful extensions.

    ### For example, process typeahead list asynchronously, return objects and then fire a callback when one is selected

    ```coffeescript
    # This example does an AJAX lookup and is in CoffeeScript
    $('.typeahead').typeahead(
    # source can be a function
    source: (typeahead, query) ->
    # this function receives the typeahead object and the query string
    $.ajax(
    url: "/lookup/?q="+query
    # i'm binding the function here using CoffeeScript syntactic sugar,
    # you can use for example Underscore's bind function instead.
    success: (data) =>
    # data must be a list of either strings or objects
    # data = [{'name': 'Joe', }, {'name': 'Henry'}, ...]
    typeahead.process(data)
    )
    # if we return objects to typeahead.process we must specify the property
    # that typeahead uses to look up the display value
    property: "name"
    )
    ```

    ### For example, process typeahead list synchronously and call a function on selection

    ```javascript
    // This example is in Javascript, collects html in some li's and returns it
    $('.typeahead').typeahead({
    source: function (typeahead, query) {
    var return_list = []
    $("li").each(function(i,v){
    return_list.push($(v).html())
    })
    // here I'm just returning a list of strings
    return return_list
    },
    // typeahead calls this function when a object is selected, and
    // passes an object or string depending on what you processed, in this case a string
    onselect: function (obj) {
    alert('Selected '+obj)
    }

    })
    ```

    Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I think these additions are very helpful.
    311 changes: 311 additions & 0 deletions bootstrap-typeahead.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,311 @@
    /* =============================================================
    * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 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.matcher = this.options.matcher || this.matcher
    this.sorter = this.options.sorter || this.sorter
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    this.onselect = this.options.onselect
    this.strings = true
    this.shown = false
    this.listen()
    }

    Typeahead.prototype = {

    constructor: Typeahead

    , select: function () {
    var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    , text

    if (!this.strings) text = val[this.options.property]
    else text = val

    this.$element.val(text)

    if (typeof this.onselect == "function")
    this.onselect(val)

    return this.hide()
    }

    , show: function () {
    var pos = $.extend({}, this.$element.offset(), {
    height: this.$element[0].offsetHeight
    })

    this.$menu.css({
    top: pos.top + pos.height
    , left: pos.left
    })

    this.$menu.show()
    this.shown = true
    return this
    }

    , hide: function () {
    this.$menu.hide()
    this.shown = false
    return this
    }

    , lookup: function (event) {
    var that = this
    , items
    , q
    , value

    this.query = this.$element.val()

    if (typeof this.source == "function")
    value = this.source(this, this.query)
    if (value)
    this.process(value)
    else
    this.process(this.source)
    }

    , process: function (results) {
    var that = this
    , items
    , q

    if (results.length && typeof results[0] != "string")
    this.strings = false

    this.query = this.$element.val()

    if (!this.query) {
    return this.shown ? this.hide() : this
    }

    items = $.grep(results, function (item) {
    if (!that.strings)
    item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    items = this.sorter(items)

    if (!items.length) {
    return this.shown ? this.hide() : this
    }

    return this.render(items.slice(0, this.options.items)).show()
    }

    , matcher: function (item) {
    return ~item.toLowerCase().indexOf(this.query.toLowerCase())
    }

    , sorter: function (items) {
    var beginswith = []
    , caseSensitive = []
    , caseInsensitive = []
    , item
    , sortby

    while (item = items.shift()) {
    if (this.strings) sortby = item
    else sortby = item[this.options.property]

    if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    return beginswith.concat(caseSensitive, caseInsensitive)
    }

    , highlighter: function (item) {
    return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
    return '<strong>' + match + '</strong>'
    })
    }

    , render: function (items) {
    var that = this

    items = $(items).map(function (i, item) {
    i = $(that.options.item).attr('data-value', JSON.stringify(item))
    if (!that.strings)
    item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })

    items.first().addClass('active')
    this.$menu.html(items)
    return this
    }

    , next: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , next = active.next()

    if (!next.length) {
    next = $(this.$menu.find('li')[0])
    }

    next.addClass('active')
    }

    , prev: function (event) {
    var active = this.$menu.find('.active').removeClass('active')
    , prev = active.prev()

    if (!prev.length) {
    prev = this.$menu.find('li').last()
    }

    prev.addClass('active')
    }

    , listen: function () {
    this.$element
    .on('blur', $.proxy(this.blur, this))
    .on('keypress', $.proxy(this.keypress, this))
    .on('keyup', $.proxy(this.keyup, this))

    if ($.browser.webkit || $.browser.msie) {
    this.$element.on('keydown', $.proxy(this.keypress, this))
    }

    this.$menu
    .on('click', $.proxy(this.click, this))
    .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
    }

    , keyup: function (e) {
    e.stopPropagation()
    e.preventDefault()

    switch(e.keyCode) {
    case 40: // down arrow
    case 38: // up arrow
    break

    case 9: // tab
    case 13: // enter
    if (!this.shown) return
    this.select()
    break

    case 27: // escape
    this.hide()
    break

    default:
    this.lookup()
    }

    }

    , keypress: function (e) {
    e.stopPropagation()
    if (!this.shown) return

    switch(e.keyCode) {
    case 9: // tab
    case 13: // enter
    case 27: // escape
    e.preventDefault()
    break

    case 38: // up arrow
    e.preventDefault()
    this.prev()
    break

    case 40: // down arrow
    e.preventDefault()
    this.next()
    break
    }
    }

    , blur: function (e) {
    var that = this
    e.stopPropagation()
    e.preventDefault()
    setTimeout(function () { that.hide() }, 150)
    }

    , click: function (e) {
    e.stopPropagation()
    e.preventDefault()
    this.select()
    }

    , mouseenter: function (e) {
    this.$menu.find('.active').removeClass('active')
    $(e.currentTarget).addClass('active')
    }

    }


    /* 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 = {
    source: []
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    , onselect: null
    , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead


    /* TYPEAHEAD DATA-API
    * ================== */

    $(function () {
    $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
    var $this = $(this)
    if ($this.data('typeahead')) return
    e.preventDefault()
    $this.typeahead($this.data())
    })
    })

    }( window.jQuery );
    108 changes: 108 additions & 0 deletions gistfile1.diff
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,108 @@
    /* =============================================================
    * bootstrap-typeahead.js v2.0.0
    * http://twitter.github.com/bootstrap/javascript.html#typeahead
    * =============================================================
    * Copyright 2012 Twitter, Inc.
    @@ -29,6 +29,8 @@
    this.highlighter = this.options.highlighter || this.highlighter
    this.$menu = $(this.options.menu).appendTo('body')
    this.source = this.options.source
    + this.onselect = this.options.onselect
    + this.strings = true
    this.shown = false
    this.listen()
    }
    @@ -38,8 +40,17 @@
    constructor: Typeahead

    , select: function () {
    - var val = this.$menu.find('.active').attr('data-value')
    - this.$element.val(val)
    + var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
    + , text
    +
    + if (!this.strings) text = val[this.options.property]
    + else text = val
    +
    + this.$element.val(text)
    +
    + if (typeof this.onselect == "function")
    + this.onselect(val)
    +
    return this.hide()
    }

    @@ -68,6 +79,25 @@
    var that = this
    , items
    , q
    + , value
    +
    + this.query = this.$element.val()
    +
    + if (typeof this.source == "function")
    + value = this.source(this, this.query)
    + if (value)
    + this.process(value)
    + else
    + this.process(this.source)
    + }
    +
    + , process: function (results) {
    + var that = this
    + , items
    + , q
    +
    + if (results.length && typeof results[0] != "string")
    + this.strings = false

    this.query = this.$element.val()

    @@ -75,7 +105,9 @@
    return this.shown ? this.hide() : this
    }

    - items = $.grep(this.source, function (item) {
    + items = $.grep(results, function (item) {
    + if (!that.strings)
    + item = item[that.options.property]
    if (that.matcher(item)) return item
    })

    @@ -97,10 +129,14 @@
    , caseSensitive = []
    , caseInsensitive = []
    , item
    + , sortby

    while (item = items.shift()) {
    - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    - else if (~item.indexOf(this.query)) caseSensitive.push(item)
    + if (this.strings) sortby = item
    + else sortby = item[this.options.property]
    +
    + if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
    + else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
    else caseInsensitive.push(item)
    }

    @@ -117,7 +153,9 @@
    var that = this

    items = $(items).map(function (i, item) {
    - i = $(that.options.item).attr('data-value', item)
    + i = $(that.options.item).attr('data-value', JSON.stringify(item))
    + if (!that.strings)
    + item = item[that.options.property]
    i.find('a').html(that.highlighter(item))
    return i[0]
    })
    @@ -251,6 +289,8 @@
    , items: 8
    , menu: '<ul class="typeahead dropdown-menu"></ul>'
    , item: '<li><a href="#"></a></li>'
    + , onselect: null
    + , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead