Skip to content

Instantly share code, notes, and snippets.

@Yavari
Forked from gudbergur/README.markdown
Created February 23, 2012 09:00
Show Gist options
  • Star 70 You must be signed in to star a gist
  • Fork 42 You must be signed in to fork a gist
  • Save Yavari/1891669 to your computer and use it in GitHub Desktop.
Save Yavari/1891669 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
@vanhalt
Copy link

vanhalt commented Feb 27, 2012

Outstanding... works like a charm!

This should be merged on Bootstrap. Thanks for this feature!

@foxtacles
Copy link

Thanks a lot!

@pezholio
Copy link

pezholio commented Mar 1, 2012

Brilliant, thanks!

@pezholio
Copy link

pezholio commented Mar 1, 2012

Is there any way I can stop the first item from being automatically selected? At the moment the onselect event is fired when a user tabs out of the typeahead element.

@durus
Copy link

durus commented Mar 2, 2012

The code is written by gudbergur and you can find it at https://gist.github.com/1866577 . I forked it in order to have a copy to work on if I need to do any changes.

@pezholio
Copy link

pezholio commented Mar 2, 2012

Oops! Sorry! Will post it on the original Gist.

@cahofmeyr
Copy link

I suggest adding a typing inactivity timeout, otherwise if an ajax request is used, it is fired on each keystroke and the search results box flashes as each ajax request completes.

Added to the 'default' case in the keyup function:

          if (this.inactivityTimer)
            window.clearTimeout(this.inactivityTimer);
          var that = this;
          this.inactivityTimer = window.setTimeout(function () {
            that.lookup();
          }, 500);

@cahofmeyr
Copy link

Oops, looks like I made the same mistake as pezholio :-)

I noticed that the typing inactivity timer was already suggested on the original gudberger gist.

@oceanBT
Copy link

oceanBT commented Aug 6, 2012

very usefull, if there is an ID field that belongs to the text of the typeahead field. ( like the value of select/dropdown option )

http://therosiek.com/2012/04/bootstrap-typeahead-with-onselect/comment-page-1/

@kneidels
Copy link

hey folks

looks very good.
using bootstrap 2.0.3
i am getting an error: "results is null" in bootstrap.js line 1639

which points to this line:

  return ~item.toLowerCase().indexOf(this.query.toLowerCase())

any ideas? Thanks!

@eXeDK
Copy link

eXeDK commented Aug 23, 2012

@kneidels you need to set the 'strings' to false and then set the property 'property' to the name of the object field which you want to be shown (and be matched on). Like so:

$('#typeahead').typeahead({
    strings : false,
    property : 'name',
    source : [{"id":"1","name":"First guy"},{"id":"2","name":"Some other guy"}]
});

@vincentjr
Copy link

I'd like to add functionality where the typeahead does not execute an ajax request till after 2 keystrokes. Any recommendations?

@vincentjr
Copy link

Sorry for the noob question, wasn't thinking. Added if( $(this).val().length >= 2 ) { } in the source function for anyone that was curious. Is this the right way or is there a better way?

Copy link

ghost commented Sep 26, 2012

I believe you can just set the minLength property of typeahead to 2.

@kneidels
Copy link

kneidels commented Oct 8, 2012

@eXeDK - thanks belatedly for your reply.

i added what you suggested, but am still getting a similar error:
"item is undefined" - referring to the same line as i mentioned above in my previous post:

      return ~item.toLowerCase().indexOf(this.query.toLowerCase())

here is my code:



$('#mainSearchBox2').typeahead({
    strings : false,
    minLength: 3,
    property : 'StudentName',
    source: function(typeahead, query) {
        $.ajax({
            url: "/system/ajax_autoCompleteSearch.asp",
            dataType: "json",
            type: "GET",
            data: {
                max_rows: 15,
                query: query,
                ajax: 1
            },
            success: function(data) {
                var return_list = [], i = data.length;
                while (i--) {
                    return_list[i] = {id: data[i].StudentID, value: removenull(data[i].StudentName) };
                }
                typeahead.process(return_list);
            }
        });
    },
    onselect: function(obj) {
        $('[name="studentID"]').val(obj.id);
    }
});

function removenull(str) {
    var new_str = str;
    if (str == '') {
        new_str = str.replace('', "N/A");
    }
    else if (str == null) {
        new_str = "N/A";
    }

    return new_str;
}


and here is the sample of the data returned (confirmed via http request), when typing in "liz"

[{"StudentID":"634","StudentName":"Kirsh, Elizabeth"},{"StudentID":"689","StudentName":"Alex, Elizabeth"},{"StudentID":"734","StudentName":"Brer, Elizabeth"}]

Thanks!

@Diolor
Copy link

Diolor commented Oct 30, 2012

I know that might hear stupid for a autocomplete thing, but is there any way to turn off the "match" query item with the array?

I want to use it with google geocode whose array results are not exactly the same (e.g. query= 18457, results from json 185 47 so the second is fine to be displayed but it's not the same to be on the results).

So basically use it as a input field snitcher, call the json stuff and whatever the results are, post them.

@brianwigfield
Copy link

@Diolor define a matcher that always returns true

@ammancilla
Copy link

Thanks a lot !!!

@nodesl
Copy link

nodesl commented Dec 28, 2012

this is a nice tutorial. but i have a small issue. while i'm typing on the text box it just stuck my cursor for seconds. why is that ? every time i type it take few seconds to process the ajax. why is that ?

@tinchoel10
Copy link

Accent results are NOT working!!!!!!

@Err0r404
Copy link

Just for information $.browser has been removed from jQuery in 1.9.X
So on line 196 you should replace

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

by
if (/webkit/.test(navigator.userAgent.toLowerCase()) || /msie/.test(navigator.userAgent.toLowerCase())) {
this.$element.on('keydown', $.proxy(this.keypress, this))
}

Hope it will help someone

@Gramzivi
Copy link

Gramzivi commented Aug 7, 2013

Thanx Err0r404, u just cured my headache :)

@dejurin
Copy link

dejurin commented May 6, 2014

give work example with click event (go to url)

@JuanSomniis
Copy link

Thanx Err0r404,you rock!

@paical
Copy link

paical commented Sep 25, 2015

finally i could add my own option "onkeyup" in your source, thanks for your framework bro

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