js/controllers/ModalViewController.js
import ViewController from '~/controllers/ViewController';
import ModalViewTemplate from '~/template/ModalViewTemplate';
import KeyManager from '~/models/KeyManager';
import { HandleUnhandledPromise } from '~/helpers/ErrorManager';
export const MODAL_BLUR_RADIUS = '16px';
/**
* This is like {@link ModalController} but better. This uses ModalTemplate
*/
export default class ModalViewController extends ViewController {
/**
* Creates controller for context
* @param {HTMLElement} context - you prob want to use .shared
* @param {Object} opts
* @param {number} [opts.baseZIndex=0]
* @param {boolean} bumpAnimation If should default show bump anim
*/
constructor(context, { baseZIndex = 10, bumpAnimation = true } = {}) {
super();
/** @private */
this.context = context;
this._dim = null;
this._activeTemplate = null;
this._activeTemplateModel = null;
this._eventListener = null;
this._removeKeyHandler = null;
this._baseZIndex = baseZIndex;
this._bumpAnimation = bumpAnimation;
}
/**
* Binds an element to trigger to show a modal. If the ID does NOT
* exist this will not do any binding.
*
* @param {string|HTMLElement} node - ID of node or the node itself
* @param {ModalViewTemplate} template - The modal template to show
*/
bind(node, template) {
if (typeof node === 'string') {
node = document.getElementById(node);
}
if (node) {
node.addEventListener('click', () => {
this.present(template)
.catch(HandleUnhandledPromise);
});
}
}
/**
* Presents a template.
* @param {ModalViewTemplate} template - The modal template
* @param {Object} opts
* @param {boolean} [opts.bumpAnimation=true] show a little bump when animating
* @param {string} [opts.alignmentClass=""] class to align with
*/
async present(template, { bumpAnimation = this._bumpAnimation, alignmentClass = "" } = {}) {
await this.hide();
const anime = await import('animejs');
const dim = <div class={`modal-view__dim ${alignmentClass}`}/>;
const instance = template.loadInContext(dim);
const listener = dim.addEventListener('click', (event) => {
if ((document.body.contains(event.target) || event.target === document.body) && !instance.contains(event.target)) {
this.hide()
.catch(HandleUnhandledPromise);
}
})
this._dim = dim;
this._eventListener = listener;
this._activeTemplateModel = template;
this._activeTemplate = instance; // Set this last to avoid race condition
dim.style.zIndex = this._baseZIndex;
instance.style.zIndex = +dim.style.zIndex + 1;
this.context.appendChild(dim);
this._removeKeyHandler = KeyManager.shared.register('Escape', () => {
this.hide()
.catch(HandleUnhandledPromise);
});
const timeline = anime.timeline()
.add({
targets: dim,
opacity: [0, 1],
backdropFilter: ['blur(0px)', `blur(${MODAL_BLUR_RADIUS})`],
webkitBackdropFilter: ['blur(0px)', `blur(${MODAL_BLUR_RADIUS})`],
duration: 300
})
if (bumpAnimation) {
instance.style.opacity = 0;
timeline.add({
offset: '-=50',
targets: instance,
opacity: [0, 1],
top: ['60%', '50%'],
easing: 'easeOutBack',
duration: 500
});
} else {
instance.style.opacity = 1;
}
await timeline.finished;
template.controller = this;
}
/**
* Hides the current template if there is one
* @param {Object} opts
* @param {boolean} [bumpAnimation=true] show a little bump when animating
*/
async hide({ bumpAnimation = this._bumpAnimation } = {}) {
if (!this._activeTemplate) return;
this._activeTemplateModel.willUnload();
this._removeKeyHandler?.();
// Avoids race condition
const instance = this._activeTemplate,
template = this._activeTemplateModel;
this._activeTemplate = null;
this._activeTemplateModel = null;
this._dim.removeEventListener('click', this._eventListener);
const anime = await import('animejs');
this._dim.style.pointerEvents = 'none'
const timeline = anime.timeline()
.add({
targets: this._dim,
opacity: [1, 0],
backdropFilter: [`blur(${MODAL_BLUR_RADIUS})`, 'blur(0px)'],
webkitBackdropFilter: [`blur(${MODAL_BLUR_RADIUS})`, 'blur(0px)'],
elasticity: 0,
duration: 300,
offset: bumpAnimation ? 400 : 0
});
if (bumpAnimation) {
timeline.add({
targets: instance,
opacity: [1, 0],
top: ['50%', '60%'],
easing: 'easeInBack',
offset: 0,
duration: 500,
elasticity: 0
})
}
await timeline.finished;
template.didUnload();
this.context.removeChild(this._dim);
this._dim = null;
}
static shared = new ModalViewController(document.body);
}