diff --git a/app.js b/app.js index 78bbbea..012f2f8 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,6 @@ import { Layout } from "/components/layout.js" import { Page } from "/components/page.js" +import { FileGroupPage } from "/components/file-group-page.js" import { Header } from "/components/header.js" import { NavMenu } from "/components/nav-menu.js" import { Dialog } from "/dialog/dialog.js" @@ -8,6 +9,9 @@ import { Dropdown } from "/menu/dropdown.js" customElements.define('m-layout', Layout) customElements.define('m-page', Page) +customElements.define( + 'm-file-group-page', FileGroupPage +) customElements.define('m-header', Header) customElements.define('m-nav-menu', NavMenu) customElements.define('m-dialog', Dialog) diff --git a/components/file-group-page.js b/components/file-group-page.js new file mode 100644 index 0000000..dab9dcf --- /dev/null +++ b/components/file-group-page.js @@ -0,0 +1,228 @@ +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'" + } + + 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.displayView() + }) + this.viewFrame = frame + this.shadowRoot.append(frame) + } + + displayView(doc) { + const msg = ['srcdoc', doc] + 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 + } +} \ No newline at end of file diff --git a/components/header.js b/components/header.js index 088a30f..04ff05e 100644 --- a/components/header.js +++ b/components/header.js @@ -28,6 +28,9 @@ export class Header extends HTMLElement { cancel: 'Cancel', alreadyExists: 'There is already a page with that name.', createPage: 'Create Page', + htmlCss: 'HTML/CSS', + singleFile: 'Single File', + newPage: 'New Page', } textEs = { @@ -40,6 +43,9 @@ export class Header extends HTMLElement { cancel: 'Cancelar', alreadyExists: 'Ya existe una página con ese nombre.', createPage: 'Crear Página', + htmlCss: 'HTML/CSS', + singleFile: 'Archivo único', + newPage: 'Nueva Página', } constructor() { @@ -310,6 +316,22 @@ export class Header extends HTMLElement { input.value = '/' input.style.minWidth = '300px' dialog.bodyEl.appendChild(input) + const select = document.createElement('select') + const options = ['htmlCss', 'singleFile'] + select.append(...options.map(value => { + const el = document.createElement('option') + el.value = value + el.innerText = this.text[value] + return el + })) + select.value = 'htmlCss' + input.addEventListener('input', e => { + const ext = e.target.value.match(/\.\w+$/) + select.value = ext ? 'singleFile' : 'htmlCss' + }) + select.style.marginTop = '10px' + select.style.marginBottom = '10px' + dialog.bodyEl.appendChild(select) let errorEl const bGroup = document.createElement( 'm-forms-button-group' @@ -328,7 +350,24 @@ export class Header extends HTMLElement { } return } - localStorage.setItem(newPath, '') + const value = ( + select.value === 'singleFile' ? + '' : + JSON.stringify({ + type: 'm-file-group', + files: [ + { + "name": "index.html", + "data": `

${this.text.newPage}

`, + }, + { + "name": "style.css", + "data": 'h1 { color: dodgerblue; }', + }, + ], + }) + ) + localStorage.setItem(newPath, value) location.hash = newPath dialog.close() this.dispatchEvent(new CustomEvent( diff --git a/components/layout.js b/components/layout.js index 40e1cf9..4468f06 100644 --- a/components/layout.js +++ b/components/layout.js @@ -1,3 +1,5 @@ +import { EditorBuild } from "/loader/editor-build.js" + export class Layout extends HTMLElement { constructor() { super() @@ -19,7 +21,7 @@ export class Layout extends HTMLElement { overflow-y: hidden; position: relative; } - m-page { + m-page, m-file-group-page { flex-grow: 1; } ` @@ -44,8 +46,26 @@ export class Layout extends HTMLElement { load() { const path = this.path const prevPage = this.page - this.page = document.createElement('m-page') + let isGroup = false + const body = localStorage.getItem(path) || '' + if (body.match(/^\s*{/)) { + try { + const bodyData = JSON.parse(body) + isGroup = ( + Array.isArray(bodyData?.files) && + bodyData.type === 'm-file-group' + ) + } catch (err) { + // do nothing, is not file group + } + } + this.page = document.createElement( + isGroup ? 'm-file-group-page' : 'm-page' + ) this.page.csp = this.csp + if (isGroup) { + this.page.editorBuild = this.editorBuild + } this.page.path = path this.editing = this.editing if (prevPage !== undefined) { @@ -84,4 +104,11 @@ export class Layout extends HTMLElement { return false } } + + get editorBuild() { + if (this._editorBuild === undefined) { + this._editorBuild = new EditorBuild() + } + return this._editorBuild + } } \ No newline at end of file diff --git a/components/page.js b/components/page.js index 3e1dd58..d77f271 100644 --- a/components/page.js +++ b/components/page.js @@ -35,7 +35,7 @@ addEventListener('message', event => { } frame.srcdoc = d[1] } else if (frame !== undefined) { - frame.postMessage(event.data) + frame.contentWindow.postMessage(event.data, '*') } if (isNew) { document.body.appendChild(frame) diff --git a/sw.js b/sw.js index 2978397..326907c 100644 --- a/sw.js +++ b/sw.js @@ -1,17 +1,23 @@ async function initCache() { const cache = await caches.open('v1') await cache.addAll([ - '/', - '/index.html', '/app.js', - '/components/page.js', - '/components/layout.js', + '/components/file-group-page.js', '/components/header.js', + '/components/layout.js', '/components/nav-menu.js', + '/components/page.js', '/dialog/dialog.js', + '/editor/app.js', + '/editor/file-group.js', + '/editor/file-view.js', + '/editor/text-edit.js', '/forms/button-group.js', + '/index.html', + '/loader/builder.js', + '/loader/editor-build.js', '/menu/dropdown.js', - ]) //1 + ]) } self.addEventListener("install", event => { @@ -47,4 +53,4 @@ self.addEventListener('fetch', event => { self.addEventListener('activate', event => { event.waitUntil(clients.claim()) -}) \ No newline at end of file +})