js/bootstrap-typeahead.js
changeset 0 ba8ab09f730e
equal deleted inserted replaced
-1:000000000000 0:ba8ab09f730e
       
     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);