js/bootstrap-typeahead.js
author indvd00m (gotoindvdum[at]gmail[dot]com)
Fri, 04 Jul 2014 16:42:41 +0400
changeset 0 ba8ab09f730e
permissions -rw-r--r--
First home page
     1 /* =============================================================
     2  * bootstrap-typeahead.js v2.3.1
     3  * http://twitter.github.com/bootstrap/javascript.html#typeahead
     4  * =============================================================
     5  * Copyright 2012 Twitter, Inc.
     6  *
     7  * Licensed under the Apache License, Version 2.0 (the "License");
     8  * you may not use this file except in compliance with the License.
     9  * You may obtain a copy of the License at
    10  *
    11  * http://www.apache.org/licenses/LICENSE-2.0
    12  *
    13  * Unless required by applicable law or agreed to in writing, software
    14  * distributed under the License is distributed on an "AS IS" BASIS,
    15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  * See the License for the specific language governing permissions and
    17  * limitations under the License.
    18  * ============================================================ */
    19 
    20 
    21 !function($){
    22 
    23   "use strict"; // jshint ;_;
    24 
    25 
    26  /* TYPEAHEAD PUBLIC CLASS DEFINITION
    27   * ================================= */
    28 
    29   var Typeahead = function (element, options) {
    30     this.$element = $(element)
    31     this.options = $.extend({}, $.fn.typeahead.defaults, options)
    32     this.matcher = this.options.matcher || this.matcher
    33     this.sorter = this.options.sorter || this.sorter
    34     this.highlighter = this.options.highlighter || this.highlighter
    35     this.updater = this.options.updater || this.updater
    36     this.source = this.options.source
    37     this.$menu = $(this.options.menu)
    38     this.shown = false
    39     this.listen()
    40   }
    41 
    42   Typeahead.prototype = {
    43 
    44     constructor: Typeahead
    45 
    46   , select: function () {
    47       var val = this.$menu.find('.active').attr('data-value')
    48       this.$element
    49         .val(this.updater(val))
    50         .change()
    51       return this.hide()
    52     }
    53 
    54   , updater: function (item) {
    55       return item
    56     }
    57 
    58   , show: function () {
    59       var pos = $.extend({}, this.$element.position(), {
    60         height: this.$element[0].offsetHeight
    61       })
    62 
    63       this.$menu
    64         .insertAfter(this.$element)
    65         .css({
    66           top: pos.top + pos.height
    67         , left: pos.left
    68         })
    69         .show()
    70 
    71       this.shown = true
    72       return this
    73     }
    74 
    75   , hide: function () {
    76       this.$menu.hide()
    77       this.shown = false
    78       return this
    79     }
    80 
    81   , lookup: function (event) {
    82       var items
    83 
    84       this.query = this.$element.val()
    85 
    86       if (!this.query || this.query.length < this.options.minLength) {
    87         return this.shown ? this.hide() : this
    88       }
    89 
    90       items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
    91 
    92       return items ? this.process(items) : this
    93     }
    94 
    95   , process: function (items) {
    96       var that = this
    97 
    98       items = $.grep(items, function (item) {
    99         return that.matcher(item)
   100       })
   101 
   102       items = this.sorter(items)
   103 
   104       if (!items.length) {
   105         return this.shown ? this.hide() : this
   106       }
   107 
   108       return this.render(items.slice(0, this.options.items)).show()
   109     }
   110 
   111   , matcher: function (item) {
   112       return ~item.toLowerCase().indexOf(this.query.toLowerCase())
   113     }
   114 
   115   , sorter: function (items) {
   116       var beginswith = []
   117         , caseSensitive = []
   118         , caseInsensitive = []
   119         , item
   120 
   121       while (item = items.shift()) {
   122         if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
   123         else if (~item.indexOf(this.query)) caseSensitive.push(item)
   124         else caseInsensitive.push(item)
   125       }
   126 
   127       return beginswith.concat(caseSensitive, caseInsensitive)
   128     }
   129 
   130   , highlighter: function (item) {
   131       var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
   132       return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
   133         return '<strong>' + match + '</strong>'
   134       })
   135     }
   136 
   137   , render: function (items) {
   138       var that = this
   139 
   140       items = $(items).map(function (i, item) {
   141         i = $(that.options.item).attr('data-value', item)
   142         i.find('a').html(that.highlighter(item))
   143         return i[0]
   144       })
   145 
   146       items.first().addClass('active')
   147       this.$menu.html(items)
   148       return this
   149     }
   150 
   151   , next: function (event) {
   152       var active = this.$menu.find('.active').removeClass('active')
   153         , next = active.next()
   154 
   155       if (!next.length) {
   156         next = $(this.$menu.find('li')[0])
   157       }
   158 
   159       next.addClass('active')
   160     }
   161 
   162   , prev: function (event) {
   163       var active = this.$menu.find('.active').removeClass('active')
   164         , prev = active.prev()
   165 
   166       if (!prev.length) {
   167         prev = this.$menu.find('li').last()
   168       }
   169 
   170       prev.addClass('active')
   171     }
   172 
   173   , listen: function () {
   174       this.$element
   175         .on('focus',    $.proxy(this.focus, this))
   176         .on('blur',     $.proxy(this.blur, this))
   177         .on('keypress', $.proxy(this.keypress, this))
   178         .on('keyup',    $.proxy(this.keyup, this))
   179 
   180       if (this.eventSupported('keydown')) {
   181         this.$element.on('keydown', $.proxy(this.keydown, this))
   182       }
   183 
   184       this.$menu
   185         .on('click', $.proxy(this.click, this))
   186         .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
   187         .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
   188     }
   189 
   190   , eventSupported: function(eventName) {
   191       var isSupported = eventName in this.$element
   192       if (!isSupported) {
   193         this.$element.setAttribute(eventName, 'return;')
   194         isSupported = typeof this.$element[eventName] === 'function'
   195       }
   196       return isSupported
   197     }
   198 
   199   , move: function (e) {
   200       if (!this.shown) return
   201 
   202       switch(e.keyCode) {
   203         case 9: // tab
   204         case 13: // enter
   205         case 27: // escape
   206           e.preventDefault()
   207           break
   208 
   209         case 38: // up arrow
   210           e.preventDefault()
   211           this.prev()
   212           break
   213 
   214         case 40: // down arrow
   215           e.preventDefault()
   216           this.next()
   217           break
   218       }
   219 
   220       e.stopPropagation()
   221     }
   222 
   223   , keydown: function (e) {
   224       this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
   225       this.move(e)
   226     }
   227 
   228   , keypress: function (e) {
   229       if (this.suppressKeyPressRepeat) return
   230       this.move(e)
   231     }
   232 
   233   , keyup: function (e) {
   234       switch(e.keyCode) {
   235         case 40: // down arrow
   236         case 38: // up arrow
   237         case 16: // shift
   238         case 17: // ctrl
   239         case 18: // alt
   240           break
   241 
   242         case 9: // tab
   243         case 13: // enter
   244           if (!this.shown) return
   245           this.select()
   246           break
   247 
   248         case 27: // escape
   249           if (!this.shown) return
   250           this.hide()
   251           break
   252 
   253         default:
   254           this.lookup()
   255       }
   256 
   257       e.stopPropagation()
   258       e.preventDefault()
   259   }
   260 
   261   , focus: function (e) {
   262       this.focused = true
   263     }
   264 
   265   , blur: function (e) {
   266       this.focused = false
   267       if (!this.mousedover && this.shown) this.hide()
   268     }
   269 
   270   , click: function (e) {
   271       e.stopPropagation()
   272       e.preventDefault()
   273       this.select()
   274       this.$element.focus()
   275     }
   276 
   277   , mouseenter: function (e) {
   278       this.mousedover = true
   279       this.$menu.find('.active').removeClass('active')
   280       $(e.currentTarget).addClass('active')
   281     }
   282 
   283   , mouseleave: function (e) {
   284       this.mousedover = false
   285       if (!this.focused && this.shown) this.hide()
   286     }
   287 
   288   }
   289 
   290 
   291   /* TYPEAHEAD PLUGIN DEFINITION
   292    * =========================== */
   293 
   294   var old = $.fn.typeahead
   295 
   296   $.fn.typeahead = function (option) {
   297     return this.each(function () {
   298       var $this = $(this)
   299         , data = $this.data('typeahead')
   300         , options = typeof option == 'object' && option
   301       if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
   302       if (typeof option == 'string') data[option]()
   303     })
   304   }
   305 
   306   $.fn.typeahead.defaults = {
   307     source: []
   308   , items: 8
   309   , menu: '<ul class="typeahead dropdown-menu"></ul>'
   310   , item: '<li><a href="#"></a></li>'
   311   , minLength: 1
   312   }
   313 
   314   $.fn.typeahead.Constructor = Typeahead
   315 
   316 
   317  /* TYPEAHEAD NO CONFLICT
   318   * =================== */
   319 
   320   $.fn.typeahead.noConflict = function () {
   321     $.fn.typeahead = old
   322     return this
   323   }
   324 
   325 
   326  /* TYPEAHEAD DATA-API
   327   * ================== */
   328 
   329   $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
   330     var $this = $(this)
   331     if ($this.data('typeahead')) return
   332     $this.typeahead($this.data())
   333   })
   334 
   335 }(window.jQuery);