const frameHtml = ` Frame ${'<'}script type="module"> let frame = undefined addEventListener('message', event => { const isChild = ( frame !== undefined && event.source == frame.contentWindow ) if (isChild) { parent.postMessage(event.data, '*') } else { let isNew = false const d = event.data if (Array.isArray(d) && d[0] === 'srcdoc') { isNew = frame === undefined if (isNew) { frame = document.createElement('iframe') frame.sandbox = "allow-scripts allow-top-navigation" } frame.srcdoc = d[1] if (isNew) { document.body.appendChild(frame) } } else if (frame !== undefined) { frame.contentWindow.postMessage(event.data, '*') } } }) ${' ` export class FileGroupPage extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) this.csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'" this.viewLoaded = false this.onceOnLoaded = undefined } connectedCallback() { const style = document.createElement('style') style.textContent = ` :host { display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: "main"; flex-direction: column; align-items: stretch; } iframe { border: none; margin: 0; padding: 0; grid-area: main; width: 100%; } :host(.editing) iframe.view { display: none; } :host(.viewing) iframe.edit { display: none; } ` this.shadowRoot.append(style) this.initEditFrame() this.initViewFrame() this.editing = this.editing addEventListener('message', this.handleMessage) } disconnectedCallback() { removeEventListener('message', this.handleMessage) } initEditFrame() { const frame = document.createElement('iframe') frame.classList.add('edit') if (this.csp !== undefined) { frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" const url = new URL( '/-/frame', location.href ) url.searchParams.set('csp', this.csp) url.searchParams.set('html', frameHtml) frame.src = url.href } else { frame.sandbox = "allow-scripts allow-top-navigation" frame.srcdoc = frameHtml } frame.addEventListener('load', () => { this.displayEdit() }) frame.addEventListener('message', message => { this.handleEditMessage(message) }) this.editFrame = frame this.shadowRoot.append(frame) } initViewFrame() { const frame = document.createElement('iframe') frame.classList.add('view') if (this.csp !== undefined) { frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" const url = new URL( '/-/frame', location.href ) url.searchParams.set('csp', this.csp) url.searchParams.set('html', frameHtml) frame.src = url.href } else { frame.sandbox = "allow-scripts allow-top-navigation" frame.srcdoc = frameHtml } frame.addEventListener('load', () => { this.viewLoaded = true if (this.onceOnLoaded) { this.onceOnLoaded() this.onceOnLoaded = undefined } }) this.viewFrame = frame this.shadowRoot.append(frame) } displayView(doc) { const msg = ['srcdoc', doc] if (this.viewLoaded) { this.viewFrame.contentWindow.postMessage( msg, '*' ) } else { this.onceOnLoaded = () => { this.viewFrame.contentWindow.postMessage( msg, '*' ) } } } async displayEdit() { const doc = await this.editorBuild.build() const msg = ['srcdoc', doc] this.editFrame.contentWindow.postMessage( msg, '*' ) } handleMessage = event => { const editWin = this.editFrame?.contentWindow const viewWin = this.viewFrame?.contentWindow if (editWin && event.source == editWin) { this.handleEditMessage(event) } else if (viewWin && event.source == viewWin) { this.handleViewMessage(event) } } async handleViewMessage(event) { } async handleEditMessage(event) { if (Array.isArray(event.data)) { if (event.data[0] === 'ready') { this.editFrame.contentWindow.postMessage( ['doc', this.body], '*' ) } else if (event.data[0] === 'html') { const html = event.data[1] this.displayView(html) } else if (event.data[0] === 'save') { const doc = event.data[1] this.body = doc } } } set body(value) { try { localStorage.setItem(this.path, value) } catch (err) { console.error(err) } } get body() { try { return localStorage.getItem(this.path) } catch (err) { console.error(err) return '' } } set editing(value) { this._editing = value if (this.shadowRoot.host) { const classes = this.shadowRoot.host.classList if (this.editing) { classes.add('editing') classes.remove('viewing') } else { classes.add('viewing') classes.remove('editing') if (this.editFrame) { this.editFrame.contentWindow.postMessage( ['request-html'], '*' ) } } } } get editing() { return this._editing } }