web-ui-pack
Version:
Web package with UI elements
464 lines (463 loc) • 16.3 kB
JavaScript
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;
}
(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;
}
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);