@conectate/ct-dialog
Version:
HTML dialog made with Web Components
574 lines (563 loc) • 18.9 kB
JavaScript
/**
@license
Copyright (c) 2020 Herberth Obregón. All rights reserved.
This code may only be used under the BSD style license found at
https://open.grupoconectate.com/LICENSE.txt The complete set of authors may be found at
https://open.grupoconectate.com/AUTHORS.txt The complete set of contributors may be
found at https://open.grupoconectate.com/CONTRIBUTORS.txt Code distributed by Herberth Obregón as
part of the Conectate Open Source Project is also subject to an additional IP rights grant
found at https://open.grupoconectate.com/PATENTS.txt
*/
var CtDialog_1;
import { __decorate } from "tslib";
import { getClient, sleep } from "@conectate/ct-helpers";
import { CtLit, css, customElement, html, property, state } from "@conectate/ct-lit";
window.customModals ||= {
history: [],
toDelete: null,
closePopState: null,
closeESC: null
};
function addCustomEventListener(event, callback) {
window.addEventListener(event, callback, false);
return () => window.removeEventListener(event, callback, false);
}
if (window.customModals.closePopState == null)
window.customModals.closePopState = addCustomEventListener("popstate", async (e) => {
// Si el dialogo que se esta cerrando es el mismo que el que se abrio, lo cierro [REF.1]
if (window.customModals.history.length == 0)
return;
let dialogPop = window.customModals.history[window.customModals.history.length - 1];
if (dialogPop.modal) {
// Lo elimino de la lista de dialogos
if (window.customModals.toDelete == dialogPop.modal) {
window.customModals.toDelete = null;
dialogPop.modal.destroy();
}
else {
await dialogPop.modal.close(e, "popstate");
}
}
});
if (window.customModals.closeESC == null)
window.customModals.closeESC = addCustomEventListener("keyup", (e) => {
if (e.key === "Escape") {
if (window.customModals.history.length == 0)
return;
let dialogPop = window.customModals.history[window.customModals.history.length - 1];
// if (dialogPop.dialog.interactiveDismissDisabled) return;
dialogPop.modal.close(e, "keyup");
}
});
let dialogID = 0;
/**
* Creates and displays a dialog with the specified content
*
* @param {HTMLElement} el - The HTML element to display within the dialog
* @param {string} [id] - Optional identifier for the dialog
* @param {ConectateHistory} [history] - Optional history object for browser history integration
* @returns {CtDialog} The created dialog instance
*/
export function showCtDialog(el, id, history) {
let ctDialog;
// Inserto el ID del dialogo que voy a mostrar
ctDialog = new CtDialog();
ctDialog.dialogID = id || `${dialogID++}`;
window.customModals.history.push({ id: ctDialog.dialogID, modal: ctDialog });
history && (ctDialog.history = history);
ctDialog.show();
ctDialog.element = el;
return ctDialog;
}
/**
* Closes a specific dialog or all dialogs if none specified
*
* @param {CtDialog} [id] - Optional dialog instance to close
* @returns {Promise<number>} Promise that resolves when the dialog(s) is closed
*/
export function closeCtDialog(id) {
return new Promise(async (resolve) => {
let m = document.querySelectorAll("ct-dialog");
for (let mod = 0; mod < m.length; mod++) {
let modal = m[mod];
if (modal == null)
return;
if (id == modal || id == null) {
await modal.close();
}
}
resolve(0);
});
}
// @ts-ignore
window.showCtDialog = showCtDialog;
// @ts-ignore
window.closeCtDialog = closeCtDialog;
/**
* Size preferences for dialogs
*/
export var DialogSizePreferences;
(function (DialogSizePreferences) {
/** Full screen dialog */
DialogSizePreferences[DialogSizePreferences["fullsreen"] = 0] = "fullsreen";
/** 80% of window width with max-width */
DialogSizePreferences[DialogSizePreferences["fullsize"] = 1] = "fullsize";
})(DialogSizePreferences || (DialogSizePreferences = {}));
/**
* ## `ct-dialog`
* A versatile dialog component that supports multiple animation styles and modal behaviors.
*
* ### Usage
* ```javascript
* import { showCtDialog, closeCtDialog } from '@conectate/ct-dialog';
*
* // Create content for the dialog
* const content = document.createElement('div');
* content.textContent = 'This is a dialog';
*
* // Show the dialog
* const dialog = showCtDialog(content);
*
* // Configure dialog
* dialog.setAnimation('slide-right');
* dialog.interactiveDismissDisabled = true; // Prevent closing by clicking outside
*
* // Close dialog
* dialog.close();
* // or
* closeCtDialog(dialog);
* ```
*
* ### Animation Types
* - `alert`: Standard modal dialog (default)
* - `cupertino`: iOS-style dialog animation
* - `slide-right`: Dialog slides in from the right
* - `slide-left`: Dialog slides in from the left
* - `bottom-sheet`: Dialog slides up from the bottom
*
* @group ct-elements
* @element ct-dialog
* @fires close - Fired when the dialog closes
* @fires open - Fired when the dialog opens
* @csspart overlay - The overlay that covers the screen behind the dialog
* @csspart container - The container of the dialog content
* @cssProp --ct-dialog-width - Width of the dialog (default: 25cm)
* @cssProp --ct-dialog-height - Height of the dialog (default: 80%)
* @cssProp --ct-dialog-background - Background color of the dialog
* @cssProp --ct-dialog-border-radius - Border radius of the dialog
* @cssProp --ct-dialog-box-shadow - Shadow of the dialog
*/
let CtDialog = class CtDialog extends CtLit {
static { CtDialog_1 = this; }
/** Flag for checking CriOS browser */
static { this.checkForCriOS = false; }
/** Element to hide overflow on when dialog is open */
static { this.hiddenOverflow = globalThis?.document?.body; }
static { this.styles = [
css `
in-modalFadeEffect {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
in-scale-ct {
from {
transform: scale(0.1);
}
to {
transform: scale(1);
}
}
in-scale-cupertino {
from {
transform: scale(1.2);
}
to {
transform: scale(1);
}
}
in-slide-right {
from {
transform: translateX(100%);
transition-timing-function: ease-in-out;
}
to {
transform: translateX(0);
}
}
in-slide-left {
from {
transform: translateX(-100%);
transition-timing-function: ease-in-out;
}
to {
transform: translateX(0);
}
}
in-bottom-sheet {
from {
transform: translateY(100%);
transition-timing-function: ease-in-out;
}
to {
transform: translateY(0);
}
}
`,
css `
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: fixed;
z-index: var(--zi-modal, 110);
left: env(safe-area-inset-right, 0);
top: env(safe-area-inset-top, 0);
right: env(safe-area-inset-right, 0);
bottom: env(safe-area-inset-bottom, 0);
overflow-y: auto;
overflow-x: hidden;
animation: in-modalFadeEffect 0.2s;
font-family: "Roboto", sans-serif !important;
box-align: center;
box-orient: vertical;
box-sizing: border-box;
}
:host([type="bottom-sheet"]) {
justify-content: flex-end;
}
.overlay {
position: fixed;
left: env(safe-area-inset-right, 0);
top: env(safe-area-inset-top, 0);
right: env(safe-area-inset-right, 0);
bottom: env(safe-area-inset-bottom, 0);
background-color: rgba(0, 0, 0, 0.65);
animation: in-modalFadeEffect 0.2s;
transition: backdrop-filter 200ms;
z-index: -1;
}
/* @supports (backdrop-filter: blur(12px)) {
.overlay {
backdrop-filter: blur(12px);
}
} */
.anim-normal {
animation: in-scale-cupertino 0.25s;
}
.anim-cupertino {
animation: in-scale-cupertino 0.25s;
}
.anim-slide-right {
animation: in-slide-right 0.5s;
}
.anim-slide-left {
animation: in-slide-left 0.5s;
}
.anim-bottom-sheet {
animation: in-bottom-sheet 0.5s;
}
.no-anim {
animation: none;
}
.c {
display: block;
position: relative;
max-height: var(--ct-dialog-height, 80%);
max-height: var(--ct-dialog-height, 85dvh);
max-width: var(--ct-dialog-width, 25cm);
width: 100%;
/* height: max-content; */
overflow: auto;
margin: 0;
box-sizing: border-box;
z-index: 200;
}
`
]; }
get element() {
return this._element;
}
set element(val) {
let old = this._element;
this._element = val;
this.requestUpdate("element", old);
// Calc for dvh
let client = getClient();
let oldChrome = client.browser == "chrome" && client.browserVersion < 108;
let oldSafari = client.browser == "safari" && client.browserVersion < 16.4;
let oldFirefox = client.browser == "firefox";
/**
* [FIX]
* [Windows Vista] Chrome v75 height 0px;
* [MacOs] Overflow fix
*/
this.updateComplete.then(async () => {
await sleep(300);
let bodyY = Math.min(document.body.getBoundingClientRect().height, window.innerHeight);
let elementY = this._element.offsetHeight;
// console.log('bodyY', bodyY, 'elementY', elementY, elementY / bodyY);
if (bodyY > elementY) {
// Reviso si es menor al 5% de la pantalla
if ((elementY / bodyY) * 100 < 5) {
console.warn("[ct-dialog] El elemento no es visible");
if (this._element)
this._element.style.height = `var(--ct-dialog-height, ${Math.floor(bodyY * 0.8)}px)`;
}
else if ((elementY / bodyY) * 100 >= 80) {
// console.warn("El elemento esta desbordado");
if (this._element && !this.preferences.includes(DialogSizePreferences.fullsreen) && (oldChrome || oldSafari || oldFirefox)) {
this._element.style.height = `var(--ct-dialog-height, ${Math.floor(bodyY * 0.8)}px)`;
}
}
}
});
// if (getClient().os == "ios") {
// this.updateComplete.then(async () => {
// if (this._element) this._element.style.borderRadius = "0px";
// });
// }
}
render() {
return html `
${this.getStylesPref(this.preferences)}
<div
class="overlay"
="${(e) => {
e.stopPropagation();
if (this.interactiveDismissDisabled)
return;
this.close(e, "click");
}}"
></div>
${this.element}
`;
}
getStylesPref(pref) {
return pref.map(i => {
switch (i) {
case DialogSizePreferences.fullsreen:
return html `
<style>
.c {
max-height: 92% !important;
max-height: 100dvh;
max-width: 100vw !important;
max-width: 100dvw !important;
}
</style>
`;
}
});
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("popstate", this._closeViaPopState, false);
document.removeEventListener("keyup", this._clseDialogESC, false);
}
computeAnimation(anim) {
switch (anim) {
case "alert":
return "anim-normal";
case "cupertino":
return "anim-cupertino";
case "slide-left":
return "anim-slide-left";
case "slide-right":
return "anim-slide-right";
case "bottom-sheet":
return "anim-bottom-sheet";
}
}
constructor() {
super();
this.interactiveDismissDisabled = false;
this.role = "alert";
this.type = "alert";
this.ariaModal = "true";
// Vars
this.disableHistoryAPI = false;
this.preferences = [];
this.mappingContainer = new Promise(resolve => {
this.resolveMapping = resolve;
});
}
firstUpdated() {
// Checkeo que no sea Chrome de iOS porque esa mierda no sirve aquí 😡
if (CtDialog_1.checkForCriOS && navigator.userAgent.match(/CriOS\/([0-9\.]+)/))
this.disableHistoryAPI = true;
if (this.history == null)
this.history = { title: document.title, href: window.location.href };
// Crea una entrada en el History API identica que solo va a servir para usar 'Atras' en Moviles y cerrar el dialogo
if (!this.disableHistoryAPI)
window.history.pushState({ dialogID: this.dialogID }, this.history.title, this.history.href);
this.resolveMapping();
if (this.element?.classList) {
this.element.classList.add("c");
this.element.classList.add(this.computeAnimation(this.type));
}
}
enableHistoryAPI(value = true) {
this.disableHistoryAPI = !value;
return this;
}
fullscreenMode() {
this.preferences = [...this.preferences, DialogSizePreferences.fullsreen];
return this;
}
fullsizeMode() {
this.preferences = [...this.preferences, DialogSizePreferences.fullsize];
return this;
}
setAnimation(anim) {
this.type = anim;
return this;
}
show() {
document.body.appendChild(this);
if (CtDialog_1.hiddenOverflow) {
CtDialog_1.hiddenOverflow.style.overflow = "hidden";
}
}
async waitForDefined(param, timeout = 10_000) {
while (param() == undefined) {
await new Promise(resolve => setTimeout(resolve, 200));
timeout -= 200;
if (timeout <= 0)
return false;
}
return param();
}
close(e, type) {
// Este dialog lo elimino de las lista de dialogos
return new Promise(async (resolve) => {
let finish = async () => {
if (!document.body.contains(this)) {
console.warn(`dialogID ya no se encuentra en el DOM`, this);
resolve(e);
return;
}
// Si lo cerre manual o por ESC, elimino la entrada del History API. Como ya se eliminó el dialogo de la lista, `this.close()` no se ejecutará [REF.1]
if (type != "popstate" && !this.disableHistoryAPI) {
window.customModals.toDelete = this;
window.history.back();
}
else {
this.destroy();
}
await sleep(250);
resolve(e);
};
// espero que haga el mapping en el container
await this.mappingContainer;
let index = window.customModals.history.findIndex(d => d.modal == this);
let isLast = index == window.customModals.history.length - 1;
if (!isLast) {
// Cierro todos los dialogos que estan por encima de este
for (let i = window.customModals.history.length - 1; i > index; i--) {
await window.customModals.history[i].modal.close();
}
}
if (!document.body.animate) {
await finish();
return;
}
let anim = this.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 250,
fill: "both"
});
this.element?.animate(this.getAnimOut(this.type), {
duration: 270,
fill: "both"
});
anim.onfinish = () => finish();
});
}
getAnimOut(type) {
switch (type) {
case "alert":
return [
{ transform: "scale(1)", opacity: 1 },
{ transform: "scale(1.2)", opacity: 0 }
];
case "cupertino":
return [
{ transform: "scale(1)", opacity: 1 },
{ transform: "scale(0.1)", opacity: 0 }
];
case "slide-left":
return [
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(-100%)", opacity: 0 }
];
case "slide-right":
return [
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(100%)", opacity: 0 }
];
case "bottom-sheet":
return [
{ transform: "translateY(0)", opacity: 1 },
{ transform: "translateY(100%)", opacity: 0 }
];
}
}
destroy() {
window.customModals.history.splice(window.customModals.history.findIndex(d => d.modal == this), 1);
// Elimino el dialogo
document.body.removeChild(this);
if (CtDialog_1.hiddenOverflow && window.customModals.history.length == 0) {
CtDialog_1.hiddenOverflow.style.overflow = "";
}
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("on-close"));
}
/**
* @deprecated Use close() instead. The method will be removed in the next major version.
*/
closeDialog(e) {
return this.close(e, "click");
}
static get properties() {
return {
/**
*
*/
dialogID: { type: String, reflect: true },
/**
* Entrada para el History API de tipe {title,href}
*/
history: {
type: Object
},
element: { type: Object },
free: { type: Object }
};
}
};
__decorate([
property({ type: Boolean })
], CtDialog.prototype, "interactiveDismissDisabled", void 0);
__decorate([
property({ type: String, reflect: true })
], CtDialog.prototype, "role", void 0);
__decorate([
property({ type: String, reflect: true })
], CtDialog.prototype, "type", void 0);
__decorate([
property({ type: String, reflect: true, attribute: "aria-modal" })
], CtDialog.prototype, "ariaModal", void 0);
__decorate([
state()
], CtDialog.prototype, "_element", void 0);
__decorate([
property({ type: Array })
], CtDialog.prototype, "preferences", void 0);
CtDialog = CtDialog_1 = __decorate([
customElement("ct-dialog")
], CtDialog);
export { CtDialog };