bootstrap-typeahead.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /* =============================================================
  2. * bootstrap-typeahead.js v2.1.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. !function($){
  20. "use strict"; // jshint ;_;
  21. /* TYPEAHEAD PUBLIC CLASS DEFINITION
  22. * ================================= */
  23. var Typeahead = function (element, options) {
  24. this.$element = $(element)
  25. this.options = $.extend({}, $.fn.typeahead.defaults, options)
  26. this.matcher = this.options.matcher || this.matcher
  27. this.sorter = this.options.sorter || this.sorter
  28. this.highlighter = this.options.highlighter || this.highlighter
  29. this.updater = this.options.updater || this.updater
  30. this.$menu = $(this.options.menu).appendTo('body')
  31. this.source = this.options.source
  32. this.shown = false
  33. this.listen()
  34. }
  35. Typeahead.prototype = {
  36. constructor: Typeahead
  37. , select: function () {
  38. var val = this.$menu.find('.active').attr('data-value')
  39. if (val) {
  40. this.$element
  41. .val(this.updater(val))
  42. .change()
  43. }
  44. return this.hide()
  45. }
  46. , updater: function (item) {
  47. return item
  48. }
  49. , show: function () {
  50. var pos = $.extend({}, this.$element.offset(), {
  51. height: this.$element[0].offsetHeight
  52. })
  53. this.$menu.css({
  54. top: pos.top + pos.height
  55. , left: pos.left
  56. })
  57. this.$menu.show()
  58. this.shown = true
  59. return this
  60. }
  61. , hide: function () {
  62. this.$menu.hide()
  63. this.shown = false
  64. return this
  65. }
  66. , lookup: function (event) {
  67. var items
  68. this.query = this.$element.val()
  69. if (!this.query || this.query.length < this.options.minLength) {
  70. return this.shown ? this.hide() : this
  71. }
  72. items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
  73. return items ? this.process(items) : this
  74. }
  75. , process: function (items) {
  76. var that = this
  77. items = $.grep(items, function (item) {
  78. return that.matcher(item)
  79. })
  80. items = this.sorter(items)
  81. if (!items.length) {
  82. return this.shown ? this.hide() : this
  83. }
  84. return this.render(items.slice(0, this.options.items)).show()
  85. }
  86. , matcher: function (item) {
  87. return ~item.toLowerCase().indexOf(this.query.toLowerCase())
  88. }
  89. , sorter: function (items) {
  90. var beginswith = []
  91. , caseSensitive = []
  92. , caseInsensitive = []
  93. , item
  94. while (item = items.shift()) {
  95. if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
  96. else if (~item.indexOf(this.query)) caseSensitive.push(item)
  97. else caseInsensitive.push(item)
  98. }
  99. return beginswith.concat(caseSensitive, caseInsensitive)
  100. }
  101. , highlighter: function (item) {
  102. var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
  103. return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
  104. return '<strong>' + match + '</strong>'
  105. })
  106. }
  107. , render: function (items) {
  108. var that = this
  109. items = $(items).map(function (i, item) {
  110. i = $(that.options.item).attr('data-value', item)
  111. i.find('a').html(that.highlighter(item))
  112. return i[0]
  113. })
  114. this.$menu.html(items)
  115. return this
  116. }
  117. , next: function (event) {
  118. var active = this.$menu.find('.active').removeClass('active')
  119. , next = active.next()
  120. if (!next.length) {
  121. next = $(this.$menu.find('li')[0])
  122. }
  123. next.addClass('active')
  124. }
  125. , prev: function (event) {
  126. var active = this.$menu.find('.active').removeClass('active')
  127. , prev = active.prev()
  128. if (!prev.length) {
  129. prev = this.$menu.find('li').last()
  130. }
  131. prev.addClass('active')
  132. }
  133. , listen: function () {
  134. this.$element
  135. .on('blur', $.proxy(this.blur, this))
  136. .on('keypress', $.proxy(this.keypress, this))
  137. .on('keyup', $.proxy(this.keyup, this))
  138. if ($.browser.chrome || $.browser.webkit || $.browser.msie) {
  139. this.$element.on('keydown', $.proxy(this.keydown, this))
  140. }
  141. this.$menu
  142. .on('click', $.proxy(this.click, this))
  143. .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
  144. }
  145. , move: function (e) {
  146. if (!this.shown) return
  147. switch(e.keyCode) {
  148. case 9: // tab
  149. case 13: // enter
  150. case 27: // escape
  151. e.preventDefault()
  152. break
  153. case 38: // up arrow
  154. e.preventDefault()
  155. this.prev()
  156. break
  157. case 40: // down arrow
  158. e.preventDefault()
  159. this.next()
  160. break
  161. }
  162. e.stopPropagation()
  163. }
  164. , keydown: function (e) {
  165. this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
  166. this.move(e)
  167. }
  168. , keypress: function (e) {
  169. if (this.suppressKeyPressRepeat) return
  170. this.move(e)
  171. }
  172. , keyup: function (e) {
  173. switch(e.keyCode) {
  174. case 40: // down arrow
  175. case 38: // up arrow
  176. break
  177. case 9: // tab
  178. case 13: // enter
  179. if (!this.shown) return
  180. this.select()
  181. break
  182. case 27: // escape
  183. if (!this.shown) return
  184. this.hide()
  185. break
  186. default:
  187. this.lookup()
  188. }
  189. e.stopPropagation()
  190. e.preventDefault()
  191. }
  192. , blur: function (e) {
  193. var that = this
  194. setTimeout(function () { that.hide() }, 150)
  195. }
  196. , click: function (e) {
  197. e.stopPropagation()
  198. e.preventDefault()
  199. this.select()
  200. }
  201. , mouseenter: function (e) {
  202. this.$menu.find('.active').removeClass('active')
  203. $(e.currentTarget).addClass('active')
  204. }
  205. }
  206. /* TYPEAHEAD PLUGIN DEFINITION
  207. * =========================== */
  208. $.fn.typeahead = function (option) {
  209. return this.each(function () {
  210. var $this = $(this)
  211. , data = $this.data('typeahead')
  212. , options = typeof option == 'object' && option
  213. if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
  214. if (typeof option == 'string') data[option]()
  215. })
  216. }
  217. $.fn.typeahead.defaults = {
  218. source: []
  219. , items: 8
  220. , menu: '<ul class="typeahead dropdown-menu"></ul>'
  221. , item: '<li><a href="#"></a></li>'
  222. , minLength: 1
  223. }
  224. $.fn.typeahead.Constructor = Typeahead
  225. /* TYPEAHEAD DATA-API
  226. * ================== */
  227. $(function () {
  228. $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
  229. var $this = $(this)
  230. if ($this.data('typeahead')) return
  231. e.preventDefault()
  232. $this.typeahead($this.data())
  233. })
  234. })
  235. }(window.jQuery);