Code Monkey home page Code Monkey logo

modals's Introduction

Modals Build Status

Simple modal dialogue windows.

Download Modals / View the demo


Want to learn how to write your own vanilla JS plugins? Check out "The Vanilla JS Guidebook" and level-up as a web developer. ๐Ÿš€


Getting Started

Compiled and production-ready code can be found in the dist directory. The src directory contains development code.

1. Include Modals on your site.

<link rel="stylesheet" href="dist/css/modals.css">
<script src="dist/js/modals.js"></script>

2. Add the markup to your HTML.

Add the [data-modal] attribute to your modal toggle. Add the .modal class and the [data-modal-window] to your modal window. Be sure to assign each modal a unique ID.

<a data-modal="#modal" href="#">Modal Toggle</a>

<div class="modal" data-modal-window id="modal">
	<a class="close" data-modal-close href="#">x</a>
	<h3>Modal</h3>
	<p>Modal content</p>
	<button data-modal-close>Close</button>
</div>

Add the .modal-medium or .modal-small class to change the modal size.

<div class="modal modal-small" data-modal-window id="modal">
	...
</div>

Adding a [data-modal-close] data attribute to any button or link turns it into a modal dismiss link. The .modal-close class adds special styling to close links (if you wanted to use an X for close, for example). Clicking anywhere outside the modal or pressing the escape key will close the modal, too.

3. Assign a backup URL.

Always specify a functioning link as a backup for modals.

Modals uses modern JavaScript API's that aren't supported by older browsers, including IE 10 and lower. On modern browsers, Modals will prevent the backup URL from redirecting people away from the current page.

<a data-modal="#modal" href="http://backup-url.com">Modal Toggle</a>

If you need to support older browsers, you can still download the jQuery version of modals on GitHub.

4. Initialize Modals.

In the footer of your page, after the content, initialize Modals. And that's it, you're done. Nice work!

<script>
	modals.init();
</script>

Installing with Package Managers

You can install Modals with your favorite package manager.

  • NPM: npm install cferdinandi/modals
  • Bower: bower install https://github.com/cferdinandi/modals.git
  • Component: component install install cferdinandi/modals

Working with the Source Files

If you would prefer, you can work with the development code in the src directory using the included Gulp build system. This compiles, lints, and minifies code.

Dependencies

Make sure these are installed first.

Quick Start

  1. In bash/terminal/command line, cd into your project directory.
  2. Run npm install to install required files.
  3. When it's done installing, run one of the task runners to get going:
    • gulp manually compiles files.
    • gulp watch automatically compiles files and applies changes using LiveReload.

Options and Settings

Modals includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings.

Global Settings

You can pass options and callbacks into Modals through the init() function:

modals.init({
	selectorToggle: '[data-modal]', // Modal toggle selector
	selectorWindow: '[data-modal-window]', // Modal window selector
	selectorClose: '[data-modal-close]', // Modal window close selector
	modalActiveClass: 'active', // Class applied to active modal windows
	modalBGClass: 'modal-bg', // Class applied to the modal background overlay
	preventBGScroll: false, // Boolean, prevents background content from scroll if true
	preventBGScrollHtml: true, // Boolean, adds overflow-y: hidden to <html> if true (preventBGScroll must also be true)
	preventBGScrollBody: true, // Boolean, adds overflow-y: hidden to <body> if true (preventBGScroll must also be true)
	backspaceClose: true, // Boolean, whether or not to enable backspace/delete button modal closing
	stopVideo: true, // Boolean, if true, stop videos when tab closes
	callbackOpen: function ( toggle, modal ) {}, // Functions to run after opening a modal
	callbackClose: function ( toggle, modal ) {} // Functions to run after closing a modal
});

Note: If you change the selectorToggle, you still need to include the [data-modal] attribute in order to pass in the selector for the navigation menu.

If your modal includes any form fields, you should set backspaceClose to false or users will not be able to delete their text.

Use Modals events in your own scripts

You can also call Modals events in your own scripts.

openModal()

Open a specific modal window.

modals.openModal(
	toggle, // Node that launches the modal. ex. document.querySelector('#toggle')
	modalID, // The ID of the modal to launch. ex '#modal'
	options, // Classes and callbacks. Same options as those passed into the init() function.
	event // Optional, if a DOM event was triggered.
);

Example

modals.openModal( null, '#modal' );

closeModal()

Close the open modal window.

modals.closeModal(
	options // Classes and callbacks. Same options as those passed into the init() function.
);

Example

modals.closeModal();

destroy()

Destroy the current modals.init(). This is called automatically during the init function to remove any existing initializations.

modals.destroy();

Brower Compatibility

Modals works in all modern browsers, and IE 10 and above. You can extend browser support back to IE 9 with the classList.js polyfill.

Modals is built with modern JavaScript APIs, and uses progressive enhancement. If the JavaScript file fails to load, or if your site is viewed on older and less capable browsers, all content will be displayed by default.

How to Contribute

Please review the contributing guidelines.

License

The code is available under the MIT License.

modals's People

Contributors

cferdinandi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

modals's Issues

Bumping a modal trigger, even while scrolling, opens modal on mobile

I'm experiencing this on my own implementation as well as on the demo. While swiping/scrolling on mobile, if you so much as bump a modal trigger, the modal opens. This makes scrolling past modals with a large target almost impossible.

I've experienced this on iOS and Android.

iPhone 6 modal not triggering reliably

This one is a little odd, it seems that the modal script is struggling to trigger on iPhone 6's (specifically iPhone 6 / Safari).

It does work sometimes but there doesn't seem to be a reliable situation where I can say it definitely doesn't work on xyz occasion.

I've tested the modal on lots of other browsers on Windows, Mac and other phones - Android etc and it works exactly as intended and triggers the window fine. It even works fine on iPad / Safari so I'm at a loss and figured you might be able to suggest things to try at least! It also works in iPhone browser emulators so may be specific to actual device.

Recreate: Visit https://olympicholidays.com on iPhone 6 / Safari (don't know if same happens on iPhone 7) and click the "Holiday Search" green button top right and a search form should load in a full screen modal.

Any ideas on debugging/fixing certainly appreciated :)

James.

Make sure all existing modals are closed before opening a new one

(function (root, factory) {
    if ( typeof define === 'function' && define.amd ) {
        define([], factory(root));
    } else if ( typeof exports === 'object' ) {
        module.exports = factory(root);
    } else {
        root.modals = factory(root);
    }
})(typeof global !== 'undefined' ? global : this.window || this.global, function (root) {

    'use strict';

    //
    // Variables
    //

    var publicApi = {}; // Object for public APIs
    var supports = 'querySelector' in document && 'addEventListener' in root && 'classList' in document.createElement('_'); // Feature test
    var state = 'closed';
    var scrollbarWidth, placeholder, settings;

    // Default settings
    var defaults = {
        selectorToggle: '[data-modal]',
        selectorWindow: '[data-modal-window]',
        selectorClose: '[data-modal-close]',
        modalActiveClass: 'active',
        modalBGClass: 'modal-bg',
        preventBGScroll: false,
        preventBGScrollHtml: true,
        preventBGScrollBody: true,
        backspaceClose: true,
        stopVideo: true,
        callbackOpen: function () {},
        callbackClose: function () {}
    };


    //
    // Methods
    //

    /**
     * A simple forEach() implementation for Arrays, Objects and NodeLists.
     * @private
     * @author Todd Motto
     * @link   https://github.com/toddmotto/foreach
     * @param {Array|Object|NodeList} collection Collection of items to iterate
     * @param {Function}              callback   Callback function for each iteration
     * @param {Array|Object|NodeList} scope      Object/NodeList/Array that forEach is iterating over (aka `this`)
     */
    var forEach = function ( collection, callback, scope ) {
        if ( Object.prototype.toString.call( collection ) === '[object Object]' ) {
            for ( var prop in collection ) {
                if ( Object.prototype.hasOwnProperty.call( collection, prop ) ) {
                    callback.call( scope, collection[prop], prop, collection );
                }
            }
        } else {
            for ( var i = 0, len = collection.length; i < len; i++ ) {
                callback.call( scope, collection[i], i, collection );
            }
        }
    };

    /**
     * Merge two or more objects. Returns a new object.
     * @private
     * @param {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
     * @param {Object}   objects  The objects to merge together
     * @returns {Object}          Merged values of defaults and options
     */
    var extend = function () {

        // Variables
        var extended = {};
        var deep = false;
        var i = 0;
        var length = arguments.length;

        // Check if a deep merge
        if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
            deep = arguments[0];
            i++;
        }

        // Merge the object into the extended object
        var merge = function (obj) {
            for ( var prop in obj ) {
                if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
                    // If deep merge and property is an object, merge properties
                    if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) {
                        extended[prop] = extend( true, extended[prop], obj[prop] );
                    } else {
                        extended[prop] = obj[prop];
                    }
                }
            }
        };

        // Loop through each object and conduct a merge
        for ( ; i < length; i++ ) {
            var obj = arguments[i];
            merge(obj);
        }

        return extended;

    };

    /**
     * Get the closest matching element up the DOM tree.
     * @private
     * @param  {Element} elem     Starting element
     * @param  {String}  selector Selector to match against (class, ID, data attribute, or tag)
     * @return {Boolean|Element}  Returns null if not match found
     */
    var getClosest = function ( elem, selector ) {

        // Variables
        var firstChar = selector.charAt(0);
        var attribute, value;

        // If selector is a data attribute, split attribute from value
        if ( firstChar === '[' ) {
            selector = selector.substr(1, selector.length - 2);
            attribute = selector.split( '=' );

            if ( attribute.length > 1 ) {
                value = true;
                attribute[1] = attribute[1].replace( /"/g, '' ).replace( /'/g, '' );
            }
        }

        // Get closest match
        for ( ; elem && elem !== document; elem = elem.parentNode ) {

            // If selector is a class
            if ( firstChar === '.' ) {
                if ( elem.classList.contains( selector.substr(1) ) ) {
                    return elem;
                }
            }

            // If selector is an ID
            if ( firstChar === '#' ) {
                if ( elem.id === selector.substr(1) ) {
                    return elem;
                }
            }

            // If selector is a data attribute
            if ( firstChar === '[' ) {
                if ( elem.hasAttribute( attribute[0] ) ) {
                    if ( value ) {
                        if ( elem.getAttribute( attribute[0] ) === attribute[1] ) {
                            return elem;
                        }
                    } else {
                        return elem;
                    }
                }
            }

            // If selector is a tag
            if ( elem.tagName.toLowerCase() === selector ) {
                return elem;
            }

        }

        return null;

    };

    /**
     * Stop YouTube, Vimeo, and HTML5 videos from playing when leaving the slide
     * @private
     * @param  {Element} content The content container the video is in
     * @param  {String} activeClass The class asigned to expanded content areas
     */
    var stopVideos = function ( content, settings ) {

        // Check if stop video enabled
        if ( !settings.stopVideo ) return;

        // Only run if content container was open
        if ( !content.classList.contains( settings.modalActiveClass ) ) return;

        // Check if the video is an iframe or HTML5 video
        var iframe = content.querySelector( 'iframe');
        var video = content.querySelector( 'video' );

        // Stop the video
        if ( iframe ) {
            var iframeSrc = iframe.src;
            iframe.src = iframeSrc;
        }
        if ( video ) {
            video.pause();
        }

    };

    /**
     * Get the width of the scroll bars
     * @private
     */
    var getScrollbarWidth = function () {

        // Setup div
        var outer = document.createElement('div');
        outer.style.visibility = 'hidden';
        outer.style.width = '100px';
        outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps
        document.body.appendChild(outer);

        // Force scrollbars
        var widthNoScroll = outer.offsetWidth;
        outer.style.overflow = 'scroll';

        // Add innerdiv
        var inner = document.createElement('div');
        inner.style.width = '100%';
        outer.appendChild(inner);
        var widthWithScroll = inner.offsetWidth;

        // Remove divs
        outer.parentNode.removeChild(outer);

        return widthNoScroll - widthWithScroll;

    };

    /**
     * Create the modal background and append it to the DOM
     * @private
     */
    var createModalBg = function () {

        // If modal BG already exists, don't create another one
        if ( document.querySelector('[data-modal-bg]') ) return;

        // Define the modal background
        var modalBg = document.createElement('div');
        modalBg.setAttribute('data-modal-bg', true);
        modalBg.classList.add( settings.modalBGClass );

        // Append the modal background to the page
        document.body.appendChild(modalBg);

    };

    /**
     * Remove the modal background from the DOM
     * @private
     */
    var removeModalBg = function () {
        var modalBg = document.querySelector( '[data-modal-bg]' );
        if ( !modalBg ) return;
        document.body.removeChild( modalBg );
    };

    /**
     * Close open modal window
     * @public
     * @param  {Object} options
     * @param  {Event} event
     */
    publicApi.closeModal = function (options) {

        // Selectors and variables
        var localSettings = extend( settings || defaults, options || {} ); // Merge user options with defaults
        var modal = document.querySelector( localSettings.selectorWindow + '.' + localSettings.modalActiveClass ); // Get open modal

        // Sanity check
        if ( !modal ) return;

        // Stop videos from playing
        stopVideos( modal, localSettings );

        // Close the modal
        modal.classList.remove( localSettings.modalActiveClass );

        // Remove the modal background from the DOM
        removeModalBg();

        // Set state to closed
        state = 'closed';

        // Reallow background scrolling
        if ( localSettings.preventBGScroll ) {
            document.documentElement.style.overflowY = '';
            document.body.style.overflowY = '';
            document.body.style.paddingRight = '';
        }

        // Run callbacks after closing a modal
        localSettings.callbackClose( placeholder, modal );

        // Bring focus back to the button that toggles the modal
        if ( placeholder ) {
            placeholder.focus();
            placeholder = null;
        }

    };

    /**
     * Open the target modal window
     * @public
     * @param  {Element} toggle The element that toggled the open modal event
     * @param  {String} modalID ID of the modal to open
     * @param  {Object} options
     * @param  {Event} event
     */
    publicApi.openModal = function (toggle, modalID, options) {

        // Define the modal
        var localSettings = extend( settings || defaults, options || {} );  // Merge user options with defaults
        var modal = document.querySelector(modalID);

        // If a modal is already open, close it first
        if ( state === 'open' ) {
            publicApi.closeModal( localSettings );
        }

        // Save the visitor's spot on the page
        if ( toggle ) {
            placeholder = toggle;
        }

        // Activate the modal
        modal.classList.add( localSettings.modalActiveClass );
        createModalBg();
        state = 'open';

        // Bring modal into focus
        modal.setAttribute( 'tabindex', '-1' );
        modal.focus();

        // Prevent background scrolling
        if ( localSettings.preventBGScroll ) {
            if ( localSettings.preventBGScrollHtml ) {
                document.documentElement.style.overflowY = 'hidden';
            }
            if ( localSettings.preventBGScrollBody ) {
                document.body.style.overflowY = 'hidden';
            }
            document.body.style.paddingRight = scrollbarWidth + 'px';
        }

        localSettings.callbackOpen( toggle, modal ); // Run callbacks after opening a modal

    };

    /**
     * Handle toggle click events
     * @private
     */
    var eventHandler = function (event) {
        var toggle = event.target;
        var open = getClosest(toggle, settings.selectorToggle);
        var close = getClosest(toggle, settings.selectorClose);
        var modal = getClosest(toggle, settings.selectorWindow);
        var key = event.keyCode;

        if ( key && state === 'open' ) {
            if ( key === 27 || ( settings.backspaceClose && ( key === 8 || key === 46 ) ) ) {
                publicApi.closeModal();
            }
        } else if ( toggle ) {
            if ( modal && !close ) {
                return;
            } else if ( open && ( !key || key === 13 ) ) {
                event.preventDefault();
                publicApi.openModal( open, open.getAttribute('data-modal'), settings );
            } else if ( state === 'open' ) {
                event.preventDefault();
                publicApi.closeModal();
            }
        }
    };

    /**
     * Destroy the current initialization.
     * @public
     */
    publicApi.destroy = function () {
        if ( !settings ) return;
        document.removeEventListener('click', eventHandler, false);
        document.removeEventListener('touchstart', eventHandler, false);
        document.removeEventListener('keydown', eventHandler, false);
        document.documentElement.style.overflowY = '';
        document.body.style.overflowY = '';
        document.body.style.paddingRight = '';
        scrollbarWidth = null;
        placeholder = null;
        settings = null;
    };

    /**
     * Initialize Modals
     * @public
     * @param {Object} options User settings
     */
    publicApi.init = function ( options ) {

        // feature test
        if ( !supports ) return;

        // Destroy any existing initializations
        publicApi.destroy();

        // Merge user options with defaults
        settings = extend( defaults, options || {} );

        // Get scrollbar width
        scrollbarWidth = getScrollbarWidth();

        // Listen for events
        document.addEventListener('click', eventHandler, false);
        document.addEventListener('touchstart', eventHandler, false);
        document.addEventListener('keydown', eventHandler, false);

    };


    //
    // Public APIs
    //

    return publicApi;

});

[Question]: Interested in converting Bootstrap 3 Plugins to Plain JS ?

Hi. I know you don't like me :)
(you may not like BS)

BUT gonna ask anyways.

I see you have plenty examples working fine with IE9+ you have already developed. Most of them cover the most of the needs and I was thinking...

Would you be interested in teaming up to convert all BS plugins to PLAIN JS?
This would create more opportunities for us and many more developers to drop jQuery and develop better performance apps for the web :)

Modals and Inputs

Adding modals.js breaks radio and checkbox functionality. I was able to fix the issue by removing event.preventDefault(); on line 65. So far I haven't seen any negative side effects by removing that line.

Update NPM deps and switch to lib-sass

package.json

"gulp": "^3.9.0",
"node-fs": "^0.1.7",
"del": "^1.2.0",
"lazypipe": "^0.2.4",
"gulp-plumber": "^1.0.1",
"gulp-flatten": "^0.0.4",
"gulp-tap": "^0.1.3",
"gulp-rename": "^1.2.2",
"gulp-header": "^1.2.2",
"gulp-footer": "^1.0.5",
"gulp-watch": "^4.2.4",
"gulp-livereload": "^3.8.0",
"gulp-jshint": "^1.11.1",
"jshint-stylish": "^2.0.1",
"gulp-concat": "^2.6.0",
"gulp-uglify": "^1.2.0",
"karma": "^0.12.37",
"gulp-karma": "^0.0.4",
"karma-coverage": "^0.4.2",
"jasmine": "^2.3.1",
"karma-jasmine": "^0.3.6",
"karma-phantomjs-launcher": "^0.2.0",
"karma-spec-reporter": "^0.0.19",
"karma-htmlfile-reporter": "^0.2.1",
"gulp-sass": "^2.0.3",
"gulp-minify-css": "^1.2.0",
"gulp-autoprefixer": "^2.3.1",
"gulp-svgmin": "^1.1.2",
"gulp-svgstore": "^5.0.2",
"gulp-markdown": "^1.0.0",
"gulp-file-include": "^0.11.1"

gulpfile.js

.pipe(sass({
    outputStyle: 'expanded',
    sourceComments: true
}))

Add optimize js

package.json

"gulp-optimize-js": "^1.0.2",

gulpfile.js

var optimizejs = require('gulp-optimize-js');

    var jsTasks = lazypipe()
        .pipe(header, banner.full, { package : package })
        .pipe(optimizejs)
        .pipe(gulp.dest, paths.scripts.output)
        .pipe(rename, { suffix: '.min' })
        .pipe(uglify)
        .pipe(optimizejs)
        .pipe(header, banner.min, { package : package })
        .pipe(gulp.dest, paths.scripts.output);

Dist Sass

Have you thought about adding Sass for the dist to?
I would be more than willing to help you create something!

Add an async option

Check if the modal is in the window, and if it isn't, emit an event that can be hooked into for async functionality.

Close called as soon as openModal is called manually with dynamic modal.

Hi @cferdinandi,

First off, I fully appreciate that this is likely an issue my end, but I wanted to see if I am missing anything really obvious.

modals.init({my: options}) is called on page load.

I am then loading modal html in on request via ajax, on the first request the modal is loaded into to the dom and then modals.openModal(null, '#idOfNewElement') is called, this works fine.

On all modal requests I run a check to see if '#idOfNewElement' exists in the dom and if it has previously been loaded I then call modals.openModal(null, '#idOfNewElement') and don't load it in again.

This is where it falls down, it looks as if the classes get added and then removed, which ties up with the both callbackOpen and then callbackClose being called.

I just cant work out why or find any errors anywhere :(

And oddly if I call modals.openModal(null, '#idOfNewElement') from the Chrome console it fires ok.

Any ideas would be really appreciated!

Sam

Links changed

To create a link it says in the documentation:
<a data-modal data-target="#modal" href="#">Modal Toggle</a>

In the new version data-target is not used. It should be:
<a data-modal="#modal" href="#">Modal Toggle</a>

Incorrect docs for `modals.closeModals()`

Hello,

The docs state that the first parameter for closeModals() is the options object but it's actually the target so in order to pass the options object it needs to be called like this: modals.closeModals(null, options)

Thanks for the work!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.