Skip to content

Instantly share code, notes, and snippets.

@gudbergur
Created February 19, 2012 23:49
Show Gist options
  • Save gudbergur/1866577 to your computer and use it in GitHub Desktop.
Save gudbergur/1866577 to your computer and use it in GitHub Desktop.
Bootstrap's Typeahead plugin extended (allowing for AJAX functionality) among other things

This is a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

For example, process typeahead list asynchronously and return objects

  # 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

  // 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)
    }

  })

and a very simple example, showing you can pass list of objects as source, and get that object via onselect

  $('.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.

Update 02/23/2012: Fixed a bug

Gudbergur Erlendsson, reach me here or gudbergur at gmail

/* =============================================================
* 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 );
/* =============================================================
* 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
@xlozinguez
Copy link

Not sure if anyone has encountered this problem but I've followed @trippo example and everything works fine... except that my options didn't show up so I used Chrome's js debugger and noticed that the process method was invoked twice: one time via the success callback and the second time via the lookup function wich seems to be the default to the keyup event...

The problem with this is that the first one has the response items, the other has an empty array...

Noone ever had this issue?

@xlozinguez
Copy link

Only way to prevent that is to comment out the line as such:

, 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) <-- this will trigger process twice...
      } else {
        this.process(this.source)
      }
    }

@slacktracer
Copy link

I'm being really lazy here, but... Any plans for a new version? ( I mean, forking the new typeahead, 2.1)

@shellac
Copy link

shellac commented Sep 25, 2012

That's a bug due to lax syntax, I think. The 'else' is associated with the inner 'if (value)'. Try adding a semicolon after 'this.process(value)'?

@benjsno
Copy link

benjsno commented Oct 22, 2012

I had some trouble on Chrome. When clicking on menu, the input wasn't fill.
I have modified line 257 :

setTimeout(function () { that.hide() }, 150)

in

setTimeout(function () { that.hide() }, 300)

thanks
(Chrome V 22.0.1229.94, Mac OS X 10.8.2)

@jonschlinkert
Copy link

This is great, @gudberger have you considered making this an actual repo, so issues can be logged and uses can fork and do pull requests? Seems like it would get some traction

@Morpho
Copy link

Morpho commented Nov 8, 2012

I'd love to see this in Bootstrap 2.1!

@noreiller
Copy link

To those who get the error message "Uncaught TypeError: Cannot call method 'toLowerCase'", I resolved this by sending to the process callback a simple array with the values instead of the full JSON.
So instead of

[{'name': 'Joe', }, {'name': 'Henry'}, ...]

You have to send

['Joe', 'Henry', ...]

@ikkentim
Copy link

I get an error on line 147 when using the character '['. :
Invalid regular expression: missing terminating ] for character class

(due to the regular expression) ;(

@northern
Copy link

Not sure if this helps anyone but for v2.2.2 the asynchronous data retrieval seems to be built-in without the need of a forked ajax version:

http://www.lukeschreur.com/posts/bootstrap-asynchronous-auto-complete-input-fields

@dhenze
Copy link

dhenze commented Jan 10, 2013

I've updated your code to use it with Boostrap v2.2.2. https://gist.github.com/4502186

Didn't check for any improvements, does work fine for me. Very much appreciated, this is a great enhancement. Very much appreciated.

@alexdma
Copy link

alexdma commented Feb 8, 2013

Loved this one, though I only got it to work on Firefox.

but under IE9, Opera and Chrome, it does not seem to capture scroll key presses for scrolling across dropdown items.

Also, in no browser was it accepting selection by mouse click, unlike the plain non-Ajax typeahead.

I might have done something wrong though. The only other script running on the page is jquery-latest.

Does anyone have a clue? I can provide whatever code sample I used for this plugin.

@alexdma
Copy link

alexdma commented Feb 8, 2013

Ok I'm beginning to figure out - it's the use of the deprecated $.browser for detecting keydown event support...

@mrgcohen
Copy link

mrgcohen commented Mar 1, 2013

https://gist.github.com/mrgcohen/5062352

Here's an additional example where it does not search for all entries when a user types fast. I needed something for a project but don't have time to fork. Hope someone finds this useful. Also it will cache searches so you won't be making a ton of calls (easy to disable if you choose).

So if you search for

Supercalifragilisticexpialidocious

and you are a fast typer and type superca really fast instead of making ajax calls for..

s
su
sup
super
superca

It will just do

superca

https://gist.github.com/mrgcohen/5062352

cheers 😃

@dnprock
Copy link

dnprock commented Mar 7, 2013

I just attempted to integrate this again and notice the gist is broken with jquery 1.9. $.browser.webkit call was removed.

Quick solution is to use jquery migrate:

http://stackoverflow.com/questions/9638247/is-jquery-browser-deprecated

It's useful to update the gist.

@donriga
Copy link

donriga commented Jan 10, 2014

Great fork!!! I try to autocomplete two days to do, but only solved the problem of installing a fork

@rtselva03
Copy link

thanks dude, and i want one more thing from this, to avoid free type entry, user can select from suggestion list only, someone help me for this....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment