infojune/resources/sass/materialize/js/modal.js

383 lines
9.6 KiB
JavaScript

(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 = $('<div class="modal-overlay"></div>');
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);