diff --git a/components/file-group-page.json b/components/file-group-page.json new file mode 100644 index 0000000..4895c5d --- /dev/null +++ b/components/file-group-page.json @@ -0,0 +1,363 @@ +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] + 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], '*' + ) + } 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) + } + } + } + + 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 = localStorage.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) + } + localStorage.setItem(path, value) + } + + 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 settings(value) { + try { + localStorage.setItem( + 'settings/page:' + this.path, + JSON.stringify(value) + ) + } catch (err) { + console.error(err) + } + } + + get settings() { + let data + try { + data = localStorage.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 + } +} \ No newline at end of file