floriancapelle/jquery-flex-modal

View on GitHub
jquery.flex-modal.js

Summary

Maintainability
A
2 hrs
Test Coverage
/*! jQuery FlexModal - v2.0.0
 * https://github.com/floriancapelle/jquery-flex-modal
 * Licensed MIT
 */
(function ( root, factory ) {
    if ( typeof define === 'function' && define.amd ) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else {
        // Browser globals
        factory(jQuery);
    }
}(this, function ( $ ) {
    'use strict';

    // Module Namespace
    var NAMESPACE = 'flexModal';

    /**
     * Configuration
     * @see https://github.com/floriancapelle/jquery-flex-modal/blob/master/README.md for configuration details
     */
    var defaults = {
        triggerSelector: '[data-modal-target]',
        triggerTargetKey: 'modalTarget',
        modalOptions: {
            closeBtnMarkup: '<button class="flex-modal-item__close" type="button">x</button>',
            autoCloseOthers: true,
            closeOnOverlayClick: true,
            closeOnEscKey: true
        }
    };

    // Private state
    var $root;
    var CLASS_MODAL_ITEM_HIDDEN = 'flex-modal-hide';
    var CLASS_MODAL_ITEM = 'flex-modal-item';
    var CLASS_MODAL_ITEM_MODIFIER_READY = 'flex-modal-item--ready';
    var CLASS_MODAL_ITEM_MODIFIER_VISIBLE = 'flex-modal-item--visible';
    var CLASS_MODAL_ITEM_CONTENT = 'flex-modal-item__content';
    var CLASS_MODAL_ITEM_CONTENT_INNER = 'flex-modal-item__content-inner';
    var CLASS_MODAL_ITEM_CLOSE = 'flex-modal-item__close';
    var MODAL_ITEM_TPL = '<article class="flex-modal-item"><div class="flex-modal-item__content"><div class="flex-modal-item__content-inner"></div></div></article>';

    $(function() {
        // create wrapper and append to configured element
        var $body = $('body');
        $root = $('<aside class="flex-modal"></aside>');

        // append wrapper to body
        $body.append($root);

        // trigger event handling
        // open target modal on click on a trigger
        $body.on('click.' + NAMESPACE, function( event ) {
            var $trigger = $(event.target);
            var modalId;

            if ( !$trigger.is(defaults.triggerSelector) ) return;

            if ( typeof defaults.triggerTargetKey === 'function' ) {
                modalId = defaults.triggerTargetKey().call($trigger, event);
            } else {
                modalId = $trigger.data(defaults.triggerTargetKey);
            }

            moduleApi.open(modalId);
        });

        // esc key handling
        $(document).on('keydown.' + NAMESPACE, function( event ) {
            if ( event.keyCode !== 27 ) return;

            // close all visible modals if the escape key has been pressed
            $root.children('.' + CLASS_MODAL_ITEM_MODIFIER_VISIBLE).each(function() {
                var $modal = $(this);

                // close modal if the option is set correctly
                if ( $modal.data('options').closeOnEscKey === true ) {
                    moduleApi.close($modal.attr('id'));
                }
            });
        });

        // modal item event handling
        $root.on('click.' + NAMESPACE, function( event ) {
            var $evtTarget = $(event.target);

            // close modal on click on overlay
            if ( $evtTarget.hasClass(CLASS_MODAL_ITEM) ) {
                if ( $evtTarget.data('options').closeOnOverlayClick !== true ) return;
                moduleApi.close($evtTarget.attr('id'));
            }
            // close modal on click on close btn
            else if ( $evtTarget.hasClass(CLASS_MODAL_ITEM_CLOSE) || $evtTarget.closest('.' + CLASS_MODAL_ITEM_CLOSE).length ) {
                moduleApi.close($evtTarget.closest('.' + CLASS_MODAL_ITEM).attr('id'));
            }
        });

        // expose public api as soon as the document is ready
        $.flexModal = moduleApi;
    });

    /**
     * Module api
     */
    var moduleApi = {

        /**
         * Add a modal by appending the source modal content to a new modal item.
         * @param {string} modalId - modal id like '#modal', will be used as jQuery selector
         * @param {{}|function} [options] - additional options or callback
         * @property {function} options.afterInit - callback to run when modal has been initialized, 'this' will be the modal element
         * @property {{}} options.modalOptions - custom options for this modal only
         * @returns {boolean} - whether the initialization was successful or not
         */
        add: function( modalId, options ) {
            modalId = modalId || '';
            modalId = sanitizeId(modalId);
            if ( $.isFunction(options) ) {
                options = {
                    cb: options
                };
            } else {
                options = options || {};
            }

            var $sourceModal = $('#'+ modalId);
            if ( !$sourceModal.length ) return false;

            var modalContent = $sourceModal.html();
            if ( modalContent === undefined ) return false;

            // create a new modal item
            var $newModal = $(MODAL_ITEM_TPL);
            var $newModalContent = $newModal.children('.' + CLASS_MODAL_ITEM_CONTENT);
            // merge options with defaults and options on the source modal tag if defined
            // like: data-close-btn-markup="false" => closeBtnMarkup: false
            var modalOptions = $.extend(true, {}, defaults.modalOptions, $sourceModal.data(), options.modalOptions);

            // set id and options for later use
            $newModal.attr('id', modalId);
            $newModal.data('options', modalOptions);
            // append the source markup to the new item content
            $newModalContent.children('.' + CLASS_MODAL_ITEM_CONTENT_INNER).append(modalContent);

            if ( modalOptions.closeBtnMarkup ) {
                $newModalContent.append($(modalOptions.closeBtnMarkup));
            }
            // copy all classes from target modal to new modal item
            // except the hide class
            $newModal.addClass($sourceModal.attr('class').replace(CLASS_MODAL_ITEM_HIDDEN, ''));

            $sourceModal.remove();
            $root.append($newModal);
            if ( $.isFunction(options.cb) ) {
                options.cb.call($newModal[0], this);
            }

            return true;
        },

        /**
         * Open a modal
         * @param {string} modalId - with or without leading hash supported
         * @returns {{}}
         */
        open: function( modalId ) {
            modalId = modalId || '';
            modalId = sanitizeId(modalId);

            var self = this;
            var $modal = $root.children('#' + modalId);
            // if the modal is not initialized yet, do it and open it afterwards
            if ( !$modal.length ) {
                if ( moduleApi.add(modalId) === true ) {
                    moduleApi.open(modalId);
                }
                return this;
            }

            var options = $modal.data('options');

            if ( options.autoCloseOthers === true ) {
                // close every child modal that's visible
                $root.children('.' + CLASS_MODAL_ITEM_MODIFIER_VISIBLE).each(function() {
                    moduleApi.close($(this).attr('id'));
                });
            }

            // wait for transitionend event to remove the ready class
            $modal.on('transitionend.open.' + NAMESPACE + ' webkitTransitionEnd.open.' + NAMESPACE, function( event ) {
                if ( !$modal.is(event.target) ) return;

                $modal.trigger('afterOpen.' + NAMESPACE, self);
                $modal.off('.open.' + NAMESPACE);
            });

            $modal.addClass(CLASS_MODAL_ITEM_MODIFIER_READY);
            // force layout, to enable css transitions
            $modal.width();
            $modal.addClass(CLASS_MODAL_ITEM_MODIFIER_VISIBLE);
            $modal.trigger('open.' + NAMESPACE, this);

            return this;
        },

        /**
         * Close and remove a modal or all modals if no modal id has been supplied
         * @param {string} [modalId] - with or without leading hash supported
         * @returns {{}}
         */
        close: function( modalId ) {
            var self = this;
            var $modal;

            if ( modalId === undefined ) {
                $modal = $root.children();
            } else {
                modalId = sanitizeId(modalId);

                $modal = $root.children('#' + modalId);
                if ( !$modal.length ) return this;
            }

            // wait for transitionend event to remove the ready class
            $modal.on('transitionend.close.' + NAMESPACE + ' webkitTransitionEnd.close.' + NAMESPACE, function( event ) {
                if ( !$modal.is(event.target) ) return;

                $modal.removeClass(CLASS_MODAL_ITEM_MODIFIER_READY);
                $modal.trigger('afterClose.' + NAMESPACE, self);
                $modal.off('.close.' + NAMESPACE);
            });

            $modal.removeClass(CLASS_MODAL_ITEM_MODIFIER_VISIBLE);
            $modal.trigger('close.' + NAMESPACE, this);

            return this;
        }
    };

    /**
     * Return a string without the leading hash character if existing
     * @param {string} id
     * @returns {string}
     */
    function sanitizeId( id ) {
        return id.slice(0, 1) === '#' ? id.slice(1) : id;
    }

}));