0

I'm working on my own dropdown plugin built with jquery (slim). The dropdown element itself is a div with tabindex="0".

I'd like the dropdown to work with the browser's focus state: open the dropdown when the element is focused and close it when the element loses focus. Currently I'm getting the following error:

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

The code looks like this (removed parts for readability, marked problems):

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);
        }
    }

});

I know I'm triggering a (endless) recursive function, but I'm unsure how to tackle this problem.

All help is appreciated!

Edit: Fixed

;(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>
JasonK
  • 5,214
  • 9
  • 33
  • 61
  • 1
    Sorry, but why you want to set `$element.focus()` again if it already focused?! – pleinx Oct 14 '16 at 12:36
  • @pleinx At what part in the code are you referring to? In the `open` method I first check if the input isn't focused yet. – JasonK Oct 14 '16 at 12:42
  • 1
    You open/close your dropdown in your init function (binding focus/ focusout) handler. In your close/open function for this example close-function: You check is "focused" and trigger again `focusout()` which again trigger close() (endless while). So i dont understand why you check here is "focused" within focusout() function? that cant be possible. Can you please try to remove in your close/open functions the line which trigger again `focus()` and `focusout()` – pleinx Oct 14 '16 at 12:49
  • @pleinx Thanks a bunch... I see the stupid mistake now. This still leaves me with the problem that clicking the dropdown (to close it) causes weird behaviour. Could you point me in the right direction? How would I close the dropdown when it is open and a user clicks on it. I've edited the code. – JasonK Oct 14 '16 at 13:04
  • @pleinx I've added a fiddle to the post which shows the problem. Everything works fine when you remove the `.click()` handler. – JasonK Oct 14 '16 at 13:19
  • yes sure, because you close your dropdown if clicking on it (and its open). You can use focus or click (both is okay) in my answer is only a recommendation. I find focus not so nice because it will triggered while clicking (means not finished click like keyPress) – pleinx Oct 14 '16 at 13:34

1 Answers1

1

So. The Problems:

1) You triggered again and again focus()and focusout() if Dropdown open/close. (You already done this)

2) Use your toggle() function to close/open your dropdown

Your Problem was you have click event which checks is dropdown open, then close. But you have todo this in focusOut().

I edited your fiddle

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

3) Update from your comment

Fiddle with changing the values

pleinx
  • 616
  • 3
  • 8
  • Thanks for your answer. I think my explanation was unclear: I want the dropdown to open/close when the user clicks on the dropdown (no mouseenter event). Somehow 'focus' and 'close' get logged at the same time. – JasonK Oct 14 '16 at 13:32
  • edited/added again the focus/focusout version :) But my recommendation to use `click` event for open the dropdown i will continue to represent :P btw: sorry i added a small animation to hide/show the dropdown, this was just for me, sorry hehe – pleinx Oct 14 '16 at 13:36
  • I've edited my fiddle, using the `mousedown` handler was the solution as seen here http://stackoverflow.com/questions/8735764/prevent-firing-focus-event-when-clicking-on-div . Still upvoted your question :-). See my edited fiddle. – JasonK Oct 14 '16 at 13:46
  • @JasonK so now i understand, in relation to your edited fiddle, what you want to get. Open/Close on clicking on "Favorite Animate" and close if user clicking on dropdown. So sorry, missunderstanding hehe. I updated Fiddle (version5) you only need one event - the click event to handle all these. – pleinx Oct 14 '16 at 13:56
  • Not really :/ I want the dropdown to work exactly like the first (gender) dropdown here: http://semantic-ui.com/modules/dropdown.html#/usage (this also works with using the `tab` key). Look at my fiddle, I'm close but not there yet. 'opening' and 'toggling' are logging at the same time. – JasonK Oct 14 '16 at 14:01
  • thats what is currently do in my last fiddle. Or you mean with changing the values??? – pleinx Oct 14 '16 at 14:25