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 mat 103 }) 104 105 106 items = this.sorter(items) 107 108 if (!items.length) { 109 return this.shown ? this.hide() : this 110 } 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 else 120 return ~item[this.propName].toLowerCase().indexOf(this.query.toLowerCase()) 121 } 122 123 , sorter: function (items) { 124 var beginswith = [] 125 , caseSensitive = [] 126 , caseInsensitive = [] 127 , item 128 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 = item 144 }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 '<strong>' + match + '</strong>' 154 }) 155 } 156 157 , render: function (items) { 158 var that = this 159 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 this 170 } 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.$element 196 .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.$menu 205 .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.$element 211 if (!isSupported) { 212 this.$element.setAttribute(eventName, 'return;') 213 isSupported = typeof this.$element[eventName] === 'function' 214 } 215 return isSupported 216 } 217 218 , move: function (e) { 219 if (!this.shown) return 220 221 switch(e.keyCode) { 222 case 9: // tab 223 case 13: // enter 224 case 27: // escape 225 e.preventDefault() 226 break 227 228 case 38: // up arrow 229 e.preventDefault() 230 this.prev() 231 break 232 233 case 40: // down arrow 234 e.preventDefault() 235 this.next() 236 break 237 } 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) return 249 this.move(e) 250 } 251 252 , keyup: function (e) { 253 switch(e.keyCode) { 254 case 40: // down arrow 255 case 38: // up arrow 256 case 16: // shift 257 case 17: // ctrl 258 case 18: // alt 259 break 260 261 case 9: // tab 262 case 13: // enter 263 if (!this.shown) return 264 this.select() 265 break 266 267 case 27: // escape 268 if (!this.shown) return 269 this.hide() 270 break 271 272 default: 273 this.lookup() 274 } 275 276 e.stopPropagation() 277 e.preventDefault() 278 } 279 280 , blur: function (e) { 281 var that = this 282 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 DEFINITION 300 * =========================== */ 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' && option 307 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: 8 315 , menu: '<ul class="typeahead dropdown-menu"></ul>' 316 , item: '<li><a href="#"></a></li>' 317 , minLength: 3 318 , propName:'value' 319 , labeler:['value'] 320 321 } 322 323 $.fn.typeahead.Constructor = Typeahead 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 e.preventDefault() 333 $this.typeahead($this.data()) 334 }) 335 336 }(window.jQuery);