const closeSvg = ` `; import type { Theme } from '@penpot/plugin-types'; import { dragHandler } from '../drag-handler.js'; import modalCss from './plugin.modal.css?inline'; import { resizeModal } from '../create-modal.js'; const MIN_Z_INDEX = 3; export class PluginModalElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } wrapper = document.createElement('div'); #inner = document.createElement('div'); #dragEvents: ReturnType | null = null; setTheme(theme: Theme) { if (this.wrapper) { this.wrapper.setAttribute('data-theme', theme); } } resize(width: number, height: number) { if (this.wrapper) { resizeModal(this, width, height); } } disconnectedCallback() { this.#dragEvents?.(); } calculateZIndex() { const modals = document.querySelectorAll('plugin-modal'); const zIndexModals = Array.from(modals) .filter((modal) => modal !== this) .map((modal) => { return Number(modal.style.zIndex); }); const maxZIndex = Math.max(...zIndexModals, MIN_Z_INDEX); this.style.zIndex = (maxZIndex + 1).toString(); } connectedCallback() { const title = this.getAttribute('title'); const iframeSrc = this.getAttribute('iframe-src'); const allowDownloads = this.getAttribute('allow-downloads') || false; const allowClipboardRead = this.getAttribute('allow-clipboard-read') || false; const allowClipboardWrite = this.getAttribute('allow-clipboard-write') || false; if (!title || !iframeSrc) { throw new Error('title and iframe-src attributes are required'); } if (!this.shadowRoot) { throw new Error('Error creating shadow root'); } this.#inner.classList.add('inner'); this.wrapper.classList.add('wrapper'); this.wrapper.style.maxInlineSize = '90vw'; this.wrapper.style.maxBlockSize = '90vh'; const header = document.createElement('div'); header.classList.add('header'); const h1 = document.createElement('h1'); h1.textContent = title; header.appendChild(h1); const closeButton = document.createElement('button'); closeButton.setAttribute('type', 'button'); closeButton.innerHTML = `
${closeSvg}
`; closeButton.addEventListener('click', () => { if (!this.shadowRoot) { return; } this.shadowRoot.dispatchEvent( new CustomEvent('close', { composed: true, bubbles: true, }), ); }); header.appendChild(closeButton); const iframe = document.createElement('iframe'); iframe.src = iframeSrc; const allowList: string[] = []; if (allowClipboardRead) allowList.push('clipboard-read'); if (allowClipboardWrite) allowList.push('clipboard-write'); iframe.allow = allowList.join('; '); iframe.sandbox.add( 'allow-scripts', 'allow-forms', 'allow-modals', 'allow-popups', 'allow-popups-to-escape-sandbox', 'allow-storage-access-by-user-activation', 'allow-same-origin', ); if (allowDownloads) { iframe.sandbox.add('allow-downloads'); } iframe.addEventListener('load', () => { this.shadowRoot?.dispatchEvent( new CustomEvent('load', { composed: true, bubbles: true, }), ); }); // move modal to the top this.#dragEvents = dragHandler( header, this.wrapper, () => { this.calculateZIndex(); }, { start: () => { this.wrapper.classList.add('is-dragging'); }, end: () => { this.wrapper.classList.remove('is-dragging'); }, }, ); this.addEventListener('message', (e: Event) => { if (!iframe.contentWindow) { return; } try { iframe.contentWindow.postMessage((e as CustomEvent).detail, '*'); } catch (err) { console.error( 'plugin modal: failed to send message to iframe via postMessage.', err, ); } }); this.shadowRoot.appendChild(this.wrapper); this.wrapper.appendChild(this.#inner); this.#inner.appendChild(header); this.#inner.appendChild(iframe); const style = document.createElement('style'); style.textContent = modalCss; this.shadowRoot.appendChild(style); this.calculateZIndex(); } size() { const width = Number(this.wrapper.style.width.replace('px', '') || '300'); const height = Number(this.wrapper.style.height.replace('px', '') || '400'); return { width, height }; } } customElements.define('plugin-modal', PluginModalElement);