自定義select控件開發

目的:select下拉框條目太多(上百),當用戶選擇具體項時會浪費用戶不少時間去尋找,所以須要一個搜索框讓用戶輸入關鍵字來匹配列表,便於用戶選擇css

示例圖:html

一、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>

說明:jquery

初始化容器屬性:
data-name: 至關於原始select的name
data-default-value: input文本搜索框的初始化值
data-placeholder: input文本搜索框的佔位值json

textarea:後端

裏面是一個JONS字符串,保存着自定義select的鍵值對,注意裏面的id纔是須要傳遞給後端接口的,而name只是顯示文本app

 

二、實現原理

將用戶輸入的關鍵字用正則去匹配數據,展現匹配後的數據下拉列表,供用戶選擇ide

 

三、重要交互實現點

3.一、用戶點擊(或鼠標聚焦)搜索框,須要顯示全部的數據下拉列表
3.二、用戶每次輸入文本,即當文本框值有改變時,匹配相應的數據列表並展現
3.三、當用戶點擊了數據列表某一項時,即當用戶選擇了
3.四、當用戶在指定的列表項按下enter鍵時,即當用戶選擇了
3.五、當用戶鼠標移動在數據下拉列表上時,能夠經過鍵盤up,down上下鍵來選擇
3.六、當用戶選擇了列表項後,再次點擊(或聚焦)搜索框,須要展現全部數據列表,並高亮顯示所選擇的數據項
3.七、當用戶在搜索框中用鼠標粘貼了關鍵字後,須要顯示匹配的數據列表並展現(此項較複雜,併兼容了ie7,8)函數

注:jQuery在處理paste事件時,event參數並無處理event.clipboardData,即爲undefined,所以須要本身處理事件綁定(兼容ie)this

 

四、示例

<!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.1、如何隱藏數據下拉列表(失去焦點)spa

試過不少種實現方式,如結合focus,blur,mouseenter,mouseleave等事件處理,都很難處理數據下拉列表的隱藏,最終決定在
body上註冊事件處理,判斷當前元素是否在容器上,若是不是,則隱藏。

5.2、粘貼事件處理的考慮

粘貼事件處理須要判斷用戶是在搜索框的起始,中間,末尾粘貼文本,這樣才能正確的處理用戶輸入的關鍵字搜索

 

PS:插件爲是一個構造函數,這裏只是一個例子,你也能夠將其改造爲一個模塊(seajs模塊),轉載請註明出處 博客園楊君華

相關文章
相關標籤/搜索