jquery.combo.select.js对下拉框的支持比较友好,但是公司业务需要智能筛选下拉框的值,不能输入….(其实控件本身也不支持)

于是优化了下,优化内容如下:

  1. 输入内容,失去焦点后自动选择筛选出的第一个值
  2. 若未筛选出值,则选择默认的第一个值
  3. 源控制不能选择value为""的值,优化为可选.

贴上完整js代码:

/*jshint asi:true, expr:true */
/**
 * Plugin Name: Combo Select
 * Author : Vinay@Pebbleroad
 * Date: 23/11/2014
 * Description: 
 * Converts a select box into a searchable and keyboard friendly interface. Fallbacks to native select on mobile and tablets
 */

// Expose plugin as an AMD module if AMD loader is present:
(function (factory) {
    \\\'use strict\\\';
    if (typeof define === \\\'function\\\' && define.amd) {
        // AMD. Register as an anonymous module.
        define([\\\'jquery\\\'], factory);
    } else if (typeof exports === \\\'object\\\' && typeof require === \\\'function\\\') {
        // Browserify
        factory(require(\\\'jquery\\\'));
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ( $, undefined ) {

    var pluginName = "comboSelect",
        dataKey = \\\'comboselect\\\';
    var defaults = {            
        comboClass         : \\\'combo-select\\\',
        comboArrowClass    : \\\'combo-arrow\\\',
        comboDropDownClass : \\\'combo-dropdown\\\',
        inputClass         : \\\'combo-input text-input\\\',
        disabledClass      : \\\'option-disabled\\\',
        hoverClass         : \\\'option-hover\\\',
        selectedClass      : \\\'option-selected\\\',
        markerClass        : \\\'combo-marker\\\',
        themeClass         : \\\'\\\',
        maxHeight          : 200,
        extendStyle        : true,
        focusInput         : true
    };

    /**
     * Utility functions
     */

    var keys = {
        ESC: 27,
        TAB: 9,
        RETURN: 13,
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40,
        ENTER: 13,
        SHIFT: 16
    },  
    isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));

    /**
     * Constructor
     * @param {[Node]} element [Select element]
     * @param {[Object]} options [Option object]
     */
    function Plugin ( element, options ) {

        /* Name of the plugin */

        this._name = pluginName;

        /* Reverse lookup */

        this.el = element

        /* Element */

        this.$el = $(element)

        /* If multiple select: stop */

        if(this.$el.prop(\\\'multiple\\\')) return;

        /* Settings */

        this.settings = $.extend( {}, defaults, options, this.$el.data() );

        /* Defaults */

        this._defaults = defaults;

        /* Options */

        this.$options = this.$el.find(\\\'option, optgroup\\\')

        /* Initialize */

        this.init();

        /* Instances */

        $.fn[ pluginName ].instances.push(this);

    }

    $.extend(Plugin.prototype, {
        init: function () {

            /* Construct the comboselect */

            this._construct();


            /* Add event bindings */          

            this._events();


        },
        _construct: function(){

            var self = this

            /**
             * Add negative TabIndex to `select`
             * Preserves previous tabindex
             */

            this.$el.data(\\\'plugin_\\\'+ dataKey + \\\'_tabindex\\\', this.$el.prop(\\\'tabindex\\\'))

            /* Add a tab index for desktop browsers */

            !isMobile && this.$el.prop("tabIndex", -1)

            /**
             * Wrap the Select
             */

            this.$container = this.$el.wrapAll(\\\'<div class="\\\' + this.settings.comboClass + \\\' \\\'+ this.settings.themeClass + \\\'" />\\\').parent();

            /**
             * Check if select has a width attribute
             */
            if(this.settings.extendStyle && this.$el.attr(\\\'style\\\')){

                this.$container.attr(\\\'style\\\', this.$el.attr("style"))

            }


            /**
             * Append dropdown arrow
             */

            this.$arrow = $(\\\'<div class="\\\'+ this.settings.comboArrowClass+ \\\'" />\\\').appendTo(this.$container)


            /**
             * Append dropdown
             */

            this.$dropdown = $(\\\'<ul class="\\\'+this.settings.comboDropDownClass+\\\'" />\\\').appendTo(this.$container)


            /**
             * Create dropdown options
             */

            var o = \\\'\\\', k = 0, p = \\\'\\\';

            this.selectedIndex = this.$el.prop(\\\'selectedIndex\\\')

            this.$options.each(function(i, e){

                if(e.nodeName.toLowerCase() == \\\'optgroup\\\'){

                    return o+=\\\'<li class="option-group">\\\'+this.label+\\\'</li>\\\'
                }

                if(!e.value) p = e.innerHTML

                o+=\\\'<li class="\\\'+(this.disabled? self.settings.disabledClass : "option-item") + \\\' \\\' +(k == self.selectedIndex? self.settings.selectedClass : \\\'\\\')+ \\\'" data-index="\\\'+(k)+\\\'" data-value="\\\'+this.value+\\\'">\\\'+ (this.innerHTML) + \\\'</li>\\\'

                k++;
            })

            this.$dropdown.html(o)

            /**
             * Items
             */

            this.$items = this.$dropdown.children();


            /**
             * Append Input
             */

            this.$input = $(\\\'<input type="text"\\\' + (isMobile? \\\'tabindex="-1"\\\': \\\'\\\') + \\\' placeholder="\\\'+p+\\\'" class="\\\'+ this.settings.inputClass + \\\'">\\\').appendTo(this.$container)

            /* Update input text */

            this._updateInput()

        },

        _events: function(){

            /* Input: focus */

            this.$container.on(\\\'focus.input\\\', \\\'input\\\', $.proxy(this._focus, this))

            /**
             * Input: mouseup
             * For input select() event to function correctly
             */
            this.$container.on(\\\'mouseup.input\\\', \\\'input\\\', function(e){
                e.preventDefault()
            })

            /* Input: blur */

            this.$container.on(\\\'blur.input\\\', \\\'input\\\', $.proxy(this._blur, this))

            /* Select: change */

            this.$el.on(\\\'change.select\\\', $.proxy(this._change, this))

            /* Select: focus */

            this.$el.on(\\\'focus.select\\\', $.proxy(this._focus, this))

            /* Select: blur */

            this.$el.on(\\\'blur.select\\\', $.proxy(this._blurSelect, this))

            /* Dropdown Arrow: click */

            this.$container.on(\\\'click.arrow\\\', \\\'.\\\'+this.settings.comboArrowClass , $.proxy(this._toggle, this))

            /* Dropdown: close */

            this.$container.on(\\\'comboselect:close\\\', $.proxy(this._close, this))

            /* Dropdown: open */

            this.$container.on(\\\'comboselect:open\\\', $.proxy(this._open, this))


            /* HTML Click */

            $(\\\'html\\\').off(\\\'click.comboselect\\\').on(\\\'click.comboselect\\\', function(){

                $.each($.fn[ pluginName ].instances, function(i, plugin){

                    plugin.$container.trigger(\\\'comboselect:close\\\')

                })
            });

            /* Stop `event:click` bubbling */

            this.$container.on(\\\'click.comboselect\\\', function(e){
                e.stopPropagation();
            })


            /* Input: keydown */

            this.$container.on(\\\'keydown\\\', \\\'input\\\', $.proxy(this._keydown, this))

            /* Input: keyup */

            this.$container.on(\\\'keyup\\\', \\\'input\\\', $.proxy(this._keyup, this))

            /* Dropdown item: click */

            this.$container.on(\\\'click.item\\\', \\\'.option-item\\\', $.proxy(this._select, this))

        },

        _keydown: function(event){



            switch(event.which){

                case keys.UP:
                    this._move(\\\'up\\\', event)
                    break;

                case keys.DOWN:
                    this._move(\\\'down\\\', event)
                    break;

                case keys.TAB:
                    this._enter(event)
                    break;

                case keys.RIGHT:
                    this._autofill(event);
                    break;

                case keys.ENTER:
                    this._enter(event);
                    break;

                default:                            
                    break;


            }

        },


        _keyup: function(event){

            switch(event.which){
                case keys.ESC:                                                  
                    this.$container.trigger(\\\'comboselect:close\\\')
                    break;

                case keys.ENTER:
                case keys.UP:
                case keys.DOWN:
                case keys.LEFT:
                case keys.RIGHT:
                case keys.TAB:
                case keys.SHIFT:                            
                    break;

                default:                            
                    this._filter(event.target.value)
                    break;
            }
        },

        _enter: function(event){

            var item = this._getHovered()

            item.length && this._select(item);

            /* Check if it enter key */
            if(event && event.which == keys.ENTER){

                if(!item.length) {

                    /* Check if its illegal value */

                    this._blur();

                    return true;
                }

                event.preventDefault();
            }


        },
        _move: function(dir){

            var items = this._getVisible(),
                current = this._getHovered(),
                index = current.prevAll(\\\'.option-item\\\').filter(\\\':visible\\\').length,
                total = items.length


            switch(dir){
                case \\\'up\\\':
                    index--;
                    (index < 0) && (index = (total - 1));
                    break;

                case \\\'down\\\':                          
                    index++;
                    (index >= total) && (index = 0);                            
                    break;
            }


            items
                .removeClass(this.settings.hoverClass)
                .eq(index)
                .addClass(this.settings.hoverClass)


            if(!this.opened) this.$container.trigger(\\\'comboselect:open\\\');

            this._fixScroll()
        },

        _select: function(event){

            var item = event.currentTarget? $(event.currentTarget) : $(event);

            if(!item.length) return;

            /**
             * 1. get Index
             */

            var index = item.data(\\\'index\\\');

            this._selectByIndex(index);

            this.$container.trigger(\\\'comboselect:close\\\')                 

        },

        _selectByIndex: function(index){

            /**
             * Set selected index and trigger change
             * @type {[type]}
             */
            if(typeof index == \\\'undefined\\\'){

                index = 0

            }

            // if(this.$el.prop(\\\'selectedIndex\\\') != index){
                this.$el.prop(\\\'selectedIndex\\\', index).trigger(\\\'change\\\');
            // }

        },

        _autofill: function(){

            var item = this._getHovered();

            if(item.length){

                var index = item.data(\\\'index\\\')

                this._selectByIndex(index)

            }

        },


        _filter: function(search){

            var self = this,
                items = this._getAll();
                needle = $.trim(search).toLowerCase(),
                reEscape = new RegExp(\\\'(\\\\\\\\\\\' + [\\\'/\\\', \\\'.\\\', \\\'*\\\', \\\'+\\\', \\\'?\\\', \\\'|\\\', \\\'(\\\', \\\')\\\', \\\'[\\\', \\\']\\\', \\\'{\\\', \\\'}\\\', \\\'\\\\\\\\\\\'].join(\\\'|\\\\\\\\\\\') + \\\')\\\', \\\'g\\\'),
                pattern = \\\'(\\\' + search.replace(reEscape, \\\'\\\\\\\\$1\\\') + \\\')\\\';


            /**
             * Unwrap all markers
             */

            $(\\\'.\\\'+self.settings.markerClass, items).contents().unwrap();

            /* Search */

            if(needle){

                /* Hide Disabled and optgroups */

                this.$items.filter(\\\'.option-group, .option-disabled\\\').hide();


                items                           
                    .hide()
                    .filter(function(){

                        var $this = $(this),
                            text = $.trim($this.text()).toLowerCase();

                        /* Found */
                        if(text.toString().indexOf(needle) != -1){

                            /**
                             * Wrap the selection
                             */                                 

                            $this
                                .html(function(index, oldhtml){
                                return oldhtml.replace(new RegExp(pattern, \\\'gi\\\'), \\\'<span class="\\\'+self.settings.markerClass+\\\'">$1</span>\\\')
                            })                                  

                            return true
                        }

                    })
                    .show()
            }else{


                this.$items.show();
            }

            /* Open the comboselect */

            this.$container.trigger(\\\'comboselect:open\\\')


        },

        _highlight: function(){

            /* 
            1. Check if there is a selected item 
            2. Add hover class to it
            3. If not add hover class to first item
            */

            var visible = this._getVisible().removeClass(this.settings.hoverClass),
                $selected = visible.filter(\\\'.\\\'+this.settings.selectedClass)

            if($selected.length){

                $selected.addClass(this.settings.hoverClass);

            }else{

                visible
                    .removeClass(this.settings.hoverClass)
                    .first()
                    .addClass(this.settings.hoverClass)
            }

        },

        _updateInput: function(){

            var selected = this.$el.prop(\\\'selectedIndex\\\')
            // if(this.$el.val()){

                text = this.$el.find(\\\'option\\\').eq(selected).text()

                this.$input.val(text)

            // }else{

                //this.$input.val(\\\'\\\')

            // }

            return this._getAll()
                .removeClass(this.settings.selectedClass)
                .filter(function(){

                    return $(this).data(\\\'index\\\') == selected
                })
                .addClass(this.settings.selectedClass)

        },
        _blurSelect: function(){

            this.$container.removeClass(\\\'combo-focus\\\');

        },
        _focus: function(event){

            /* Toggle focus class */

            this.$container.toggleClass(\\\'combo-focus\\\', !this.opened);

            /* If mobile: stop */

            if(isMobile) return;

            /* Open combo */

            if(!this.opened) this.$container.trigger(\\\'comboselect:open\\\');

            /* Select the input */

            this.settings.focusInput && event && event.currentTarget && event.currentTarget.nodeName == \\\'INPUT\\\' && event.currentTarget.select()
        },

        _blur: function(){

            /**
             * 1. Get hovered item
             * 2. If not check if input value == select option
             * 3. If none
             */

            var val = $.trim(this.$input.val().toLowerCase()),
                isNumber = !isNaN(val);

            var index = this.$options.filter(function(){

                if(isNumber){
                    return parseInt($.trim(this.innerHTML).toLowerCase()) == val
                }

                return $.trim(this.innerHTML).toLowerCase() == val

            }).prop(\\\'index\\\')

            /* Select by Index */
            if(index==undefined){
                if(this._getVisible().length==0){
                    index=0;
                }else{
                    index=this._getVisible().index();
                }
            }
            this._selectByIndex(index)

        },

        _change: function(){

            this._updateInput();

        },

        _getAll: function(){

            return this.$items.filter(\\\'.option-item\\\')

        },
        _getVisible: function(){

            return this.$items.filter(\\\'.option-item\\\').filter(\\\':visible\\\')

        },

        _getHovered: function(){

            return this._getVisible().filter(\\\'.\\\' + this.settings.hoverClass);

        },

        _open: function(){

            var self = this

            this.$container.addClass(\\\'combo-open\\\')           

            this.opened = true

            /* Focus input field */         

            this.settings.focusInput && setTimeout(function(){ !self.$input.is(\\\':focus\\\') && self.$input.focus(); });

            /* Highligh the items */

            this._highlight()

            /* Fix scroll */

            this._fixScroll()

            /* Close all others */


            $.each($.fn[ pluginName ].instances, function(i, plugin){

                if(plugin != self && plugin.opened) plugin.$container.trigger(\\\'comboselect:close\\\')
            })

        },

        _toggle: function(){

            this.opened? this._close.call(this) : this._open.call(this)
        },

        _close: function(){             

            this.$container.removeClass(\\\'combo-open combo-focus\\\')

            this.$container.trigger(\\\'comboselect:closed\\\')

            this.opened = false

            /* Show all items */

            this.$items.show();

        },
        _fixScroll: function(){

            /**
             * If dropdown is hidden
             */
            if(this.$dropdown.is(\\\':hidden\\\')) return;


            /**
             * Else                  
             */
            var item = this._getHovered();

            if(!item.length) return;                    

            /**
             * Scroll
             */

            var offsetTop,
                upperBound,
                lowerBound,
                heightDelta = item.outerHeight()

            offsetTop = item[0].offsetTop;

            upperBound = this.$dropdown.scrollTop();

            lowerBound = upperBound + this.settings.maxHeight - heightDelta;

            if (offsetTop < upperBound) {

                this.$dropdown.scrollTop(offsetTop);

            } else if (offsetTop > lowerBound) {

                this.$dropdown.scrollTop(offsetTop - this.settings.maxHeight + heightDelta);
            }

        },
        /**
         * Destroy API
         */

        dispose: function(){

            /* Remove combo arrow, input, dropdown */

            this.$arrow.remove()

            this.$input.remove()

            this.$dropdown.remove()

            /* Remove tabindex property */
            this.$el
                .removeAttr("tabindex")

            /* Check if there is a tabindex set before */

            if(!!this.$el.data(\\\'plugin_\\\'+ dataKey + \\\'_tabindex\\\')){
                this.$el.prop(\\\'tabindex\\\', this.$el.data(\\\'plugin_\\\'+ dataKey + \\\'_tabindex\\\'))
            }

            /* Unwrap */

            this.$el.unwrap()

            /* Remove data */

            this.$el.removeData(\\\'plugin_\\\'+dataKey)

            /* Remove tabindex data */

            this.$el.removeData(\\\'plugin_\\\'+dataKey + \\\'_tabindex\\\')

            /* Remove change event on select */

            this.$el.off(\\\'change.select focus.select blur.select\\\');

        }   
    });



    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[ pluginName ] = function ( options, args ) {

        this.each(function() {

            var $e = $(this),
                instance = $e.data(\\\'plugin_\\\'+dataKey)

            if (typeof options === \\\'string\\\') {

                if (instance && typeof instance[options] === \\\'function\\\') {
                        instance[options](args);
                }

            }else{

                if (instance && instance.dispose) {
                        instance.dispose();
                }

                $.data( this, "plugin_" + dataKey, new Plugin( this, options ) );

            }

        });

        // chain jQuery functions
        return this;
    };

    $.fn[ pluginName ].instances = [];

}));