UNPKG

minsky-kit

Version:
489 lines (390 loc) 11.5 kB
// imports import PluginManager from "../managers/PluginManager"; import Component from "./Component"; import { insertTrapper, removeTrapper } from "../utils/trapFocus"; import { strToCssSelector } from "../utils/css"; import { DEF_GEN_ARGS, generateOverlay } from "../factories/OverlayFactory"; // private statics // class definition export default class Overlay extends Component { // constructor constructor(args = {}, objectName = "Overlay") { // debug state args.debug = args.debug || false; // capture el settings & nullify args.$el to prevent it to be treated as a dom element const elArgs = args.$el || {}; elArgs.attributes = elArgs.attributes || {}; args.$el = null; // block autoInit const autoInit = typeof args.autoInit === "boolean" ? args.autoInit : true; args.autoInit = false; // call super constructor super(args, objectName); // log this.log("Preparing"); // add properties this.$el = generateOverlay.call( this, { ...(elArgs || {}) }, args.generators || {} ); this.triggers = []; this._keepInDom = args.keepInDom || false; this._$parent = args.$parent || document.body; this._generators = args.generators || {}; this._plugins = args.plugins || {}; this._blockScroll = typeof args.blockScroll === "boolean" ? args.blockScroll : true; this._trapFocus = args.trapFocus || true; this.quitOnEscape = true; this._mainMenuId = args.mainMenuId || false; if (this._trapFocus) { this._lastActiveElement = null; } this._scrollBlocked = false; // set states using the methods for cleaner & more readable codebae const baseClass = elArgs.attributes.baseClass || DEF_GEN_ARGS.baseClass; this.states.add("show", [ { name: "showing", duration: 0, removeClass: [ `${baseClass}--shown`, `${baseClass}--hiding`, `${baseClass}--hidden`, ], addClass: [`${baseClass}--showing`], }, { name: "shown", duration: 0, removeClass: [ `${baseClass}--showing`, `${baseClass}--hiding`, `${baseClass}--hidden`, ], addClass: [`${baseClass}--shown`], }, ]); this.states.add("hide", [ { name: "hiding", duration: 0, removeClass: [ `${baseClass}--shown`, `${baseClass}--showing`, `${baseClass}--hidden`, ], addClass: [`${baseClass}--hiding`], }, { name: "hidden", duration: 500, removeClass: [ `${baseClass}--showing`, `${baseClass}--hiding`, `${baseClass}--shown`, ], addClass: [`${baseClass}--hidden`], }, ]); // flags this._visible = null; // add triggers if (args.$trigger) this.addTrigger(args.$trigger); // query close button & add close listener const closeBtn = this.$el.querySelector( strToCssSelector(baseClass, "closeBtn") ); if (closeBtn) closeBtn.addEventListener( "click", this.blm.add("closeBtn.click", onCloseBtnClick) ); // query backdrop button & add close listener const backdrop = this.$el.querySelector( strToCssSelector(baseClass, "backdrop") ); if (backdrop) backdrop.addEventListener( "click", this.blm.add("backdrop.click", onCloseBtnClick) ); // add listeners to states this.states.on("hidden", this.blm.add("onHidden", onHidden)); this.states.on("shown", this.blm.add("onShown", onShown)); // if keep in dom is active => add to dom if (this._keepInDom && !this.visible) { this._$parent.appendChild(this.$el); } // auto init if (autoInit) this.initialize(args); // run plugin init plugins.run("initialize", args, this); // set initial state (silently) this.silent = true; args.visible === true ? this.show(true) : this.hide(true); this.silent = false; // a11y: focus on first menu link this.$firstMenuLink = this.$el.querySelector('.menu .menu__link'); // a11y: to activate aria-controls on hamburger if (this._mainMenuId) { this.$el.id = this._mainMenuId; } } // methods show(instant = false) { this.log("Show"); // append element to parent this._$parent.appendChild(this.$el); // block scroll if (this._blockScroll) blockScroll.call(this); if (this._trapFocus) { this._lastActiveElement = document.activeElement; insertTrapper({ wrapper: this.$el }); } // focus on first menu link if present if (this.$firstMenuLink) this.$firstMenuLink.focus(); // body listener if (this._quitOnEscape) listenToBodyKeyUp.call(this, true); // set show state this.states.set("show", { instant, }); // run plugins plugins.run("show", instant, this); // dispatch this.dispatch("show"); } hide(instant = false) { this.log("Hide"); // unblock scroll unblockScroll.call(this); // body listener if (this._quitOnEscape) listenToBodyKeyUp.call(this, false); if (this._trapFocus) { removeTrapper({ wrapper: this.$el, lastActiveElement: this._lastActiveElement, }); this._lastActiveElement = document.activeElement; } // set show state this.states.set("hide", { instant, }); // run plugins plugins.run("hide", instant, this); // dispatch this.dispatch("hide"); } toggle(instant = false) { this.log("toggle"); this.visible ? this.hide(instant) : this.show(instant); // run plugins plugins.run("toggle", instant, this); } addTrigger(trigger) { // ensure it's an array if (!Array.isArray(trigger) && !(trigger instanceof NodeList)) trigger = [trigger]; // loop over all triggers for (const t of trigger) { // add to trigger list this.triggers.push(t); // add trigger class t.classList.add("js-isOverlayTrigger"); // add listeners t.addEventListener( "click", this.blm.add("trigger.click", onTriggerClick) ); } // run plugins plugins.run("addTrigger", trigger, this); this.log("Triggers added", trigger); this.dispatch("triggerAdded"); return this; } removeTrigger(trigger) { // ensure it's an array if (!Array.isArray(trigger) && !(trigger instanceof NodeList)) trigger = [trigger]; // loop over all triggers for (const t of trigger) { // add trigger class t.classList.add("js-isOverlayTrigger"); // get index of trigger const index = this.triggers.indexOf(t); // process item if it's known & remove listeners if (index > -1) { this.triggers.slice(index, 1); t.removeEventListener("click", this.blm.remove("trigger.click")); } } // run plugins plugins.run("removeTrigger", trigger, this); this.log("Triggers added", trigger); // dispatch this.dispatch("triggerRemoved"); return this; } setContent(content) { const container = this.$el.querySelector(".overlay__content"); container.innerHTML = ""; if (content instanceof Element) { container.appendChild(content); } else if (content instanceof NodeList) { for (const c of content) { container.appendChild(c); } } else { container.innerHTML = content; } // run plugins plugins.run("setContent", content, this); // dispatch this.dispatch("contentChanged"); } destroy() { // run plugins plugins.run("destroy", this); // remove all trigger listeners this.removeTrigger(this.triggers.slice(0)); // remove blockScroll if (this._scrollBlocked) unblockScroll.call(this); // remove el from dom this.$el.remove(); // super destroy super.destroy(); } // getters & setters get visible() { return this._visible; } set visible(value) { value ? this.show() : this.hide(); } get hiding() { return this._visible && this.states.state.name === "hide"; } get showing() { return !this._visible && this.states.state.name === "show"; } get quitOnEscape() { return this._quitOnEscape; } set quitOnEscape(value) { if (this.quitOnEscape !== value) { this._quitOnEscape = value; if (this.visible) { listenToBodyKeyUp.call(this, value); } } } static get plugins() { return plugins; } // deprecations get parent() { this.log( "warn", "Deprecation: this._parent is deprecated, use this._$parent instead" ); return this._$parent; } } // utils function blockScroll() { this.log("block scroll"); if (this._scrollBlocked) return; document.body.style.overflow = "hidden"; document.body.style.top = `-${window.scrollY}px`; document.body.style.left = `-${window.scrollX}px`; document.body.style.right = "0"; document.body.style.position = "fixed"; this._scrollBlocked = true; this.dispatch("scrollBlocked"); } function unblockScroll() { this.log("unblock scroll"); if (!this._scrollBlocked) return; const left = parseInt(document.body.style.left || 0); const top = parseInt(document.body.style.top || 0); document.body.style.position = ""; document.body.style.top = ""; document.body.style.left = ""; document.body.style.right = ""; document.body.style.overflow = ""; window.scrollTo(left, top * -1); this._scrollBlocked = false; this.dispatch("scrollUnblocked"); } function listenToBodyKeyUp(listen) { const listener = this.blm.get("onBodyKeyUp"); if (listen && !listener) { document.body.addEventListener( "keyup", this.blm.add("onBodyKeyUp", onBodyKeyUp) ); } else if (!listen && listener) { document.body.removeEventListener("keyup", this.blm.remove("onBodyKeyUp")); } } // handlers (supposed to root a user action to another method) function onTriggerClick(e) { this.log("Trigger click", e.currentTarget); // prevent default e.preventDefault(); // trigger show this.toggle(); this.dispatch("triggerClick", { $trigger: e.currentTarget, }); } function onCloseBtnClick(e) { this.log("Close button click", e.currentTarget); // prevent default e.preventDefault(); // trigger show this.hide(); this.dispatch("closeBtnClick"); } function onShown() { this.log("Visible"); this._visible = true; this.dispatch("shown"); } function onHidden() { this.log("Hidden"); // clear self from dom if (!this._keepInDom && this._$parent.contains(this.$el)) this._$parent.removeChild(this.$el); // change state this._visible = false; // dispatch event this.dispatch("hidden"); } // generators function onBodyKeyUp(e) { this.log("Key up", e.key); if (e.key) { // close on escape if required if (this.quitOnEscape && e.key.toLowerCase() === "escape") { this.log("Escape pressed => closing"); this.hide(); } } } // add parent to pluginManager let plugins = new PluginManager({ Target: Overlay, }); // deprecated methods export function generateOverlayHtml(args) { this.log( "warn", "Deprecation warning: 'generateOverlayHtml' in Overlay is deprecated, use 'generateOverlay()` from /factories/OverlayFactory.js instead." ); return generateOverlay(args); }