自定义select控件开发

目的: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({
                            width: 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>
View Code

 

5、重难点实现

5.1、如何隐藏数据下拉列表(失去焦点)

试过很多种实现方式,如结合focus,blur,mouseenter,mouseleave等事件处理,都很难处理数据下拉列表的隐藏,最终决定在
body上注册事件处理,判断当前元素是否在容器上,如果不是,则隐藏。

5.2、粘贴事件处理的考虑

粘贴事件处理需要判断用户是在搜索框的起始,中间,末尾粘贴文本,这样才能正确的处理用户输入的关键字搜索

 

PS:插件为是一个构造函数,这里只是一个例子,你也可以将其改造为一个模块(seajs模块),转载请注明出处 博客园杨君华

 

posted @ 2015-11-09 12:54  杨君华  阅读(3985)  评论(1编辑  收藏  举报