const frameHtml = ` Frame ${'<'}script type="module"> let frame = undefined addEventListener('message', event => { const isChild = ( frame !== undefined && event.source == frame.contentWindow ) if (isChild) { const transferArg = ( event.ports?.length ? [[...event.ports]] : [] ) parent.postMessage(event.data, '*', ...transferArg) } 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) { const transferArg = ( event.ports?.length ? [[...event.ports]] : [] ) frame.contentWindow.postMessage( event.data, '*', ...transferArg ) } } }) ${' ` export class FileGroupPage extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) this.viewLoaded = false this.onceOnLoaded = undefined this.isGroup = true } 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 { visibility: hidden; } :host(.viewing) iframe.edit { visibility: hidden; } ` 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') const csp = this.csp if (csp !== undefined) { frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" const url = new URL( '/-/frame', location.href ) url.searchParams.set('csp', 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') const csp = this.csp if (csp !== undefined) { frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" const url = new URL( '/-/frame', location.href ) url.searchParams.set('csp', 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, {codeMirror: this.settings.codeMirror}, ] 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) { if (Array.isArray(event.data)) { if (['get', 'put'].includes(event.data[0])) { await this.handleRequestMessage('view', event) } } } async handleEditMessage(event) { if (Array.isArray(event.data)) { if (event.data[0] === 'ready') { this.editFrame.contentWindow.postMessage( [ 'doc', this.body, {codeMirror: !!(this.settings.codeMirror)} ], '*' ) } 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 } else if (['get', 'put'].includes(event.data[0])) { await this.handleRequestMessage('edit', event) } else if (event.data[0] === 'load') { if (event.data[1] === '/editor-lib-codemirror/codemirror-bundle.js') { const url = event.data[1] const resp = await fetch(url) const text = await resp.text() const port = event.ports[0] port.postMessage({ status: 200, body: text, }) port.close() } } } } async handleRequestMessage(frame, event) { if (event.ports.length === 1) { const port = event.ports[0] const method = event.data[0] const path = event.data[1] const access = this.getAccess(path) if (method === 'get') { if (['read', 'readWrite'].includes(access)) { port.postMessage({ status: 200, body: this.getPage(path), }) } else { port.postMessage({ status: 401, }) } } else if (method === 'put') { const body = event.data[2] if (access === 'readWrite') { this.setPage(path, body) port.postMessage({ status: 200, }) } else { port.postMessage({ status: 401, }) } } port.close() } else { throw new Error('request message without port') } } getAccess(path) { const settings = this.settings const outbound = settings.connections?.outbound if (outbound && outbound[path]) { return outbound[path] } } getPage(path) { const body = this.storage.getItem(path) try { return JSON.parse(body) } catch (err) { return body } } setPage(path, body) { let value = body if (typeof value !== 'string') { value = JSON.stringify(value) } this.storage.setItem(path, value) } set body(value) { try { this.storage.setItem(this.path, value) } catch (err) { console.error(err) } } get body() { try { return this.storage.getItem(this.path) } catch (err) { console.error(err) return '' } } set settings(value) { try { this.storage.setItem( 'settings/page:' + this.path, JSON.stringify(value) ) } catch (err) { console.error(err) } } get settings() { let data try { data = this.storage.getItem( 'settings/page:' + this.path ) if (data === null || data === undefined) { return {} } } catch (err) { console.error(err) return {} } try { return JSON.parse(data) } catch (err) { return {} } } get csp() { if (this.cspOff) { return undefined } else { return ( this.settings.networkAccess in this.cspProfiles ? this.cspProfiles[this.settings.networkAccess] : this.cspProfiles.local ) } } 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 } }