JasonK JasonK - 1 month ago 15
jQuery Question

Custom dropdown focus: call stack size exceeded

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>




Answer

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