bootstrap-typeahead.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. !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.source = this.options.source
  31. this.$menu = $(this.options.menu)
  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. this.$element
  40. .val(this.updater(val))
  41. .change()
  42. return this.hide()
  43. }
  44. , updater: function (item) {
  45. return item
  46. }
  47. , show: function () {
  48. var pos = $.extend({}, this.$element.position(), {
  49. height: this.$element[0].offsetHeight
  50. })
  51. this.$menu
  52. .insertAfter(this.$element)
  53. .css({
  54. top: pos.top + pos.height
  55. , left: pos.left
  56. })
  57. .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. items.first().addClass('active')
  115. this.$menu.html(items)
  116. return this
  117. }
  118. , next: function (event) {
  119. var active = this.$menu.find('.active').removeClass('active')
  120. , next = active.next()
  121. if (!next.length) {
  122. next = $(this.$menu.find('li')[0])
  123. }
  124. next.addClass('active')
  125. }
  126. , prev: function (event) {
  127. var active = this.$menu.find('.active').removeClass('active')
  128. , prev = active.prev()
  129. if (!prev.length) {
  130. prev = this.$menu.find('li').last()
  131. }
  132. prev.addClass('active')
  133. }
  134. , listen: function () {
  135. this.$element
  136. .on('focus', $.proxy(this.focus, this))
  137. .on('blur', $.proxy(this.blur, this))
  138. .on('keypress', $.proxy(this.keypress, this))
  139. .on('keyup', $.proxy(this.keyup, this))
  140. if (this.eventSupported('keydown')) {
  141. this.$element.on('keydown', $.proxy(this.keydown, this))
  142. }
  143. this.$menu
  144. .on('click', $.proxy(this.click, this))
  145. .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
  146. .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
  147. }
  148. , eventSupported: function(eventName) {
  149. var isSupported = eventName in this.$element
  150. if (!isSupported) {
  151. this.$element.setAttribute(eventName, 'return;')
  152. isSupported = typeof this.$element[eventName] === 'function'
  153. }
  154. return isSupported
  155. }
  156. , move: function (e) {
  157. if (!this.shown) return
  158. switch(e.keyCode) {
  159. case 9: // tab
  160. case 13: // enter
  161. case 27: // escape
  162. e.preventDefault()
  163. break
  164. case 38: // up arrow
  165. e.preventDefault()
  166. this.prev()
  167. break
  168. case 40: // down arrow
  169. e.preventDefault()
  170. this.next()
  171. break
  172. }
  173. e.stopPropagation()
  174. }
  175. , keydown: function (e) {
  176. this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
  177. this.move(e)
  178. }
  179. , keypress: function (e) {
  180. if (this.suppressKeyPressRepeat) return
  181. this.move(e)
  182. }
  183. , keyup: function (e) {
  184. switch(e.keyCode) {
  185. case 40: // down arrow
  186. case 38: // up arrow
  187. case 16: // shift
  188. case 17: // ctrl
  189. case 18: // alt
  190. break
  191. case 9: // tab
  192. case 13: // enter
  193. if (!this.shown) return
  194. this.select()
  195. break
  196. case 27: // escape
  197. if (!this.shown) return
  198. this.hide()
  199. break
  200. default:
  201. this.lookup()
  202. }
  203. e.stopPropagation()
  204. e.preventDefault()
  205. }
  206. , focus: function (e) {
  207. this.focused = true
  208. }
  209. , blur: function (e) {
  210. this.focused = false
  211. if (!this.mousedover && this.shown) this.hide()
  212. }
  213. , click: function (e) {
  214. e.stopPropagation()
  215. e.preventDefault()
  216. this.select()
  217. this.$element.focus()
  218. }
  219. , mouseenter: function (e) {
  220. this.mousedover = true
  221. this.$menu.find('.active').removeClass('active')
  222. $(e.currentTarget).addClass('active')
  223. }
  224. , mouseleave: function (e) {
  225. this.mousedover = false
  226. if (!this.focused && this.shown) this.hide()
  227. }
  228. }
  229. /* TYPEAHEAD PLUGIN DEFINITION
  230. * =========================== */
  231. var old = $.fn.typeahead
  232. $.fn.typeahead = function (option) {
  233. return this.each(function () {
  234. var $this = $(this)
  235. , data = $this.data('typeahead')
  236. , options = typeof option == 'object' && option
  237. if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
  238. if (typeof option == 'string') data[option]()
  239. })
  240. }
  241. $.fn.typeahead.defaults = {
  242. source: []
  243. , items: 8
  244. , menu: '<ul class="typeahead dropdown-menu"></ul>'
  245. , item: '<li><a href="#"></a></li>'
  246. , minLength: 1
  247. }
  248. $.fn.typeahead.Constructor = Typeahead
  249. /* TYPEAHEAD NO CONFLICT
  250. * =================== */
  251. $.fn.typeahead.noConflict = function () {
  252. $.fn.typeahead = old
  253. return this
  254. }
  255. /* TYPEAHEAD DATA-API
  256. * ================== */
  257. $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
  258. var $this = $(this)
  259. if ($this.data('typeahead')) return
  260. $this.typeahead($this.data())
  261. })
  262. }(window.jQuery);