目的:select下拉框条目太多(上百),当用户选择具体项时会浪费用户很多时间去寻找,因此需要一个搜索框让用户输入关键字来匹配列表,便于用户选择
示例图:
1、html结构
<div class="custom-select-container" data-name="oilBrand" data-default-value="品牌系列" data-placeholder="品牌系列"> <textarea style="display: none;"> [{"id": "1", "name": "1嘉实多级护全合成油SN级5W-30"}, {"id": "11", "name": "111嘉实多级护全合成油SN级5W-30"}, {"id": "2", "name": "2实多级护全合成油SN级5W-30"}, {"id": "3", "name": "3实多级护全合成油SN级5W-30"}, {"id": "4", "name": "4实多级护全合成油SN级5W-30"}, {"id": "5", "name": "5实多级护全合成油SN级5W-30"}, {"id": "6", "name": "6实多级护全合成油SN级5W-30"}] </textarea> </div>
说明:
初始化容器属性:
data-name: 相当于原始select的name
data-default-value: input文本搜索框的初始化值
data-placeholder: input文本搜索框的占位值
textarea:
里面是一个JONS字符串,保存着自定义select的键值对,注意里面的id才是需要传递给后端接口的,而name只是显示文本
2、实现原理
将用户输入的关键字用正则去匹配数据,展示匹配后的数据下拉列表,供用户选择
3、重要交互实现点
3.1、用户点击(或鼠标聚焦)搜索框,需要显示所有的数据下拉列表
3.2、用户每次输入文本,即当文本框值有改变时,匹配相应的数据列表并展示
3.3、当用户点击了数据列表某一项时,即当用户选择了
3.4、当用户在指定的列表项按下enter键时,即当用户选择了
3.5、当用户鼠标移动在数据下拉列表上时,可以通过键盘up,down上下键来选择
3.6、当用户选择了列表项后,再次点击(或聚焦)搜索框,需要展示所有数据列表,并高亮显示所选择的数据项
3.7、当用户在搜索框中用鼠标粘贴了关键字后,需要显示匹配的数据列表并展示(此项较复杂,并兼容了ie7,8)
注:jQuery在处理paste事件时,event参数并没有处理event.clipboardData,即为undefined,因此需要自己处理事件绑定(兼容ie)
4、示例
<!DOCTYPE html> <html> <head> <script src="http://apps.bdimg.com/libs/jquery/1.11.1/jquery.min.js"></script> <meta charset="utf-8"> <title>custom select</title> <style> * {margin: 0; padding: 0;} /*customSelect*/ .custom-select-container { width: 150px; position: relative; display: inline-block; vertical-align: top; margin-right: 5px; /*兼容IE6, 7*/ *display: inline; *zoom: 1; margin: 100px 0 0 100px; } .custom-select-input { width: 120px; padding-right: 28px; height: 30px; line-height: 30px; font-size: 14px; text-indent: 5px; *margin-left: -5px; border: none 0; outline: none; } .custom-select-input-wrap { position: relative; width: 148px; height: 30px; overflow: hidden; border: 1px solid #aaa; } .list-toggle-trigger { position: absolute; right: 0; top: 0; padding: 10px; background-color: #fff; } .list-toggle-trigger i { display: block; width: 0; height: 0; border-width: 8px 5px 5px; border-style: solid; border-color: #aaa transparent transparent transparent; } .list-toggle-trigger.active { padding-top: 4px; } .list-toggle-trigger.active i { border-width: 5px 5px 8px; border-color: transparent transparent #aaa transparent; } .custom-select-list { min-width: 120px; max-height: 400px; overflow-y: auto; border: 1px solid #006ed5; position: absolute; left: 0; top: 32px; z-index: 100; background-color: #FFF; display: none; } .custom-select-list span { display: block; height: 24px; line-height: 24px; color: #000; text-indent: 5px; white-space: nowrap; /*padding-right: 25px;*/ } .custom-select-list span.hover { color: #FFF; background-color: #006ed5; cursor: default; } </style> </head> <body> <div class="custom-select-container" data-name="oilBrand" data-default-value="品牌系列" data-placeholder="品牌系列"> <textarea style="display: none;"> [{"id": "1", "name": "1嘉实多级护全合成油SN级5W-30"}, {"id": "11", "name": "111嘉实多级护全合成油SN级5W-30"}, {"id": "2", "name": "2实多级护全合成油SN级5W-30"}, {"id": "3", "name": "3实多级护全合成油SN级5W-30"}, {"id": "4", "name": "4实多级护全合成油SN级5W-30"}, {"id": "5", "name": "5实多级护全合成油SN级5W-30"}, {"id": "6", "name": "6实多级护全合成油SN级5W-30"}] </textarea> </div> <script> (function($){ var jsonParse = window.JSON && JSON.parse ? JSON.parse : eval; var addEvent; if (document.body.addEventListener) { addEvent = function(elem, type, eventHandler) { elem.addEventListener(type, eventHandler); }; } else if (document.body.attachEvent) { addEvent = function(elem, type, eventHandler) { elem.attachEvent('on' + type, eventHandler); }; } else { addEvent = function(elem, type, eventHandler) { elem['on' + type] = eventHandler; }; } /** * author: yangjunhua * email: 1025357509@qq.com * constructor: * CustomSelect * params: * options = { * container: selector, // init container * change: function(value) {} // it means select change handler * } * example: * html: * <div class="custom-select-container" data-name="carBrand" data-default-value="品牌系列" data-placeholder="品牌系列"> * <textarea style="display: none;">[{"id": "1", "name": "宝马"}, {"id": "2", "name": "奥迪"}]</textarea> * </div> * <div class="custom-select-container" data-name="carPrice" data-default-value="价格区间" data-placeholder="价格区间"> * <textarea style="display: none;">[{"id": "1", "name": "30-100万"}, {"id": "2", "name": "100-300万"}]</textarea> * </div> * js: * $('.custom-select-container').each(function() { * new CustomSelect({ * container: this, * change: function(value) { * // value it means id * // query data ... * } * }); * }); * */ function CustomSelect(options) { this.options = $.extend({}, options || {}); this.init(); } // 原型 CustomSelect.prototype = { constructor: CustomSelect, keywords: '', init: function() { if (!this.options || !this.options.container) return; this.initContainer(); this.listenFocus(); this.listenBlur(); this.listenSearch(); this.listenTrigger(); this.listenSelect(); this.listenMouseenter(); this.listenBodyClick(); this.listenPaste(); }, initContainer: function() { this.$container = $(this.options.container).addClass('custom-select-container'); var tpl = '<div class="custom-select-input-wrap">' + '<input type="text" class="custom-select-input" value="' + (this.$container.data('default-value')) + '" placeholder="' + (this.$container.data('placeholder')) + '">' + '<div class="list-toggle-trigger"><i></i></div>' + '</div>' + '<div class="custom-select-list"></div>'; this.dataList = jsonParse(this.$container.find('textarea')[0].value); this.$container.html(tpl); this.$input = this.$container.find('.custom-select-input'); this.$list = this.$container.find('.custom-select-list'); this.$filterList = $(); this.$trigger = this.$container.find('.list-toggle-trigger'); this.defaltValue = this.$container.data('default-value'); this.$container.data({ 'customSelect': this, 'value': '' }); }, _isRended: false, _isResetSize: false, _highlightIndex: -1, _seletedIndex: -1, highlight: function(idx) { idx = idx !== undefined && idx > -1 ? idx : this._highlightIndex; idx >= 0 && this.$filterList.children().removeClass('hover').eq(idx).addClass('hover'); }, renderList: function(list) { var listTpl = '', len = list.length; if (len > 0) { for (var i = 0; i < len; i++) { listTpl += '<span data-value="' + list[i].id + '">' + list[i].name + '</span>'; } this.$list.html(listTpl).slideDown('fast'); } else { this.$list.html(listTpl).hide(); } this.filterDataList = list; this._isRended = true; if (!this._isResetSize) { this._isResetSize = true; this.$list.css({ this.$list[0].scrollWidth + 25 + 'px' }); } }, search: function() { if (this.keywords === '' || this.keywords === this.defaltValue) { this.$input.val(''); this.renderList(this.dataList); this.$filterList = this.$list; return; } var searchList = []; var len = this.dataList.length; var reg = new RegExp(this.keywords, 'i'); for (var i = 0; i < len; i++) { var dataItem = this.dataList[i]; dataItem.name.match(reg) && (searchList.push(dataItem)); this.$filterList = this.$filterList.add(this.$list.eq(i)); } this.renderList(searchList); }, listenFocus: function() { var self = this; this.$input.on('focus', function() { if (self._isRended && self.filterDataList.length > 0) { self.highlight(self._seletedIndex); self.$list.slideDown('fast'); self.keywords === '' && self.$input.val(''); return; } self.search(); }); }, listenBlur: function() { var self = this; this.$input.on('blur', function() { if (self.filterDataList.length === 0) { self.$input.val(self.defaltValue); self.keywords = ''; } else if ($.trim(self.$input.val()) === '') { self.$input.val(self.defaltValue); } }); }, keyboardSelect: function(code) { if (code === 38) { this._highlightIndex--; this._highlightIndex = this._highlightIndex < 0 ? 0 : this._highlightIndex; this.highlight(); } else if (code === 40) { this._highlightIndex++; this._highlightIndex = this._highlightIndex > (this.filterDataList.length - 1) ? (this.filterDataList.length - 1) : this._highlightIndex; this.highlight(); } this._seletedIndex = this._highlightIndex; }, listenSearch: function() { var self = this; this.$input.on('keyup', function(e) { var code = e.keyCode || e.which; self.keywords = $.trim(self.$input.val()); if (code === 38 || code === 40) { // up down select self.keyboardSelect(code); } else if (code === 13 && self._highlightIndex >= 0) { // enter var selectObj = self.filterDataList[self._highlightIndex]; self.$input.val(selectObj.name); self.$container.data('value', selectObj.id); self.options.change && self.options.change(self.$container.data('value')); self.$list.hide(); self.$input.trigger('blur'); } else { self.search(); } }); }, listenTrigger: function() { var self = this; this.$trigger.on('click', function() { var $this = $(this); if (self._isRended && self.filterDataList.length > 0) { self.$list.slideToggle('fast'); } else { self.$input.trigger('focus'); } }); }, listenSelect: function() { var self = this; this.$container.on('click', '[data-value]', function() { var $this = $(this), value = $this.data('value'); self.$input.val($this.text()); self.keywords = $this.text(); self.$list.hide(); self.$container.data('value', value); self.options.change && self.options.change(value); self._seletedIndex = self.$filterList.children().index(this); }); }, listenMouseenter: function() { var self = this; this.$container .on('mouseenter', '[data-value]', function() { var $childs = self.$filterList.children(); var i = self._highlightIndex = $childs.index(this); $childs.removeClass('hover').eq(i).addClass('hover'); }); }, listenBodyClick: function() { var self = this; $('body').on('click', function(e) { if ($(e.target).parents('.custom-select-container')[0] !== self.$container[0]) { self.$list.hide(); } }); }, listenPaste: function() { var self = this; addEvent(this.$input[0], 'paste', function(e) { var clipboardData = e.clipboardData || window.clipboardData; var clipValue = clipboardData.getData('text'); self.keywords = self.getValueAsPaste(clipValue); self.search(); }); }, getValueAsPaste: function(pasteText) { var existingVal = this.$input.val(); var length = existingVal.length; var start = this.getSelectionStart(this.$input[0]); var value = ''; if (start === undefined) return existingVal; if (start > 0) { if (start < length) { value = existingVal.substring(0, start) + pasteText + existingVal.substring(start, length); } else if (start === length) { value = existingVal.substring(0, start) + pasteText; } } else { value = pasteText + existingVal.substring(0, length); } return value; }, getSelectionStart: function(el) { if (el.selectionStart) { return el.selectionStart; } else if (document.selection) { el.focus(); var r = document.selection.createRange(); if (!r) return 0; var re = el.createTextRange(), rc = re.duplicate(); re.moveToBookmark(r.getBookmark()); rc.setEndPoint('EndToStart', re); return rc.text.length; } return 0; } }; window.CustomSelect = CustomSelect; }(jQuery)); $('.custom-select-container').each(function() { new CustomSelect({ container: this, change: function(value) { // value it means id // query data ... // test code alert(value); } }); }); </script> </body> </html>
5、重难点实现
5.1、如何隐藏数据下拉列表(失去焦点)
试过很多种实现方式,如结合focus,blur,mouseenter,mouseleave等事件处理,都很难处理数据下拉列表的隐藏,最终决定在
body上注册事件处理,判断当前元素是否在容器上,如果不是,则隐藏。
5.2、粘贴事件处理的考虑
粘贴事件处理需要判断用户是在搜索框的起始,中间,末尾粘贴文本,这样才能正确的处理用户输入的关键字搜索
PS:插件为是一个构造函数,这里只是一个例子,你也可以将其改造为一个模块(seajs模块),转载请注明出处 博客园杨君华