Skip to content

Instantly share code, notes, and snippets.

@ericvoid
Forked from gudbergur/README.markdown
Last active December 15, 2015 10:59
Show Gist options
  • Save ericvoid/5249721 to your computer and use it in GitHub Desktop.
Save ericvoid/5249721 to your computer and use it in GitHub Desktop.
Bootstrap Typeahead with three new features: selectfist option, source function call debounce and template support.

This is a fork of Bootstrap Typeahead that adds some more modifications.

Selectfirst: false

  $('.typeahead').typeahead({
    source: MYDATA

    // Typeahead will not select the first suggestion.
    // If the user press enter without selecting any suggestion
    // the onselect function will be called with the inputted text
    selectfirst: false,
    onselect: function (obj) {
      alert('Selected '+obj)
    }

  })

Debounce

Debounce wasn't written by me. I actually don't know who did it. But it prevents the source function being called on each and every keypress. Useful when source function contains an ajax call.

Templating

Templating feature similar to twitter typeahead.

Example using internal (minimal) engine:

  $('input[name="ta-internal"]').typeahead({
    source: [{ value: 'John Doe', age: '28', email: 'john.doe@ceda.org' }, ...],
    engine: 'internal', // <-- internal lower case
    template: [
      '<div class="search-suggestion">',
      '<p class="age">%AGE</p>', // variables begin with percentage 
      '<p class="name">%VALUE</p>',  // chars are all-upper-case
      '<p class="email">%EMAIL</p>',
      '</div>'
    ].join('')
  });

Example using Hogan template engine:

$('input[name="ta-hogan"]').typeahead({
    source: [{ value: 'Many Doe', age: '27', email: 'mary.doe@apperture.com' }, ...],
    engine: Hogan, // pass Hogan obj
    template: [
      '<div class="search-suggestion">',
      '<p class="age">{{{age}}}</p>',
      '<p class="name">{{{value}}}</p>',
      '<p class="email">{{{email}}}</p>',
      '</div>'
    ].join('')
  });

Compatible engines are accepted. They must have two methods:

  // engine must have compile method
  var compiled = engine.compile(template)

  // compiled object must have render method
  var result = compiled.render(context)
/* =============================================================
* 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.timer_lookup = false
this.timer_timeout = this.options.timeout || 400
this.listen()
}
Typeahead.prototype = {
constructor: Typeahead
, select: function () {
var selected = this.$menu.find('.active').attr('data-value')
var val = null
var text = null
if (!selected) {
val = this.$element.val()
}
else {
val = JSON.parse(selected)
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>'
})
}
, select_renderfunction: function () {
if (!this.options.engine) {
return this.renderitem_templateless()
}
if (typeof this.options.engine == 'string' && this.options.engine == 'internal') {
return this.renderitem_internal()
}
return this.renderitem_thirdparty()
}
, render: function (items) {
items = $(items).map(this.select_renderfunction())
if (this.options.selectfirst) {
items.first().addClass('active')
}
this.$menu.html(items)
return this
}
, renderitem_templateless: function () {
var that = this
return function (i, data) {
i = $(that.options.item).attr('data-value', JSON.stringify(data))
if (!that.strings)
data = data[that.options.property]
i.find('a').html(that.highlighter(data))
return i[0]
}
}
, renderitem_internal: function () {
var that = this
return function (i, data) {
var html = that.options.template
if (that.strings)
html = html.replace('%' + that.options.property.toUpperCase(), that.highlighter(data))
else
for (var k in data) {
if (data.hasOwnProperty(k)) html = html.replace('%' + k.toUpperCase(), that.highlighter(data[k]))
}
i = $(that.options.item).attr('data-value', JSON.stringify(data))
i.find('a').html(html)
return i[0]
}
}
, renderitem_thirdparty: function () {
var compiledTemplate = this.options.engine.compile(this.options.template)
var that = this
return function (i, data) {
var context = {}
if (that.strings)
context[that.options.property] = that.highlighter(data)
else
for (var k in data) {
if (data.hasOwnProperty(k)) context[k] = that.highlighter(data[k])
}
var html = compiledTemplate.render(context)
i = $(that.options.item).attr('data-value', JSON.stringify(data))
i.find('a').html($(html))
return i[0]
}
}
, 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:
var that = this;
clearTimeout(this.timer_lookup);
this.timer_lookup = setTimeout(function(){that.lookup()}, this.timer_timeout);
}
}
, 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>'
, selectfirst: true
, onselect: null
, property: 'value'
, engine: null
, template: ''
}
$.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 );
--- <unnamed>
+++ <unnamed>
@@ -32,6 +32,8 @@
this.onselect = this.options.onselect
this.strings = true
this.shown = false
+ this.timer_lookup = false
+ this.timer_timeout = this.options.timeout || 400
this.listen()
}
@@ -40,13 +42,22 @@
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)
+ var selected = this.$menu.find('.active').attr('data-value')
+
+ var val = null
+ var text = null
+
+ if (!selected) {
+ val = this.$element.val()
+ }
+ else {
+ val = JSON.parse(selected)
+
+ if (!this.strings) text = val[this.options.property]
+ else text = val
+
+ this.$element.val(text)
+ }
if (typeof this.onselect == "function")
this.onselect(val)
@@ -149,20 +160,83 @@
})
}
+ , select_renderfunction: function () {
+ if (!this.options.engine) {
+ return this.renderitem_templateless()
+ }
+
+ if (typeof this.options.engine == 'string' && this.options.engine == 'internal') {
+ return this.renderitem_internal()
+ }
+
+ return this.renderitem_thirdparty()
+ }
+
, 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')
+ items = $(items).map(this.select_renderfunction())
+
+ if (this.options.selectfirst) {
+ items.first().addClass('active')
+ }
+
this.$menu.html(items)
return this
+ }
+
+ , renderitem_templateless: function () {
+ var that = this
+
+ return function (i, data) {
+ i = $(that.options.item).attr('data-value', JSON.stringify(data))
+
+ if (!that.strings)
+ data = data[that.options.property]
+ i.find('a').html(that.highlighter(data))
+ return i[0]
+ }
+ }
+
+ , renderitem_internal: function () {
+ var that = this
+
+ return function (i, data) {
+ var html = that.options.template
+
+ if (that.strings)
+ html = html.replace('%' + that.options.property.toUpperCase(), that.highlighter(data))
+ else
+ for (var k in data) {
+ if (data.hasOwnProperty(k)) html = html.replace('%' + k.toUpperCase(), that.highlighter(data[k]))
+ }
+
+ i = $(that.options.item).attr('data-value', JSON.stringify(data))
+ i.find('a').html(html)
+
+ return i[0]
+ }
+ }
+
+ , renderitem_thirdparty: function () {
+ var compiledTemplate = this.options.engine.compile(this.options.template)
+ var that = this
+
+ return function (i, data) {
+ var context = {}
+
+ if (that.strings)
+ context[that.options.property] = that.highlighter(data)
+ else
+ for (var k in data) {
+ if (data.hasOwnProperty(k)) context[k] = that.highlighter(data[k])
+ }
+
+ var html = compiledTemplate.render(context)
+
+ i = $(that.options.item).attr('data-value', JSON.stringify(data))
+ i.find('a').html($(html))
+
+ return i[0]
+ }
}
, next: function (event) {
@@ -222,7 +296,9 @@
break
default:
- this.lookup()
+ var that = this;
+ clearTimeout(this.timer_lookup);
+ this.timer_lookup = setTimeout(function(){that.lookup()}, this.timer_timeout);
}
}
@@ -289,8 +365,11 @@
, items: 8
, menu: '<ul class="typeahead dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
+ , selectfirst: true
, onselect: null
, property: 'value'
+ , engine: null
+ , template: ''
}
$.fn.typeahead.Constructor = Typeahead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment