bootstrap 的typeahead默认不支持复杂对象数组作为数据源,实际中很难满足业务需求,为了实现这个目标,特地花了几个小时加以改造使之支持复杂对象数据源,但是暂时不支持嵌套属性,只支持[{name:'name',age:10}]这种对象数组,大多数情况下应该够用了
下面是bootstrap-typeahead.js改造后大代码,喜欢的可以直接使用改造比较少,如果需要看改动,找文件对比工具和原文件对比就知道了
调用例子
1 $('#ttext').typeahead({ 2 source: function (query,process) { 3 return $.getJSON( 4 '/test/typeahead', 5 { query: query }, 6 function (data) { 7 return process(data); 8 }); 9 },10 minLength:3,11 labeler:['value','name']//下拉列表需要显示的对象属性12 ,propName:'value' //用来匹配搜索的对象属性,默认是对象的value属性,依然支持单值数据源,只需要把这个属性设置为'' 13 })14 });
1 * bootstrap-typeahead.js v2.2.1 2 * http://twitter.github.com/bootstrap/javascript.html#typeahead 3 * ============================================================= 4 * Copyright 2012 Twitter, Inc. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * ============================================================ */ 18 19 20 !function($){ 21 22 "use strict"; // jshint ;_; 23 24 25 /* TYPEAHEAD PUBLIC CLASS DEFINITION 26 * ================================= */ 27 28 var Typeahead = function (element, options) { 29 this.$element = $(element) 30 this.options = $.extend({}, $.fn.typeahead.defaults, options) 31 this.matcher = this.options.matcher || this.matcher 32 this.sorter = this.options.sorter || this.sorter 33 this.highlighter = this.options.highlighter || this.highlighter 34 this.updater = this.options.updater || this.updater 35 36 this.$menu = $(this.options.menu).appendTo('body') 37 this.source = this.options.source 38 this.propName= this.options.propName || "" 39 this.labeler = this.options.labeler || [this.propName] 40 this.shown = false 41 this.listen() 42 } 43 44 Typeahead.prototype = { 45 46 constructor: Typeahead 47 , propName:'value' 48 , labeler:['value'] 49 , select: function () { 50 var val = this.$menu.find('.active').attr('data-value') 51 52 this.$element 53 .val(this.updater(val)) 54 .change() 55 return this.hide() 56 } 57 58 , updater: function (item) { 59 return item; 60 } 61 , show: function () { 62 var pos = $.extend({}, this.$element.offset(), { 63 height: this.$element[0].offsetHeight 64 }) 65 66 this.$menu.css({ 67 top: pos.top + pos.height 68 , left: pos.left 69 }) 70 71 this.$menu.show() 72 this.shown = true 73 return this 74 } 75 76 , hide: function () { 77 this.$menu.hide() 78 this.shown = false 79 return this 80 } 81 82 , lookup: function (event) { 83 var items 84 85 this.query = this.$element.val() 86 87 if (!this.query || this.query.length < this.options.minLength) { 88 return this.shown ? this.hide() : this 89 } 90 91 items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source 92 93 return items ? this.process(items) : this 94 } 95 96 , process: function (items) { 97 var that = this 98 99 items = $.grep(items, function (item) {100 var mat = that.matcher(item)101 //console.log(mat)102 return mat103 })104 105 106 items = this.sorter(items)107 108 if (!items.length) {109 return this.shown ? this.hide() : this110 }111 112 return this.render(items.slice(0, this.options.items)).show()113 }114 115 , matcher: function (item) {116 // console.log(item)117 if(this.propName=="")118 return ~item.toLowerCase().indexOf(this.query.toLowerCase())119 else120 return ~item[this.propName].toLowerCase().indexOf(this.query.toLowerCase())121 }122 123 , sorter: function (items) {124 var beginswith = []125 , caseSensitive = []126 , caseInsensitive = []127 , item128 129 while (item = items.shift()) {130 131 var myPropVal = this.propName==""?item:item[this.propName];132 if (!myPropVal.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)133 else if (~myPropVal.indexOf(this.query)) caseSensitive.push(item)134 else caseInsensitive.push(item)135 }136 137 return beginswith.concat(caseSensitive, caseInsensitive)138 }139 140 , highlighter: function (item) { 141 var myPropVal=''142 if(this.labeler.length==0){143 myPropVal = item144 }else{145 146 $.each(this.labeler, function(i, pname){147 myPropVal = myPropVal.concat(item[pname]).concat(' ')148 })149 }150 151 var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')152 return myPropVal.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {153 return '' + match + ''154 })155 }156 157 , render: function (items) {158 var that = this159 160 items = $(items).map(function (i, item) {161 162 i = $(that.options.item).attr('data-value',that.propName==""?item:item[that.propName])163 i.find('a').html(that.highlighter(item))164 return i[0]165 })166 167 items.first().addClass('active')168 this.$menu.html(items)169 return this170 }171 172 , next: function (event) {173 var active = this.$menu.find('.active').removeClass('active')174 , next = active.next()175 176 if (!next.length) {177 next = $(this.$menu.find('li')[0])178 }179 180 next.addClass('active')181 }182 183 , prev: function (event) {184 var active = this.$menu.find('.active').removeClass('active')185 , prev = active.prev()186 187 if (!prev.length) {188 prev = this.$menu.find('li').last()189 }190 191 prev.addClass('active')192 }193 194 , listen: function () {195 this.$element196 .on('blur', $.proxy(this.blur, this))197 .on('keypress', $.proxy(this.keypress, this))198 .on('keyup', $.proxy(this.keyup, this))199 200 if (this.eventSupported('keydown')) {201 this.$element.on('keydown', $.proxy(this.keydown, this))202 }203 204 this.$menu205 .on('click', $.proxy(this.click, this))206 .on('mouseenter', 'li', $.proxy(this.mouseenter, this))207 }208 209 , eventSupported: function(eventName) {210 var isSupported = eventName in this.$element211 if (!isSupported) {212 this.$element.setAttribute(eventName, 'return;')213 isSupported = typeof this.$element[eventName] === 'function'214 }215 return isSupported216 }217 218 , move: function (e) {219 if (!this.shown) return220 221 switch(e.keyCode) {222 case 9: // tab223 case 13: // enter224 case 27: // escape225 e.preventDefault()226 break227 228 case 38: // up arrow229 e.preventDefault()230 this.prev()231 break232 233 case 40: // down arrow234 e.preventDefault()235 this.next()236 break237 }238 239 e.stopPropagation()240 }241 242 , keydown: function (e) {243 this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])244 this.move(e)245 }246 247 , keypress: function (e) {248 if (this.suppressKeyPressRepeat) return249 this.move(e)250 }251 252 , keyup: function (e) {253 switch(e.keyCode) {254 case 40: // down arrow255 case 38: // up arrow256 case 16: // shift257 case 17: // ctrl258 case 18: // alt259 break260 261 case 9: // tab262 case 13: // enter263 if (!this.shown) return264 this.select()265 break266 267 case 27: // escape268 if (!this.shown) return269 this.hide()270 break271 272 default:273 this.lookup()274 }275 276 e.stopPropagation()277 e.preventDefault()278 }279 280 , blur: function (e) {281 var that = this282 setTimeout(function () { that.hide() }, 150)283 }284 285 , click: function (e) {286 e.stopPropagation()287 e.preventDefault()288 this.select()289 }290 291 , mouseenter: function (e) {292 this.$menu.find('.active').removeClass('active')293 $(e.currentTarget).addClass('active')294 }295 296 }297 298 299 /* TYPEAHEAD PLUGIN DEFINITION300 * =========================== */301 302 $.fn.typeahead = function (option) {303 return this.each(function () {304 var $this = $(this)305 , data = $this.data('typeahead')306 , options = typeof option == 'object' && option307 if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))308 if (typeof option == 'string') data[option]()309 })310 }311 312 $.fn.typeahead.defaults = {313 source: []314 , items: 8315 , menu: ''316 , item: '