UNPKG

web-ui-pack

Version:
464 lines (463 loc) 16.3 kB
import WUPBaseModal from "./baseModal"; import focusFirst from "./helpers/focusFirst"; import { WUPcssButton } from "./styles"; export var ModalOpenCases; (function (ModalOpenCases) { ModalOpenCases[ModalOpenCases["onManualCall"] = 0] = "onManualCall"; ModalOpenCases[ModalOpenCases["onInit"] = 1] = "onInit"; ModalOpenCases[ModalOpenCases["onTargetClick"] = 2] = "onTargetClick"; })(ModalOpenCases || (ModalOpenCases = {})); export var ModalCloseCases; (function (ModalCloseCases) { ModalCloseCases[ModalCloseCases["onManualCall"] = 0] = "onManualCall"; ModalCloseCases[ModalCloseCases["onCloseClick"] = 1] = "onCloseClick"; ModalCloseCases[ModalCloseCases["onOutsideClick"] = 2] = "onOutsideClick"; ModalCloseCases[ModalCloseCases["onPressEsc"] = 3] = "onPressEsc"; ModalCloseCases[ModalCloseCases["onSubmitEnd"] = 4] = "onSubmitEnd"; })(ModalCloseCases || (ModalCloseCases = {})); const tagName = "wup-modal"; const attrConfirm = "w-confirm"; export default class WUPModalElement extends WUPBaseModal { #ctr = this.constructor; static $useConfirmHook(opts) { document.addEventListener("click", (e) => { if (e.button || window.__wupFixCycleClick) { return; } const btn = WUPModalElement.prototype.findParent.call(e.target, (el) => el.hasAttribute(attrConfirm), { bubbleCount: 5, }); const txt = btn?.getAttribute(attrConfirm); if (txt) { e.stopPropagation(); this.$showConfirm({ question: txt, ...opts, onRender: (m) => { btn.$onRenderModal?.call(btn, m); opts?.onRender?.call(this, m); }, }).then((isOk) => { isOk && btn.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); } }, { capture: true }); } static async $showConfirm(opts) { const me = document.createElement(tagName); opts?.className && me.classList.add(opts.className); me.$options.selfRemove = true; Object.assign(me.$options, opts?.defaults); me.gotRenderConfirm(opts?.question || ""); document.body.appendChild(me); opts?.onRender?.call(this, me); setTimeout(() => { if (me._mid && me._mid > 1) { const p = me._openedItems[me._mid - 2]; if (me._opts.replace) { switch (me._opts.placement) { case "left": case "right": me.setAttribute("w-placement", "center"); break; default: break; } return; } me.setAttribute("w-placement", "center"); const prev = { x: -1, y: -1 }; const goCenter = () => { if (!me.$isOpened || me.$isClosing || !p.isConnected) { return; } const r = p.getBoundingClientRect(); const rme = me.getBoundingClientRect(); const x = Math.max(0, Math.round(r.x + (r.width - rme.width) / 2)); const y = Math.max(0, Math.round(r.y + (r.height - rme.height) / 2)); if (prev.x !== x || prev.y !== y) { prev.x = x; prev.y = y; me.style.margin = `${y}px 0 0 ${x}px`; } window.requestAnimationFrame(goCenter); }; goCenter(); } }); const btnConfirm = me.querySelector("[data-close=confirm]"); let isConfirmed = false; if (btnConfirm) { btnConfirm.onclick = (ev) => { isConfirmed = true; setTimeout(() => (isConfirmed = false), 1); window.__wupFixCycleClick = true; setTimeout(() => delete window.__wupFixCycleClick); me.goClose(1, ev); }; } else { me.throwError("button[data-close=confirm] missed"); } return new Promise((res) => { me.addEventListener("$willClose", () => res(isConfirmed), { once: true, passive: true }); }); } static $classFade = "wup-modal-fade"; static $classOpened = "wup-modal-open"; static get $styleRoot() { return `:root { --modal-anim-t: 400ms; --modal-text: inherit; --modal-bg: #fff; --modal-fade: #0007; --modal-margin: 2em; } [wupdark] { --modal-bg: #222a36; }`; } static get $style() { return `${super.$style} :host { z-index: 9002; width: 100%; max-width: 600px; min-height: 150px; color: var(--modal-text); background: var(--modal-bg); border: 1px solid #0002; user-select: none; pointer-events: none; touch-action: none; outline: none; } :host[open] { user-select: initial; pointer-events: initial; touch-action: initial; } :host[w-placement="top"] { top:0;left:0;right:0; margin: var(--modal-margin); margin-left: auto; margin-right: auto; max-height: calc(100% - var(--modal-margin) * 2); transform: translateY(-50%); } :host[w-placement="center"] { top:0;left:0;right:0;bottom:0; margin: auto; height: fit-content${""}; max-height: calc(100% - var(--modal-margin) * 2); transform: translateY(-150%); } :host[w-placement="right"] { right:0;top:0;bottom:0; border-radius: 0; transform: translateX(100%); } :host[w-placement="left"] { left:0;top:0;bottom:0; border-radius: 0; transform: translateX(-100%); } :host[show] { transform: none; } :host[hide] { opacity: 0!important; pointer-events: none!important; user-select: none!important; touch-action: none!important; } @media (max-width: 600px) { :host { --modal-margin: 0px; border-radius: 0; } } .${this.$classFade} { --modal-anim: var(--modal-anim-t) cubic-bezier(0, 0, 0.2, 1) 0ms; z-index: 9000; display: block; position: fixed; top:0;left:0;right:0;bottom:0; background: var(--modal-fade); opacity: 0; pointer-events: none; touch-action: none; } .${this.$classFade}[show] { pointer-events: initial; touch-action: initial; opacity: 1; } @media not all and (prefers-reduced-motion) { .${this.$classFade} { transition: opacity var(--modal-anim), transform var(--modal-anim); } } :host > button[close] { --icon-img: var(--wup-icon-cross); z-index: 10; position: absolute; right: 0; margin: -0.2em 1em 0; } :host h2 { margin: 0 0 1em; padding-right: 1.8em; } :host wup-form { max-width: initial; } :host wup-form button[type=submit] { margin-bottom: 0; margin-left: auto; } :host footer { display: flex; gap: calc(var(--base-margin) / 2); margin-top: var(--base-margin); justify-content: flex-end; } ${WUPcssButton(":host footer>button")} :host footer>button[type] { margin: 0; min-width: 7em; } :host footer>button[data-close=modal] { background: var(--base-btn2-bg); color: var(--base-btn2-text); }`; } static $defaults = { target: null, placement: "center", autoFocus: true, autoClose: true, selfRemove: false, replace: false, confirmUnsaved: true, }; static get mappedAttributes() { const m = super.mappedAttributes; m.target = { type: 6 }; return m; } $refFade; $refClose; gotRender(isOpening = false) { if (!isOpening) { return; } this.tabIndex = -1; this.role = "dialog"; this.setAttribute("aria-modal", true); const header = this.querySelector("h1,h2,h3,h4,h5,h6,[role=heading]"); if (!header) { console.warn("WA: header missed. Add <h2>..<h6> or role='heading' to modal content"); } else { header.id ||= this.#ctr.$uniqueId; this.setAttribute("aria-labelledby", header.id); } this.$refClose ??= document.createElement("button"); this.$refClose.type = "button"; this.$refClose.setAttribute("aria-label", __wupln("close", "aria")); this.$refClose.setAttribute(this.#ctr.classNameBtnIcon, ""); this.$refClose.setAttribute("close", ""); this.$refClose.setAttribute("data-close", "modal"); this.prepend(this.$refClose); this.setAttribute("w-placement", this._opts.placement); document.body.classList.add(this.#ctr.$classOpened); } gotRenderConfirm(headerContent) { this.innerHTML = `<h2>${headerContent}</h2> <footer> <button type='button' data-close='modal'>${__wupln("Cancel", "content")}</button> <button type='button' data-close='confirm'>${__wupln("Confirm", "content")}</button> </footer>`; } _target; _targetClick; gotChanges(propsChanged) { super.gotChanges(propsChanged); const trg = this._opts.target; const isTrgChange = this._target !== trg; if (isTrgChange) { this._target?.removeEventListener("click", this._targetClick); this._target = trg; } if (!trg) { !propsChanged && this.goOpen(1, null); } else if (isTrgChange) { this._targetClick = (e) => { setTimeout(() => !e.defaultPrevented && this.goOpen(2, e)); }; trg.addEventListener("click", this._targetClick, { passive: true }); } } _lastFocused; _mid; gotOpen(openCase, e) { const ael = document.activeElement; this._lastFocused = ael?.id || ael; this.gotRender(true); this._target?.$onRenderModal?.call(this._target, this); this._mid = this._openedItems.push(this); if (this._mid === 1) { this.$refFade ??= document.body.appendChild(document.createElement("div")); setTimeout(() => this.$refFade?.setAttribute("show", "")); } else { const p = this._openedItems[this._mid - 2]; const isReplace = this._opts.replace; this.$refFade ??= (isReplace ? document.body : p).appendChild(document.createElement("div")); this.$refFade.setAttribute("show", ""); p.$refFade.style.display = "none"; isReplace && p.setAttribute("hide", ""); } this.$refFade.className = this.#ctr.$classFade; this.$refFade.onclick = (ev) => this.goClose(2, ev); this.appendEvent(this, "click", (ev) => this.gotClick(ev)); this.appendEvent(this, "keydown", (ev) => this.gotKeyDown(ev), { passive: false }); this.appendEvent(this, "$submitEnd", (sev) => { sev.detail.success && !sev.defaultPrevented && this.goClose(4, sev); }); this._opts.autoFocus ? this.focusAny() : this.focus(); } goClose(closeCase, ev, immediately) { if (this._whenClose) { return this._whenClose; } if (!this._whenOpen) { return Promise.resolve(true); } if (this._opts.confirmUnsaved && closeCase !== 4) { const f = this.querySelector("wup-form"); if (f?.$isChanged && !f.$options.autoStore) { return this.#ctr .$showConfirm({ question: __wupln("You have unsaved changes.\nWould you like to skip it?", "content") }) .then((isOk) => (isOk ? super.goClose(closeCase, ev, immediately) : false)); } } return super.goClose(closeCase, ev, immediately); } gotClose(closeCase, ev) { this.$refFade.onclick = null; this.disposeEvents(); if (this._mid === 1) { this.$refFade.removeAttribute("show"); const b = document.body; b.classList.remove(this.#ctr.$classOpened); !b.className && b.removeAttribute("class"); } else { this.$refFade.remove(); this.$refFade = undefined; const p = this._openedItems[this._mid - 2]; p.$refFade.style.display = ""; this.removeEmptyStyle.call(p.$refFade); p.removeAttribute("hide"); } this._openedItems.splice(this._mid - 1, 1); this.focusBack(); } gotClick(e) { const t = e.target; if (e.defaultPrevented || t === this || e.button) { return; } const all = this.querySelectorAll("[data-close=modal]").values(); for (const el of all) { if (this.itsMe.call(el, t)) { this._mid !== 1 && e.stopPropagation(); this.goClose(1, e); break; } } } gotKeyDown(e) { if (e.altKey || e.ctrlKey || e.metaKey) { return; } switch (e.key) { case "Escape": if (!e.shiftKey && !e.defaultPrevented) { e.preventDefault(); this.goClose(3, e); } break; case "Tab": { let isFocusLast; if (e.shiftKey) { const isFirst = document.activeElement === this.querySelector(focusFirst.$selector); if (isFirst) { isFocusLast = true; } } else { const all = this.querySelectorAll(focusFirst.$selector); const isLast = document.activeElement === all.item(all.length - 1); if (isLast) { isFocusLast = false; } } if (isFocusLast != null) { e.preventDefault(); focusFirst(this, { isFocusLast }); } break; } default: break; } } focusBack() { let el; if (typeof this._lastFocused === "string") { el = document.getElementById(this._lastFocused); } else { el = this._lastFocused; } if (el && el.focus && el.isConnected) { el.focus(); } else { this.throwError("Impossible to return focus back: element missed. Before opening modal set 'id' to focused element"); document.activeElement?.blur?.(); } } focusAny() { if (this.$isOpened) { this.$refClose.disabled = true; const isOk = this.focus(); this.$refClose.disabled = false; !isOk && this.$refClose.focus(); } } resetState() { super.resetState(); this.$refFade?.remove(); this.$refFade = undefined; this.isConnected && this._opts.selfRemove && this.remove(); } dispose() { const bd = document.body; bd.classList.remove(this.#ctr.$classOpened); !bd.className && bd.removeAttribute("class"); this._target?.removeEventListener("click", this._targetClick); const i = this._openedItems.indexOf(this); i > -1 && this._openedItems.splice(i, 1); super.dispose(); } get _openedItems() { return __openedItems; } } const __openedItems = []; customElements.define(tagName, WUPModalElement);