自定义下拉焦点:超出调用堆栈大小

Custom dropdown focus: call stack size exceeded

我正在使用 jquery (slim) 构建我自己的下拉插件。下拉元素本身是 div 和 tabindex="0"

我希望下拉菜单与浏览器的焦点状态一起工作:当元素获得焦点时打开下拉菜单,当元素失去焦点时关闭它。目前我收到以下错误:

jquery.slim.min.js:2 Uncaught RangeError: Maximum call stack size exceeded

代码如下所示(为了便于阅读删除了部分,标记了问题):

var plugin   = 'dropdown',
    defaults = {
        onOpened : function() {},
        onClosed : function() {}
    };

// Constructor
function Dropdown(element, options) {
    this.element  = element;
    this.settings = $.extend({}, defaults, options);
    this.init();
}

// Instance
$.extend(Dropdown.prototype, {

    init: function() {
        var instance = this,
            $element = $(instance.element);

        // Bind listeners
        $element.focus(function(e) {
            instance.open();
            e.preventDefault();
        }).focusout(function() {
            instance.close();
        }).mousedown(function() {
            instance.toggle();
        });
    },

    /**
     * Check the state of the dropdown.
     *
     * @param state
     * @returns {*}
     */
    is: function(state) {
        var $element = $(this.element);

        return {
            open: function() {
                return $element.hasClass('dropdown--open');
            },
            focused: function() {
                return document.activeElement === $element[0];
            }
        }[state].apply();
    },

    /**
     * Open the dropdown.
     */
    open: function() {
        var instance = this,
            $element = $(instance.element);

        if (instance.is('open')) {
            return;
        }

        $element.addClass('dropdown--open');

        this.callback(this.settings.onOpened, $element);
    },

    /**
     * Close the dropdown.
     */
    close: function() {
        var instance = this,
            $element = $(this.element);

        if ( ! instance.is('open')) {
            return;
        }

        $element.removeClass('dropdown--open');

        this.callback(this.settings.onClosed, $element);
    },

    /**
     * Make a callback.
     *
     * @param callback
     * @param $element
     */
    callback: function(callback, $element) {
        if (callback && typeof callback === 'function') {
            callback($element);
        }
    }

});

我知道我正在触发一个(无穷无尽的)递归函数,但我不确定如何解决这个问题。

感谢所有帮助!

编辑: 固定

;(function($, window, document) {
    'use strict';

    var plugin   = 'dropdown',
        defaults = {
            onOpened : function() {},
            onClosed : function() {}
        };

    // Constructor
    function Dropdown(element, options) {
        this.element  = element;
        this.settings = $.extend({}, defaults, options);
        this.init();
    }

    // Instance
    $.extend(Dropdown.prototype, {

        init: function() {
            var instance = this,
                $element = $(instance.element);

            // Bind listeners
            $element.focus(function(e) {
                console.log('opening');
                instance.open();
                e.preventDefault();
            }).focusout(function() {
                console.log('closing');
                instance.close();
            }).mousedown(function() {
                console.log('toggling');
                instance.toggle();
            });
        },

        /**
         * Check the state of the dropdown.
         *
         * @param state
         * @returns {*}
         */
        is: function(state) {
            var $element = $(this.element);

            return {
                open: function() {
                    return $element.hasClass('dropdown--open');
                },
                empty: function() {
                    return $element.hasClass('dropdown--empty');
                },
                focused: function() {
                    return document.activeElement === $element[0];
                }
            }[state].apply();
        },

        /**
         * Toggles the dropdown.
         */
        toggle: function() {
            if  (this.is('open')) this.close();
            else this.open();
        },

        /**
         * Open the dropdown.
         */
        open: function() {
            var instance = this,
                $element = $(instance.element);

            if (instance.is('open')) {
                return;
            }

            $element.addClass('dropdown--open');

            this.callback(this.settings.onOpened, $element);
        },

        /**
         * Close the dropdown.
         */
        close: function() {
            var instance = this,
                $element = $(this.element);

            if ( ! instance.is('open')) {
                return;
            }

            $element.removeClass('dropdown--open');

            this.callback(this.settings.onClosed, $element);
        },

        /**
         * Make a callback.
         *
         * @param callback
         * @param $element
         */
        callback: function(callback, $element) {
            if (callback && typeof callback === 'function') {
                callback($element);
            }
        }

    });

    // Plugin definition
    $.fn.dropdown = function(options, args) {
        return this.each(function() {
            if ( ! $ .data(this, plugin)) {
                $.data(this, plugin, new Dropdown(this, options));
            }
        });
    };
})(jQuery, window, document);

$('.dropdown').dropdown();
.dropdown {
    position: relative;
    display: block;
    padding: .625rem .8125rem;
    padding-right: 2rem;
    font-size: 16px;
    color: #333;
    line-height: 1.125;
    outline: 0;
    cursor: pointer;
    border: 1px solid #d9d9d9;
    background-color: #fff;
}
.dropdown.dropdown--open .dropdown__menu {
    display: block;
}
.dropdown__menu {
    display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="dropdown" tabindex="0">
  <span>Favorite animal</span>
  <ul class="dropdown__menu" tabindex="-1">
    <li class="dropdown__item">Cats</li>
    <li class="dropdown__item">Dogs</li>
    <li class="dropdown__item">Monkeys</li>
    <li class="dropdown__item">Elephants</li>
  </ul>
</div>

所以。问题:

1) 你一次又一次触发 focus()focusout() if Dropdown open/close。 (你已经这样做了)

2) 使用您的 toggle() 函数 close/open 您的下拉列表

你的问题是你有点击事件检查下拉菜单是否打开,然后关闭。但是您必须在 focusOut().

中执行此操作

我编辑了你的fiddle

        // Bind listeners
        $element.on('click', function(e) {
            instance.toggle();
        });

3) 根据您的评论更新

Fiddle 更改值