Skip to content

Instantly share code, notes, and snippets.

@dhenze
Created January 10, 2013 13:55

Revisions

  1. dhenze created this gist Jan 10, 2013.
    343 changes: 343 additions & 0 deletions bootstrap-typeahead.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,343 @@
    /* =============================================================
    * bootstrap-typeahead.js v2.2.2
    * 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"; // jshint ;_;


    /* TYPEAHEAD PUBLIC CLASS DEFINITION
    * ================================= */

    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.updater = this.options.updater || this.updater
    this.source = this.options.source
    this.$menu = $(this.options.menu)
    this.onselect = this.options.onselect
    this.string = true
    this.shown = false
    this.listen()
    }

    Typeahead.prototype = {

    constructor: Typeahead

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

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

    this.$element
    .val(this.updater(text))
    .change()

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

    return this.hide()
    }

    , updater: function (item) {
    return item
    }

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

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

    this.shown = true
    return this
    }

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

    , lookup: function (event) {
    var items

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

    if (!this.query || this.query.length < this.options.minLength) {
    return this.shown ? this.hide() : this
    }

    items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source

    return items ? this.process(items) : this
    }

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

    items = $.grep(items, function (item) {
    if (!that.strings)
    item = item[that.options.property]
    return that.matcher(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) {
    var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
    return item.replace(new RegExp('(' + 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 (this.eventSupported('keydown')) {
    this.$element.on('keydown', $.proxy(this.keydown, this))
    }

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

    , eventSupported: function(eventName) {
    var isSupported = eventName in this.$element
    if (!isSupported) {
    this.$element.setAttribute(eventName, 'return;')
    isSupported = typeof this.$element[eventName] === 'function'
    }
    return isSupported
    }

    , move: function (e) {
    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
    }

    e.stopPropagation()
    }

    , keydown: function (e) {
    this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
    this.move(e)
    }

    , keypress: function (e) {
    if (this.suppressKeyPressRepeat) return
    this.move(e)
    }

    , keyup: function (e) {
    switch(e.keyCode) {
    case 40: // down arrow
    case 38: // up arrow
    case 16: // shift
    case 17: // ctrl
    case 18: // alt
    break

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

    case 27: // escape
    if (!this.shown) return
    this.hide()
    break

    default:
    this.lookup()
    }

    e.stopPropagation()
    e.preventDefault()
    }

    , blur: function (e) {
    var that = this
    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
    * =========================== */

    var old = $.fn.typeahead

    $.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>'
    , minLength: 1
    , onselect: null
    , property: 'value'
    }

    $.fn.typeahead.Constructor = Typeahead


    /* TYPEAHEAD NO CONFLICT
    * =================== */

    $.fn.typeahead.noConflict = function () {
    $.fn.typeahead = old
    return this
    }


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

    $(document).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);