(function($, anim) { 'use strict'; let _defaults = { opacity: 0.5, inDuration: 250, outDuration: 250, onOpenStart: null, onOpenEnd: null, onCloseStart: null, onCloseEnd: null, preventScrolling: true, dismissible: true, startingTop: '4%', endingTop: '10%' }; /** * @class * */ class Modal extends Component { /** * Construct Modal instance and set up overlay * @constructor * @param {Element} el * @param {Object} options */ constructor(el, options) { super(Modal, el, options); this.el.M_Modal = this; /** * Options for the modal * @member Modal#options * @prop {Number} [opacity=0.5] - Opacity of the modal overlay * @prop {Number} [inDuration=250] - Length in ms of enter transition * @prop {Number} [outDuration=250] - Length in ms of exit transition * @prop {Function} onOpenStart - Callback function called before modal is opened * @prop {Function} onOpenEnd - Callback function called after modal is opened * @prop {Function} onCloseStart - Callback function called before modal is closed * @prop {Function} onCloseEnd - Callback function called after modal is closed * @prop {Boolean} [dismissible=true] - Allow modal to be dismissed by keyboard or overlay click * @prop {String} [startingTop='4%'] - startingTop * @prop {String} [endingTop='10%'] - endingTop */ this.options = $.extend({}, Modal.defaults, options); /** * Describes open/close state of modal * @type {Boolean} */ this.isOpen = false; this.id = this.$el.attr('id'); this._openingTrigger = undefined; this.$overlay = $(''); this.el.tabIndex = 0; this._nthModalOpened = 0; Modal._count++; this._setupEventHandlers(); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Modal; } /** * Teardown component */ destroy() { Modal._count--; this._removeEventHandlers(); this.el.removeAttribute('style'); this.$overlay.remove(); this.el.M_Modal = undefined; } /** * Setup Event Handlers */ _setupEventHandlers() { this._handleOverlayClickBound = this._handleOverlayClick.bind(this); this._handleModalCloseClickBound = this._handleModalCloseClick.bind(this); if (Modal._count === 1) { document.body.addEventListener('click', this._handleTriggerClick); } this.$overlay[0].addEventListener('click', this._handleOverlayClickBound); this.el.addEventListener('click', this._handleModalCloseClickBound); } /** * Remove Event Handlers */ _removeEventHandlers() { if (Modal._count === 0) { document.body.removeEventListener('click', this._handleTriggerClick); } this.$overlay[0].removeEventListener('click', this._handleOverlayClickBound); this.el.removeEventListener('click', this._handleModalCloseClickBound); } /** * Handle Trigger Click * @param {Event} e */ _handleTriggerClick(e) { let $trigger = $(e.target).closest('.modal-trigger'); if ($trigger.length) { let modalId = M.getIdFromTrigger($trigger[0]); let modalInstance = document.getElementById(modalId).M_Modal; if (modalInstance) { modalInstance.open($trigger); } e.preventDefault(); } } /** * Handle Overlay Click */ _handleOverlayClick() { if (this.options.dismissible) { this.close(); } } /** * Handle Modal Close Click * @param {Event} e */ _handleModalCloseClick(e) { let $closeTrigger = $(e.target).closest('.modal-close'); if ($closeTrigger.length) { this.close(); } } /** * Handle Keydown * @param {Event} e */ _handleKeydown(e) { // ESC key if (e.keyCode === 27 && this.options.dismissible) { this.close(); } } /** * Handle Focus * @param {Event} e */ _handleFocus(e) { // Only trap focus if this modal is the last model opened (prevents loops in nested modals). if (!this.el.contains(e.target) && this._nthModalOpened === Modal._modalsOpen) { this.el.focus(); } } /** * Animate in modal */ _animateIn() { // Set initial styles $.extend(this.el.style, { display: 'block', opacity: 0 }); $.extend(this.$overlay[0].style, { display: 'block', opacity: 0 }); // Animate overlay anim({ targets: this.$overlay[0], opacity: this.options.opacity, duration: this.options.inDuration, easing: 'easeOutQuad' }); // Define modal animation options let enterAnimOptions = { targets: this.el, duration: this.options.inDuration, easing: 'easeOutCubic', // Handle modal onOpenEnd callback complete: () => { if (typeof this.options.onOpenEnd === 'function') { this.options.onOpenEnd.call(this, this.el, this._openingTrigger); } } }; // Bottom sheet animation if (this.el.classList.contains('bottom-sheet')) { $.extend(enterAnimOptions, { bottom: 0, opacity: 1 }); anim(enterAnimOptions); // Normal modal animation } else { $.extend(enterAnimOptions, { top: [this.options.startingTop, this.options.endingTop], opacity: 1, scaleX: [0.8, 1], scaleY: [0.8, 1] }); anim(enterAnimOptions); } } /** * Animate out modal */ _animateOut() { // Animate overlay anim({ targets: this.$overlay[0], opacity: 0, duration: this.options.outDuration, easing: 'easeOutQuart' }); // Define modal animation options let exitAnimOptions = { targets: this.el, duration: this.options.outDuration, easing: 'easeOutCubic', // Handle modal ready callback complete: () => { this.el.style.display = 'none'; this.$overlay.remove(); // Call onCloseEnd callback if (typeof this.options.onCloseEnd === 'function') { this.options.onCloseEnd.call(this, this.el); } } }; // Bottom sheet animation if (this.el.classList.contains('bottom-sheet')) { $.extend(exitAnimOptions, { bottom: '-100%', opacity: 0 }); anim(exitAnimOptions); // Normal modal animation } else { $.extend(exitAnimOptions, { top: [this.options.endingTop, this.options.startingTop], opacity: 0, scaleX: 0.8, scaleY: 0.8 }); anim(exitAnimOptions); } } /** * Open Modal * @param {cash} [$trigger] */ open($trigger) { if (this.isOpen) { return; } this.isOpen = true; Modal._modalsOpen++; this._nthModalOpened = Modal._modalsOpen; // Set Z-Index based on number of currently open modals this.$overlay[0].style.zIndex = 1000 + Modal._modalsOpen * 2; this.el.style.zIndex = 1000 + Modal._modalsOpen * 2 + 1; // Set opening trigger, undefined indicates modal was opened by javascript this._openingTrigger = !!$trigger ? $trigger[0] : undefined; // onOpenStart callback if (typeof this.options.onOpenStart === 'function') { this.options.onOpenStart.call(this, this.el, this._openingTrigger); } if (this.options.preventScrolling) { document.body.style.overflow = 'hidden'; } this.el.classList.add('open'); this.el.insertAdjacentElement('afterend', this.$overlay[0]); if (this.options.dismissible) { this._handleKeydownBound = this._handleKeydown.bind(this); this._handleFocusBound = this._handleFocus.bind(this); document.addEventListener('keydown', this._handleKeydownBound); document.addEventListener('focus', this._handleFocusBound, true); } anim.remove(this.el); anim.remove(this.$overlay[0]); this._animateIn(); // Focus modal this.el.focus(); return this; } /** * Close Modal */ close() { if (!this.isOpen) { return; } this.isOpen = false; Modal._modalsOpen--; this._nthModalOpened = 0; // Call onCloseStart callback if (typeof this.options.onCloseStart === 'function') { this.options.onCloseStart.call(this, this.el); } this.el.classList.remove('open'); // Enable body scrolling only if there are no more modals open. if (Modal._modalsOpen === 0) { document.body.style.overflow = ''; } if (this.options.dismissible) { document.removeEventListener('keydown', this._handleKeydownBound); document.removeEventListener('focus', this._handleFocusBound, true); } anim.remove(this.el); anim.remove(this.$overlay[0]); this._animateOut(); return this; } } /** * @static * @memberof Modal */ Modal._modalsOpen = 0; /** * @static * @memberof Modal */ Modal._count = 0; M.Modal = Modal; if (M.jQueryLoaded) { M.initializeJqueryWrapper(Modal, 'modal', 'M_Modal'); } })(cash, M.anime);