public
Last active — forked from gudbergur/README.markdown

Bootstrap's Typeahead plugin extended (allowing for AJAX functionality) among other things

  • Download Gist
README.markdown
Markdown

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
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 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 );
gistfile1.diff
Diff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 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

Outstanding... works like a charm!

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

Brilliant, thanks!

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.

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.

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

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);

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.

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/

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!

@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"}]
});

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

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?

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

@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!

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.

@Diolor define a matcher that always returns true

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 ?

Accent results are NOT working!!!!!!

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

Thanx Err0r404, u just cured my headache :)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.