import * as colors from './00colors.js'; import * as plugins from './00plugins.js'; import { zIndexLayers, zIndexRegistry } from './00zindex.js'; import { demoFunc } from './dees-modal.demo.js'; import { customElement, html, DeesElement, property, type TemplateResult, cssManager, css, type CSSResult, unsafeCSS, unsafeHTML, state, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { DeesWindowLayer } from './dees-windowlayer.js'; import './dees-icon.js'; declare global { interface HTMLElementTagNameMap { 'dees-modal': DeesModal; } } @customElement('dees-modal') export class DeesModal extends DeesElement { // STATIC public static demo = demoFunc; public static async createAndShow(optionsArg: { heading: string; content: TemplateResult; menuOptions: plugins.tsclass.website.IMenuItem[]; width?: 'small' | 'medium' | 'large' | 'fullscreen' | number; maxWidth?: number; minWidth?: number; showCloseButton?: boolean; showHelpButton?: boolean; onHelp?: () => void | Promise; mobileFullscreen?: boolean; }) { const body = document.body; const modal = new DeesModal(); modal.heading = optionsArg.heading; modal.content = optionsArg.content; modal.menuOptions = optionsArg.menuOptions; if (optionsArg.width) modal.width = optionsArg.width; if (optionsArg.maxWidth) modal.maxWidth = optionsArg.maxWidth; if (optionsArg.minWidth) modal.minWidth = optionsArg.minWidth; if (optionsArg.showCloseButton !== undefined) modal.showCloseButton = optionsArg.showCloseButton; if (optionsArg.showHelpButton !== undefined) modal.showHelpButton = optionsArg.showHelpButton; if (optionsArg.onHelp) modal.onHelp = optionsArg.onHelp; if (optionsArg.mobileFullscreen !== undefined) modal.mobileFullscreen = optionsArg.mobileFullscreen; modal.windowLayer = await DeesWindowLayer.createAndShow({ blur: true, }); modal.windowLayer.addEventListener('click', async () => { await modal.destroy(); }); body.append(modal.windowLayer); body.append(modal); // Get z-index for modal (should be above window layer) modal.modalZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(modal, modal.modalZIndex); return modal; } // INSTANCE @property({ type: String, }) public heading = ''; @state({}) public content: TemplateResult; @state({}) public menuOptions: plugins.tsclass.website.IMenuItem[] = []; @property({ type: String }) public width: 'small' | 'medium' | 'large' | 'fullscreen' | number = 'medium'; @property({ type: Number }) public maxWidth: number; @property({ type: Number }) public minWidth: number; @property({ type: Boolean }) public showCloseButton: boolean = true; @property({ type: Boolean }) public showHelpButton: boolean = false; @property({ attribute: false }) public onHelp: () => void | Promise; @property({ type: Boolean }) public mobileFullscreen: boolean = false; @state() private modalZIndex: number = 1000; constructor() { super(); } public static styles = [ cssManager.defaultStyles, css` :host { font-family: 'Geist Sans', sans-serif; color: ${cssManager.bdTheme('#333', '#fff')}; will-change: transform; } .modalContainer { display: flex; position: fixed; top: 0px; left: 0px; width: 100vw; height: 100vh; box-sizing: border-box; align-items: center; justify-content: center; } .modal { will-change: transform; transform: translateY(0px) scale(0.95); opacity: 0; min-height: 120px; max-height: calc(100vh - 40px); background: ${cssManager.bdTheme('#ffffff', '#111')}; border-radius: 8px; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; transition: all 0.2s; overflow: hidden; box-shadow: ${cssManager.bdTheme('0px 2px 10px rgba(0, 0, 0, 0.1)', '0px 2px 5px rgba(0, 0, 0, 0.5)')}; margin: 20px; display: flex; flex-direction: column; overscroll-behavior: contain; } /* Width variations */ .modal.width-small { width: 380px; } .modal.width-medium { width: 560px; } .modal.width-large { width: 800px; } .modal.width-fullscreen { width: calc(100vw - 40px); height: calc(100vh - 40px); max-height: calc(100vh - 40px); } @media (max-width: 768px) { .modal { width: calc(100vw - 40px) !important; max-width: none !important; } /* Allow full height on mobile when content needs it */ .modalContainer { padding: 10px; } .modal { margin: 10px; max-height: calc(100vh - 20px); } /* Full screen mode on mobile */ .modal.mobile-fullscreen { width: 100vw !important; height: 100vh !important; max-height: 100vh !important; margin: 0; border-radius: 0; } } .modal.show { opacity: 1; transform: translateY(0px) scale(1); } .modal.show.predestroy { opacity: 0; transform: translateY(10px) scale(1); } .modal .heading { height: 40px; min-height: 40px; font-family: 'Geist Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; position: relative; flex-shrink: 0; } .modal .heading .header-buttons { display: flex; align-items: center; gap: 4px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); } .modal .heading .header-button { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; background: transparent; color: ${cssManager.bdTheme('#666', '#999')}; } .modal .heading .header-button:hover { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')}; color: ${cssManager.bdTheme('#333', '#fff')}; } .modal .heading .header-button:active { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(255, 255, 255, 0.12)')}; } .modal .heading .header-button dees-icon { width: 16px; height: 16px; display: block; } .modal .heading .heading-text { flex: 1; text-align: center; font-weight: 600; font-size: 14px; line-height: 40px; padding: 0 40px; } .modal .content { padding: 16px; flex: 1; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; } .modal .bottomButtons { display: flex; flex-direction: row; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; justify-content: flex-end; gap: 8px; padding: 8px; flex-shrink: 0; } .modal .bottomButtons .bottomButton { padding: 8px 16px; border-radius: 6px; line-height: 16px; text-align: center; font-size: 14px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.2s; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; white-space: nowrap; } .modal .bottomButtons .bottomButton:hover { background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; color: #ffffff; } .modal .bottomButtons .bottomButton:active { background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; color: #ffffff; } .modal .bottomButtons .bottomButton:last-child { border-right: none; } .modal .bottomButtons .bottomButton.primary { background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; color: #ffffff; } .modal .bottomButtons .bottomButton.primary:hover { background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; } .modal .bottomButtons .bottomButton.primary:active { background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)}; } `, ]; public render(): TemplateResult { const widthClass = typeof this.width === 'string' ? `width-${this.width}` : ''; const customWidth = typeof this.width === 'number' ? `${this.width}px` : ''; const maxWidthStyle = this.maxWidth ? `${this.maxWidth}px` : ''; const minWidthStyle = this.minWidth ? `${this.minWidth}px` : ''; const mobileFullscreenClass = this.mobileFullscreen ? 'mobile-fullscreen' : ''; return html`
`; } private windowLayer: DeesWindowLayer; public async firstUpdated(_changedProperties: Map) { super.firstUpdated(_changedProperties); const domtools = await this.domtoolsPromise; await domtools.convenience.smartdelay.delayFor(30); const modal = this.shadowRoot.querySelector('.modal'); modal.classList.add('show'); } public async handleOutsideClick(eventArg: MouseEvent) { eventArg.stopPropagation(); const modalContainer = this.shadowRoot.querySelector('.modalContainer'); if (eventArg.target === modalContainer) { await this.destroy(); } } public async destroy() { const domtools = await this.domtoolsPromise; const modal = this.shadowRoot.querySelector('.modal'); modal.classList.add('predestroy'); await domtools.convenience.smartdelay.delayFor(200); document.body.removeChild(this); await this.windowLayer.destroy(); // Unregister from z-index registry zIndexRegistry.unregister(this); } private async handleHelp() { if (this.onHelp) { await this.onHelp(); } } }