(function($, Vel) { 'use strict'; let _defaults = { opacity: 0.5, inDuration: 250, outDuration: 250, ready: undefined, complete: undefined, dismissible: true, startingTop: '4%', endingTop: '10%' }; /** * @class * */ class Modal { /** * Construct Modal instance and set up overlay * @constructor * @param {jQuery} $el * @param {Object} options */ constructor($el, options) { // If exists, destroy and reinitialize if (!!$el[0].M_Modal) { $el[0].M_Modal.destroy(); } /** * The jQuery element * @type {jQuery} */ this.$el = $el; /** * 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} ready - Callback function called when modal is finished entering * @prop {Function} complete - Callback function called when modal is finished exiting * @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.$el[0].M_Modal = this; this.id = $el.attr('id'); this.openingTrigger = undefined; this.$overlay = $('
'); Modal._increment++; Modal._count++; this.$overlay[0].style.zIndex = 1000 + Modal._increment * 2; this.$el[0].style.zIndex = 1000 + Modal._increment * 2 + 1; this.setupEventHandlers(); } static get defaults() { return _defaults; } static init($els, options) { let arr = []; $els.each(function() { arr.push(new Modal($(this), options)); }); return arr; } /** * Get Instance */ getInstance() { return this; } /** * Teardown component */ destroy() { this.removeEventHandlers(); this.$el[0].removeAttribute('style') if (!!this.$overlay[0].parentNode) { this.$overlay[0].parentNode.removeChild(this.$overlay[0]); } this.$el[0].M_Modal = undefined; Modal._count--; } /** * 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[0].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[0].removeEventListener('click', this.handleModalCloseClickBound); } /** * Handle Trigger Click * @param {Event} e */ handleTriggerClick(e) { let $trigger = $(e.target).closest('.modal-trigger'); if (e.target && $trigger.length) { let modalId = $trigger[0].getAttribute('href'); if (modalId) { modalId = modalId.slice(1); } else { modalId = $trigger[0].getAttribute('data-target'); } 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 (e.target && $closeTrigger.length) { this.close(); } } /** * Handle Keydown * @param {Event} e */ handleKeydown(e) { // ESC key if (e.keyCode === 27 && this.options.dismissible) { this.close(); } } /** * Animate in modal */ animateIn() { // Set initial styles $.extend(this.$el[0].style, { display: 'block', opacity: 0 }); $.extend(this.$overlay[0].style, { display: 'block', opacity: 0 }); // Animate overlay Vel( this.$overlay[0], {opacity: this.options.opacity}, {duration: this.options.inDuration, queue: false, ease: 'easeOutCubic'} ); // Define modal animation options let enterVelocityOptions = { duration: this.options.inDuration, queue: false, ease: 'easeOutCubic', // Handle modal ready callback complete: () => { if (typeof(this.options.ready) === 'function') { this.options.ready.call(this, this.$el, this.openingTrigger); } } }; // Bottom sheet animation if (this.$el[0].classList.contains('bottom-sheet')) { Vel( this.$el[0], {bottom: 0, opacity: 1}, enterVelocityOptions); // Normal modal animation } else { Vel.hook(this.$el[0], 'scaleX', 0.7); this.$el[0].style.top = this.options.startingTop; Vel( this.$el[0], {top: this.options.endingTop, opacity: 1, scaleX: 1}, enterVelocityOptions ); } } /** * Animate out modal */ animateOut() { // Animate overlay Vel( this.$overlay[0], { opacity: 0}, {duration: this.options.outDuration, queue: false, ease: 'easeOutQuart'} ); // Define modal animation options var exitVelocityOptions = { duration: this.options.outDuration, queue: false, ease: 'easeOutCubic', // Handle modal ready callback complete: () => { this.$el[0].style.display = 'none'; // Call complete callback if (typeof(this.options.complete) === 'function') { this.options.complete.call(this, this.$el); } this.$overlay[0].parentNode.removeChild(this.$overlay[0]); } }; // Bottom sheet animation if (this.$el[0].classList.contains('bottom-sheet')) { Vel( this.$el[0], {bottom: '-100%', opacity: 0}, exitVelocityOptions ); // Normal modal animation } else { Vel( this.$el[0], {top: this.options.startingTop, opacity: 0, scaleX: 0.7}, exitVelocityOptions ); } } /** * Open Modal * @param {jQuery} [$trigger] */ open($trigger) { if (this.isOpen) { return; } this.isOpen = true; let body = document.body; body.style.overflow = 'hidden'; this.$el[0].classList.add('open'); body.appendChild(this.$overlay[0]); // Set opening trigger, undefined indicates modal was opened by javascript this.openingTrigger = !!$trigger ? $trigger : undefined; if (this.options.dismissible) { this.handleKeydownBound = this.handleKeydown.bind(this); document.addEventListener('keydown', this.handleKeydownBound); } this.animateIn(); return this; } /** * Close Modal */ close() { if (!this.isOpen) { return; } this.isOpen = false; this.$el[0].classList.remove('open'); document.body.style.overflow = ''; if (this.options.dismissible) { document.removeEventListener('keydown', this.handleKeydownBound); } this.animateOut(); return this; } } /** * @static * @memberof Modal */ Modal._increment = 0; /** * @static * @memberof Modal */ Modal._count = 0; Materialize.Modal = Modal; $.fn.modal = function(methodOrOptions) { // Call plugin method if valid method name is passed in if (Modal.prototype[methodOrOptions]) { // Getter methods if (methodOrOptions.slice(0,3) === 'get') { return this.first()[0].M_Modal[methodOrOptions](); // Void methods } else { return this.each(function() { this.M_Modal[methodOrOptions](); }); } // Initialize plugin if options or no argument is passed in } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) { Modal.init(this, arguments[0]); return this; // Return error if an unrecognized method name is passed in } else { $.error(`Method ${methodOrOptions} does not exist on jQuery.modal`); } }; })(jQuery, Materialize.Vel);