3
\$\begingroup\$

I'm working on my first jQuery plugin for a lightweight dropdown. This dropdown behaves just like a ordinary <select>.

Before implementing new features I'd like to know if my current approach is clean and efficient (I care about readability and performance).

/**
 * Dropdown plugin.
 *
 * @license MIT (https://opensource.org/licenses/MIT)
 */
;(function($, window, document) {
    'use strict';

    var plugin   = 'dropdown',
        methods  = ['open', 'close', 'toggle'],
        defaults = {
            onOpen  : function() {},
            onClose : function() {},
            classNames : {
                dropdown : 'dropdown',
                menu     : 'dropdown__menu',
                item     : 'dropdown__item',
                open     : 'dropdown--open',
                empty    : 'dropdown--empty'
            }
        };

    // 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),
                $items   = $element.find('.' + instance.settings.classNames.item);

            // Check if there already is a selection
            if ($items.filter('.selected').length === 0) {
                $element.addClass(instance.settings.classNames.empty);
            }

            // Bind listeners
            $element
                .mousedown(function() {
                    instance.toggle();
                })
                .keydown(function(e) {
                    switch (e.which) {
                        case 13 : // enter key
                            instance.toggle();
                            break;
                        case 38 : // arrow up
                            instance.select('prev');
                            e.preventDefault(); // prevent scroll
                            break;
                        case 40 : // arrow down
                            instance.select('next');
                            e.preventDefault(); // prevent scroll
                            break;
                    }
                })
                .focusout(function() {
                    instance.close();
                });

            $items
                .mousedown(function(e) {
                    instance.select(e.target);
                });
        },

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

            return {
                open: function() {
                    return $element.hasClass(instance.settings.classNames.open);
                },
                empty: function() {
                    return $element.hasClass(instance.settings.classNames.empty);
                }
            }[state].apply();
        },

        /**
         * Select a dropdown item.
         *
         * @param item
         */
        select: function(item) {
            var instance  = this,
                $element  = $(this.element),
                $items    = $element.find('.' + instance.settings.classNames.item),
                $selected = $items.filter('.selected'),
                $target;

            // Check if an element is passed
            if (typeof item === 'object') {
                $target = $(item);
            } else {
                // Check if we have a selection
                if ($selected && $selected.length > 0) {
                    if (item === 'next') {
                        $target = $selected.next();
                    } else if (item === 'prev') {
                        $target = $selected.prev();
                    }
                } else {
                    $target = $items.first();
                }
            }

            // Ensure the target is set and a different element the selected item
            if ( ! $target || $target.length === 0 || $target[0].isEqualNode($selected[0])) {
                return false;
            }

            // Set classes
            $element.removeClass(instance.settings.classNames.empty);
            $selected.removeClass('selected');
            $target.addClass('selected');

            // Set values
            $element.find('input, select').val( $target.data('value') );
            $element.find('span').first().text( $target.text() );
        },

        /**
         * 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(instance.settings.classNames.open);

            instance.settings.onOpen.call($element[0]);
        },

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

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

            $element.removeClass(instance.settings.classNames.open);

            instance.settings.onClose.call($element[0]);
        }

    });

    // Plugin definition
    $.fn.dropdown = function(options, args) {
        return this.each(function() {
            var $el  = this,
                data = $.data($el, plugin);

            // Prevent multiple instantiations
            if ( ! data) {
                $.data($el, plugin, new Dropdown($el, options));
            }
            else if (typeof options === 'string') {
                // Attempting to call public method
                if (data[options] && options.indexOf(methods) === -1) {
                    data[options].apply(data, $.isArray(args) ? args : new Array(args));
                } else {
                    console.error(plugin + ': Trying to call a undefined or inaccessible method')
                }
            }
        });
    };
})(jQuery, window, document);

See it in action: Navigational control with the arrows keys seem to not work in the fiddle for some reason. This should work tho.

/**
 * Dropdown plugin.
 *
 * @package Fundament
 * @license MIT (https://opensource.org/licenses/MIT)
 */
;(function($, window, document) {
    'use strict';

    var plugin   = 'dropdown',
        methods  = ['open', 'close', 'toggle'],
        defaults = {
            onOpen  : function() {},
            onClose : function() {},
            classNames : {
                dropdown : 'dropdown',
                menu     : 'dropdown__menu',
                item     : 'dropdown__item',
                open     : 'dropdown--open',
                empty    : 'dropdown--empty'
            }
        };

    // 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),
                $items   = $element.find('.' + instance.settings.classNames.item);

            // Check if there already is a selection
            if ($items.filter('.selected').length === 0) {
                $element.addClass(instance.settings.classNames.empty);
            }

            // Bind listeners
            $element
                .mousedown(function() {
                    instance.toggle();
                })
                .keydown(function(e) {
                    switch (e.which) {
                        case 13 : // enter key
                            instance.toggle();
                            break;
                        case 38 : // arrow up
                            instance.select('prev');
                            e.preventDefault(); // prevent scroll
                            break;
                        case 40 : // arrow down
                            instance.select('next');
                            e.preventDefault(); // prevent scroll
                            break;
                    }
                })
                .focusout(function() {
                    instance.close();
                });

            $items
                .mousedown(function(e) {
                    instance.select(e.target);
                });
        },

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

            return {
                open: function() {
                    return $element.hasClass(instance.settings.classNames.open);
                },
                empty: function() {
                    return $element.hasClass(instance.settings.classNames.empty);
                }
            }[state].apply();
        },

        /**
         * Select a dropdown item.
         *
         * @param item
         */
        select: function(item) {
            var instance  = this,
                $element  = $(this.element),
                $items    = $element.find('.' + instance.settings.classNames.item),
                $selected = $items.filter('.selected'),
                $target;

            // Check if an element is passed
            if (typeof item === 'object') {
                $target = $(item);
            } else {
                // Check if we have a selection
                if ($selected && $selected.length > 0) {
                    if (item === 'next') {
                        $target = $selected.next();
                    } else if (item === 'prev') {
                        $target = $selected.prev();
                    }
                } else {
                    $target = $items.first();
                }
            }

            // Ensure the target is set and a different element the selected item
            if ( ! $target || $target.length === 0 || $target[0].isEqualNode($selected[0])) {
                return false;
            }

            // Set classes
            $element.removeClass(instance.settings.classNames.empty);
            $selected.removeClass('selected');
            $target.addClass('selected');

            // Set values
            $element.find('input, select').val( $target.data('value') );
            $element.find('span').first().text( $target.text() );
        },

        /**
         * 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(instance.settings.classNames.open);

            instance.settings.onOpen.call($element[0]);
        },

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

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

            $element.removeClass(instance.settings.classNames.open);

            instance.settings.onClose.call($element[0]);
        }

    });

    // Plugin definition
    $.fn.dropdown = function(options, args) {
        return this.each(function() {
            var $el  = this,
                data = $.data($el, plugin);

            // Prevent multiple instantiations
            if ( ! data) {
                $.data($el, plugin, new Dropdown($el, options));
            }
            else if (typeof options === 'string') {
                // Attempting to call public method
                if (data[options] && options.indexOf(methods) === -1) {
                    data[options].apply(data, $.isArray(args) ? args : new Array(args));
                } else {
                    console.error(plugin + ': Trying to call a undefined or inaccessible method')
                }
            }
        });
    };
})(jQuery, window, document);

$('.dropdown').dropdown();
.dropdown{position:relative;display:block;padding:.625rem .8125rem;padding-right:2rem;font-family:Source Sans Pro,sans-serif;font-size:16px;color:#333;line-height:1.125;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;cursor:pointer;border:1px solid #d9d9d9;border-radius:.25rem;background-color:#fff;-webkit-transition:all .25s ease-out;transition:all .25s ease-out}.dropdown>span{pointer-events:none}.dropdown>select{display:none!important}.dropdown:focus{border-color:#49a2bd}.dropdown:after{content:" ";position:absolute;top:50%;right:1rem;margin-top:-2.5px;width:0;height:0;border-color:#d9d9d9;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid}.dropdown.dropdown--empty{color:#b3b3b3}.dropdown.dropdown--open{border-color:#49a2bd;border-bottom-left-radius:0;border-bottom-right-radius:0}.dropdown.dropdown--open:after{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.dropdown.dropdown--open .dropdown__menu{display:block;border-color:#49a2bd}.dropdown__menu{display:none;position:absolute;top:100%;left:-1px;right:-1px;z-index:1;margin:0;padding:0;max-height:300px;overflow-y:auto;outline:0;list-style:none;border:1px solid #d9d9d9;border-top:0;border-radius:0 0 .25rem .25rem;background-color:#fff;box-shadow:0 3px 4px rgba(0,0,0,.15)}.dropdown__item{display:block;padding:.625rem .8125rem;color:#333;vertical-align:middle;line-height:1.125;border-top:1px solid rgba(0,0,0,.04)}.dropdown__item>.icon{margin-right:.5rem}.dropdown__item.selected{font-weight:700;background-color:rgba(0,0,0,.01)}.dropdown__item:hover{background-color:rgba(0,0,0,.03)}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="dropdown">
  <input type="hidden" name="foo">
  <span>Placeholder</span>
  <ul class="dropdown__menu">
    <li class="dropdown__item" data-value="1">First option</li>
    <li class="dropdown__item" data-value="2">Second option</li>
    <li class="dropdown__item" data-value="3">Third option</li>
    <li class="dropdown__item" data-value="4">Fourth option</li>
    <li class="dropdown__item" data-value="5">Fifth option</li>
  </ul>
</div>

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.