Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Bootstrap's Typeahead plugin extended (AJAX functionality, comma-separated values, autowidth, and autoselect)

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

  • 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 the proper source, and other examples, please see the original gist.

Example showing off all the above features

  // 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);
      });
    }
  , 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
  });
/* =============================================================
* 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.autoselect = this.options.autoselect
this.autowidth = this.options.autowidth
this.strings = true
this.shown = false
this.listen()
}
Typeahead.prototype = {
constructor: Typeahead
, select: function () {
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()
}
, 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]
})
if (that.autoselect) items.first().addClass('active')
if (that.autowidth) this.$menu.width(this.$element.width());
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 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
, autoselect: true
, autowidth: true
, 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.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
@simon-lang

This comment has been minimized.

Copy link

simon-lang commented Jun 18, 2012

This is exactly what I was looking for, thanks mate!

@jasoncoding

This comment has been minimized.

Copy link

jasoncoding commented Jun 28, 2013

Nice, this lead me in the right direction for a width override. Thanks!

@rtselva03

This comment has been minimized.

Copy link

rtselva03 commented Jun 28, 2016

thanks dude, and i want one more thing from this, to avoid free type entry which 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
You can’t perform that action at this time.