Compare commits

..

No commits in common. 'main' and 'integrate-editor' have entirely different histories.

@ -1,33 +1,14 @@
import { Storage } from "/storage/storage.js"
import { Layout } from "/components/layout.js" import { Layout } from "/components/layout.js"
import { Page } from "/components/page.js" import { Page } from "/components/page.js"
import { PageActions } from "/components/page-actions.js"
import { FileGroupPage } from "/components/file-group-page.js" import { FileGroupPage } from "/components/file-group-page.js"
import { Header } from "/components/header.js" import { Header } from "/components/header.js"
import { NavMenu } from "/components/nav-menu.js" import { NavMenu } from "/components/nav-menu.js"
import { Dialog } from "/dialog/dialog.js" import { Dialog } from "/dialog/dialog.js"
import { ButtonGroup } from "/forms/button-group.js" import { ButtonGroup } from "/forms/button-group.js"
import { Dropdown } from "/menu/dropdown.js" import { Dropdown } from "/menu/dropdown.js"
import { NetworkSettings } from "/settings/network-settings.js"
import { PageSettings } from "/settings/page-settings.js"
import { Connections } from "/settings/connections.js"
import { ConnectionEdit } from "/settings/connection-edit.js"
customElements.define('m-layout', Layout) customElements.define('m-layout', Layout)
customElements.define('m-page', Page) customElements.define('m-page', Page)
customElements.define('m-page-actions', PageActions)
customElements.define(
'm-settings-network-settings', NetworkSettings
)
customElements.define(
'm-settings-page-settings', PageSettings
)
customElements.define(
'm-settings-connections', Connections
)
customElements.define(
'm-settings-connection-edit', ConnectionEdit
)
customElements.define( customElements.define(
'm-file-group-page', FileGroupPage 'm-file-group-page', FileGroupPage
) )
@ -42,13 +23,6 @@ customElements.define(
) )
class Setup { class Setup {
constructor() {
this.layout = document.createElement(
'm-layout'
)
this.layout.storage = new Storage()
}
async runWithSw() { async runWithSw() {
navigator.serviceWorker.addEventListener( navigator.serviceWorker.addEventListener(
'controllerchange', 'controllerchange',
@ -63,10 +37,12 @@ class Setup {
} }
async runWithoutSw() { async runWithoutSw() {
this.layout.storage = new Storage() const layout = document.createElement(
this.layout.csp = undefined 'm-layout'
this.layout.header.menu.handleLinks = true )
document.body.appendChild(this.layout) layout.csp = undefined
layout.header.menu.handleLinks = true
document.body.appendChild(layout)
} }
async register() { async register() {
@ -76,9 +52,6 @@ class Setup {
'/sw.js', '/sw.js',
{scope: '/'} {scope: '/'}
) )
if (this.registration.waiting) {
this.registration.active.postMessage(['skipWaiting'])
}
} catch (err) { } catch (err) {
console.error( console.error(
'error registering service worker', err 'error registering service worker', err
@ -89,7 +62,7 @@ class Setup {
load() { load() {
if (this.registration.active) { if (this.registration.active) {
document.body.appendChild( document.body.appendChild(
this.layout document.createElement('m-layout')
) )
} }
} }

@ -29,10 +29,7 @@ addEventListener('message', event => {
frame !== undefined && event.source == frame.contentWindow frame !== undefined && event.source == frame.contentWindow
) )
if (isChild) { if (isChild) {
const transferArg = ( parent.postMessage(event.data, '*')
event.ports?.length ? [[...event.ports]] : []
)
parent.postMessage(event.data, '*', ...transferArg)
} else { } else {
let isNew = false let isNew = false
const d = event.data const d = event.data
@ -47,12 +44,7 @@ addEventListener('message', event => {
document.body.appendChild(frame) document.body.appendChild(frame)
} }
} else if (frame !== undefined) { } else if (frame !== undefined) {
const transferArg = ( frame.contentWindow.postMessage(event.data, '*')
event.ports?.length ? [[...event.ports]] : []
)
frame.contentWindow.postMessage(
event.data, '*', ...transferArg
)
} }
} }
}) })
@ -64,9 +56,7 @@ export class FileGroupPage extends HTMLElement {
constructor() { constructor() {
super() super()
this.attachShadow({mode: 'open'}) this.attachShadow({mode: 'open'})
this.viewLoaded = false this.csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'"
this.onceOnLoaded = undefined
this.isGroup = true
} }
connectedCallback() { connectedCallback() {
@ -88,10 +78,10 @@ export class FileGroupPage extends HTMLElement {
width: 100%; width: 100%;
} }
:host(.editing) iframe.view { :host(.editing) iframe.view {
visibility: hidden; display: none;
} }
:host(.viewing) iframe.edit { :host(.viewing) iframe.edit {
visibility: hidden; display: none;
} }
` `
this.shadowRoot.append(style) this.shadowRoot.append(style)
@ -108,13 +98,12 @@ export class FileGroupPage extends HTMLElement {
initEditFrame() { initEditFrame() {
const frame = document.createElement('iframe') const frame = document.createElement('iframe')
frame.classList.add('edit') frame.classList.add('edit')
const csp = this.csp if (this.csp !== undefined) {
if (csp !== undefined) {
frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation"
const url = new URL( const url = new URL(
'/-/frame', location.href '/-/frame', location.href
) )
url.searchParams.set('csp', csp) url.searchParams.set('csp', this.csp)
url.searchParams.set('html', frameHtml) url.searchParams.set('html', frameHtml)
frame.src = url.href frame.src = url.href
} else { } else {
@ -134,13 +123,12 @@ export class FileGroupPage extends HTMLElement {
initViewFrame() { initViewFrame() {
const frame = document.createElement('iframe') const frame = document.createElement('iframe')
frame.classList.add('view') frame.classList.add('view')
const csp = this.csp if (this.csp !== undefined) {
if (csp !== undefined) {
frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation"
const url = new URL( const url = new URL(
'/-/frame', location.href '/-/frame', location.href
) )
url.searchParams.set('csp', csp) url.searchParams.set('csp', this.csp)
url.searchParams.set('html', frameHtml) url.searchParams.set('html', frameHtml)
frame.src = url.href frame.src = url.href
} else { } else {
@ -148,11 +136,7 @@ export class FileGroupPage extends HTMLElement {
frame.srcdoc = frameHtml frame.srcdoc = frameHtml
} }
frame.addEventListener('load', () => { frame.addEventListener('load', () => {
this.viewLoaded = true this.displayView()
if (this.onceOnLoaded) {
this.onceOnLoaded()
this.onceOnLoaded = undefined
}
}) })
this.viewFrame = frame this.viewFrame = frame
this.shadowRoot.append(frame) this.shadowRoot.append(frame)
@ -160,26 +144,14 @@ export class FileGroupPage extends HTMLElement {
displayView(doc) { displayView(doc) {
const msg = ['srcdoc', doc] const msg = ['srcdoc', doc]
if (this.viewLoaded) { this.viewFrame.contentWindow.postMessage(
this.viewFrame.contentWindow.postMessage( msg, '*'
msg, '*' )
)
} else {
this.onceOnLoaded = () => {
this.viewFrame.contentWindow.postMessage(
msg, '*'
)
}
}
} }
async displayEdit() { async displayEdit() {
const doc = await this.editorBuild.build() const doc = await this.editorBuild.build()
const msg = [ const msg = ['srcdoc', doc]
'srcdoc',
doc,
{codeMirror: this.settings.codeMirror},
]
this.editFrame.contentWindow.postMessage( this.editFrame.contentWindow.postMessage(
msg, '*' msg, '*'
) )
@ -196,23 +168,13 @@ export class FileGroupPage extends HTMLElement {
} }
async 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) { async handleEditMessage(event) {
if (Array.isArray(event.data)) { if (Array.isArray(event.data)) {
if (event.data[0] === 'ready') { if (event.data[0] === 'ready') {
this.editFrame.contentWindow.postMessage( this.editFrame.contentWindow.postMessage(
[ ['doc', this.body], '*'
'doc',
this.body,
{codeMirror: !!(this.settings.codeMirror)}
],
'*'
) )
} else if (event.data[0] === 'html') { } else if (event.data[0] === 'html') {
const html = event.data[1] const html = event.data[1]
@ -220,88 +182,13 @@ export class FileGroupPage extends HTMLElement {
} else if (event.data[0] === 'save') { } else if (event.data[0] === 'save') {
const doc = event.data[1] const doc = event.data[1]
this.body = doc 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) { set body(value) {
try { try {
this.storage.setItem(this.path, value) localStorage.setItem(this.path, value)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -309,56 +196,13 @@ export class FileGroupPage extends HTMLElement {
get body() { get body() {
try { try {
return this.storage.getItem(this.path) return localStorage.getItem(this.path)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return '' 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) { set editing(value) {
this._editing = value this._editing = value
if (this.shadowRoot.host) { if (this.shadowRoot.host) {

@ -19,6 +19,9 @@ export class Header extends HTMLElement {
} }
textEn = { textEn = {
download: 'Download',
rename: 'Move/Rename',
delete: 'Delete',
confirmDelete: f => ( confirmDelete: f => (
`Are you sure you want to delete ${f}?` `Are you sure you want to delete ${f}?`
), ),
@ -28,10 +31,12 @@ export class Header extends HTMLElement {
htmlCss: 'HTML/CSS', htmlCss: 'HTML/CSS',
singleFile: 'Single File', singleFile: 'Single File',
newPage: 'New Page', newPage: 'New Page',
go: 'Go',
} }
textEs = { textEs = {
download: 'Descargar',
rename: 'Mover/Renombrar',
delete: 'Borrar',
confirmDelete: f => ( confirmDelete: f => (
`¿Desea borrar ${f}?` `¿Desea borrar ${f}?`
), ),
@ -41,7 +46,6 @@ export class Header extends HTMLElement {
htmlCss: 'HTML/CSS', htmlCss: 'HTML/CSS',
singleFile: 'Archivo único', singleFile: 'Archivo único',
newPage: 'Nueva Página', newPage: 'Nueva Página',
go: 'Ir',
} }
constructor() { constructor() {
@ -52,12 +56,11 @@ export class Header extends HTMLElement {
this.appBar = document.createElement('div') this.appBar = document.createElement('div')
this.appBar.classList.add('app-bar') this.appBar.classList.add('app-bar')
this.shadowRoot.appendChild(this.appBar) this.shadowRoot.appendChild(this.appBar)
const navBtn = this.addButton( this.addButton(this.icons.menu, 'nav', () => {
this.icons.menu, this.menu.pages = this.getPages()
'nav', this.menuPanel.classList.add('open')
() => this.openNav(), this.overlay.classList.add('open')
) }).classList.add('left-end')
navBtn.classList.add('left-end')
this.addButton(this.icons.add, 'add', () => { this.addButton(this.icons.add, 'add', () => {
this.addPage() this.addPage()
}) })
@ -71,16 +74,14 @@ export class Header extends HTMLElement {
this.icons.dot, this.icons.dot,
'page', 'page',
() => { () => {
const actions = this.pageActions.menuActions
this.pageMenu.clear()
for (const {text, click} of actions) {
this.pageMenu.add(text, click)
}
this.pageMenu.open(this.pageButton) this.pageMenu.open(this.pageButton)
} }
) )
this.pageButton.classList.add('right-end') this.pageButton.classList.add('right-end')
this.addPageMenu()
this.addMenu() this.addMenu()
this.dialogWrap = document.createElement('div')
this.shadowRoot.appendChild(this.dialogWrap)
} }
connectedCallback() { connectedCallback() {
@ -117,9 +118,9 @@ export class Header extends HTMLElement {
div.menu { div.menu {
position: fixed; position: fixed;
top: 0; top: 0;
left: max(-90vw, -480px); left: -90vw;
height: 100vh; height: 100vh;
width: min(90vw, 480px); width: 90vw;
background-color: #fff; background-color: #fff;
transition: left .25s ease-in-out; transition: left .25s ease-in-out;
} }
@ -147,9 +148,11 @@ export class Header extends HTMLElement {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
m-dialog::part(footer) {
padding-top: 15px;
}
` `
this.shadowRoot.append(style) this.shadowRoot.append(style)
this.addPageMenu()
} }
encodePath(path) { encodePath(path) {
@ -188,28 +191,94 @@ export class Header extends HTMLElement {
this.menu.addEventListener('close-menu', () => { this.menu.addEventListener('close-menu', () => {
this.close() this.close()
}) })
this.menu.addEventListener(
'click-location',
() => {
this.close()
this.navPage()
},
)
} }
addPageMenu() { addPageMenu() {
this.pageMenu = document.createElement( this.pageMenu = document.createElement(
'm-menu-dropdown' 'm-menu-dropdown'
) )
this.shadowRoot.appendChild(this.pageMenu) this.pageMenu.add(this.text.download, () => {
} const text = localStorage.getItem(this.path)
const sp = this.path.split('/')
const filename = sp[sp.length - 1]
const el = document.createElement('a')
el.setAttribute(
'href',
'data:text/plain;charset=utf-8,' +
encodeURIComponent(text)
)
el.setAttribute('download', filename)
el.style.display = 'none'
this.shadowRoot.appendChild(el)
el.click()
this.shadowRoot.removeChild(el)
})
this.pageMenu.add(this.text.rename, () => {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = this.path
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
let errorEl
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.rename, () => {
const newPath = input.value
const v = localStorage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
errorEl.style.color = 'red'
const errText = this.text.alreadyExists
errorEl.innerText = errText
dialog.bodyEl.appendChild(errorEl)
}
return
}
localStorage.setItem(
newPath,
localStorage.getItem(this.path)
)
localStorage.removeItem(this.path)
dialog.close()
location.hash = newPath
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
})
this.pageMenu.add(this.text.delete, () => {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const p = document.createElement('p')
p.innerText = this.text.confirmDelete(
JSON.stringify(this.path)
)
dialog.bodyEl.appendChild(p)
openNav() { const bGroup = document.createElement(
this.menu.location = this.path 'm-forms-button-group'
this.menu.pages = this.getPages() )
this.menu.storageUse = this.getStorageUse() bGroup.addPrimary(this.text.delete, () => {
this.menuPanel.classList.add('open') localStorage.removeItem(this.path)
this.overlay.classList.add('open') location.hash = '/'
dialog.close()
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
})
this.shadowRoot.appendChild(this.pageMenu)
} }
close() { close() {
@ -222,21 +291,7 @@ export class Header extends HTMLElement {
} }
getPages() { getPages() {
return ( return Object.keys(localStorage).slice().sort()
this.storage.keys().slice()
.filter(s => s.startsWith('/'))
.sort()
)
}
getStorageUse() {
let bytes = 0
const entries = this.storage.entries()
for (const [k, v] of entries) {
bytes += k.length
bytes += v.length
}
return bytes / 5000000
} }
set editing(value) { set editing(value) {
@ -254,22 +309,6 @@ export class Header extends HTMLElement {
return this.editing ? this.icons.check : this.icons.edit return this.editing ? this.icons.check : this.icons.edit
} }
getTemplates() {
const text = this.storage.getItem(
'settings:templates'
)
if (text ?? undefined !== undefined) {
const data = JSON.parse(text)
return Object.entries(
data.templates
).map(([id, data]) => (
{...data, id}
))
} else {
return []
}
}
addPage() { addPage() {
const dialog = document.createElement('m-dialog') const dialog = document.createElement('m-dialog')
this.dialogWrap.replaceChildren(dialog) this.dialogWrap.replaceChildren(dialog)
@ -285,13 +324,6 @@ export class Header extends HTMLElement {
el.innerText = this.text[value] el.innerText = this.text[value]
return el return el
})) }))
const templates = this.getTemplates()
select.append(...templates.map(t => {
const el = document.createElement('option')
el.value = t.id
el.innerText = t.name
return el
}))
select.value = 'htmlCss' select.value = 'htmlCss'
input.addEventListener('input', e => { input.addEventListener('input', e => {
const ext = e.target.value.match(/\.\w+$/) const ext = e.target.value.match(/\.\w+$/)
@ -306,7 +338,7 @@ export class Header extends HTMLElement {
) )
bGroup.addPrimary(this.text.createPage, () => { bGroup.addPrimary(this.text.createPage, () => {
const newPath = this.encodePath(input.value) const newPath = this.encodePath(input.value)
const v = this.storage.getItem(newPath) const v = localStorage.getItem(newPath)
if (v !== null || newPath === this.path) { if (v !== null || newPath === this.path) {
if (!errorEl) { if (!errorEl) {
errorEl = document.createElement('p') errorEl = document.createElement('p')
@ -318,9 +350,10 @@ export class Header extends HTMLElement {
} }
return return
} }
let value const value = (
if (select.value === 'htmlCss') { select.value === 'singleFile' ?
value = JSON.stringify({ '' :
JSON.stringify({
type: 'm-file-group', type: 'm-file-group',
files: [ files: [
{ {
@ -333,15 +366,8 @@ export class Header extends HTMLElement {
}, },
], ],
}) })
} else {
value = this.storage.getItem(
select.value
) ?? ''
}
this.storage.setItem(newPath, value)
this.storage.setItem(
'settings/page:' + newPath, '{}'
) )
localStorage.setItem(newPath, value)
location.hash = newPath location.hash = newPath
dialog.close() dialog.close()
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
@ -355,30 +381,6 @@ export class Header extends HTMLElement {
dialog.open() dialog.open()
} }
navPage() {
const dialog = document.createElement('m-dialog')
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = this.path
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.go, () => {
dialog.close()
location.hash = (
input.value.startsWith('/') ?
input.value : '/'
)
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
get language() { get language() {
return this._language return this._language
} }

@ -1,29 +1,11 @@
import { EditorBuild } from "/loader/editor-build.js" import { EditorBuild } from "/loader/editor-build.js"
export class Layout extends HTMLElement { export class Layout extends HTMLElement {
cspProfiles = {
local: "default-src 'self' 'unsafe-inline' 'unsafe-eval'",
unpkg: "default-src unpkg.com 'self' 'unsafe-inline' 'unsafe-eval'",
open: undefined,
}
constructor() { constructor() {
super() super()
this.attachShadow({mode: 'open'}) this.attachShadow({mode: 'open'})
this.csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'" this.csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'"
this.header = document.createElement('m-header') this.header = document.createElement('m-header')
this.dialogWrap = document.createElement('div')
this.header.dialogWrap = this.dialogWrap
this.pageActions = document.createElement(
'm-page-actions'
)
this.pageActions.cspProfiles = this.cspProfiles
this.header.pageActions = this.pageActions
this.shadowRoot.append(
this.header,
this.dialogWrap,
this.pageActions,
)
} }
connectedCallback() { connectedCallback() {
@ -42,9 +24,6 @@ export class Layout extends HTMLElement {
m-page, m-file-group-page { m-page, m-file-group-page {
flex-grow: 1; flex-grow: 1;
} }
m-dialog::part(footer) {
padding-top: 15px;
}
` `
this.shadowRoot.appendChild(style) this.shadowRoot.appendChild(style)
this.header.editing = this.editing this.header.editing = this.editing
@ -54,6 +33,7 @@ export class Layout extends HTMLElement {
this.header.addEventListener('create-page', () => { this.header.addEventListener('create-page', () => {
this.editing = true this.editing = true
}) })
this.shadowRoot.appendChild(this.header)
this.load() this.load()
addEventListener('hashchange', () => { addEventListener('hashchange', () => {
this.load() this.load()
@ -61,17 +41,13 @@ export class Layout extends HTMLElement {
this.addEventListener('hash-change', () => { this.addEventListener('hash-change', () => {
this.load() this.load()
}) })
this.addEventListener('settings-change', () => {
this.load()
})
} }
load() { load() {
const path = this.path const path = this.path
this.editing = false
const prevPage = this.page const prevPage = this.page
let isGroup = false let isGroup = false
const body = this.storage.getItem(path) || '' const body = localStorage.getItem(path) || ''
if (body.match(/^\s*{/)) { if (body.match(/^\s*{/)) {
try { try {
const bodyData = JSON.parse(body) const bodyData = JSON.parse(body)
@ -86,13 +62,7 @@ export class Layout extends HTMLElement {
this.page = document.createElement( this.page = document.createElement(
isGroup ? 'm-file-group-page' : 'm-page' isGroup ? 'm-file-group-page' : 'm-page'
) )
this.page.storage = this.storage this.page.csp = this.csp
if (isGroup) {
this.page.cspOff = this.csp === undefined
this.page.cspProfiles = this.cspProfiles
} else {
this.page.csp = this.csp
}
if (isGroup) { if (isGroup) {
this.page.editorBuild = this.editorBuild this.page.editorBuild = this.editorBuild
} }
@ -103,8 +73,6 @@ export class Layout extends HTMLElement {
} }
this.shadowRoot.appendChild(this.page) this.shadowRoot.appendChild(this.page)
this.header.path = path this.header.path = path
this.pageActions.path = path
this.pageActions.page = this.page
} }
get path() { get path() {
@ -118,7 +86,7 @@ export class Layout extends HTMLElement {
} }
set editing(value) { set editing(value) {
this.storage.session.setItem( sessionStorage.setItem(
'editing', value ? 'true' : 'false' 'editing', value ? 'true' : 'false'
) )
this.header.editing = this.editing this.header.editing = this.editing
@ -130,7 +98,7 @@ export class Layout extends HTMLElement {
get editing() { get editing() {
try { try {
return ( return (
this.storage.session.getItem('editing') === 'true' sessionStorage.getItem('editing') === 'true'
) )
} catch (e) { } catch (e) {
return false return false
@ -143,14 +111,4 @@ export class Layout extends HTMLElement {
} }
return this._editorBuild return this._editorBuild
} }
set storage(value) {
this._storage = value
this.header.storage = value
this.pageActions.storage = value
}
get storage() {
return this._storage
}
} }

@ -1,17 +1,4 @@
export class NavMenu extends HTMLElement { export class NavMenu extends HTMLElement {
textByLang = {
en: {
pages: 'Pages',
localStorage: 'Local storage',
full: 'full',
},
es: {
pages: 'Páginas',
localStorage: 'Almacenamiento local',
full: 'lleno',
},
}
constructor() { constructor() {
super() super()
this.attachShadow({mode: 'open'}) this.attachShadow({mode: 'open'})
@ -27,21 +14,8 @@ export class NavMenu extends HTMLElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: #000; color: #000;
height: 100dvh;
max-height: 100dvh; max-height: 100dvh;
} }
.location-bar {
padding: 5px 25px;
display: flex;
flex-direction: row;
}
.location-box {
border: 2px solid #aaa;
border-radius: 3px;
flex-grow: 1;
padding: 5px;
font-size: 14px;
}
h1 { h1 {
width: 100%; width: 100%;
text-align: center; text-align: center;
@ -58,45 +32,15 @@ export class NavMenu extends HTMLElement {
display: block; display: block;
margin: 8px 10px; margin: 8px 10px;
} }
.footer {
padding: 3px 5px;
}
` `
this.shadowRoot.append(style) this.shadowRoot.append(style)
this.locationBar = document.createElement(
'div'
)
this.locationBar.classList.add(
'location-bar'
)
this.locationBox = document.createElement(
'div'
)
this.locationBox.classList.add(
'location-box'
)
this.locationBar.append(this.locationBox)
this.locationBox.addEventListener(
'click',
() => {
this.dispatchEvent(new CustomEvent(
'click-location', {bubbles: true}
))
}
)
this.shadowRoot.append(this.locationBar)
this.renderLocation()
this.header = document.createElement('h1') this.header = document.createElement('h1')
this.header.innerText = this.text.pages this.header.innerText = this.pagesText
this.shadowRoot.append(this.header) this.shadowRoot.append(this.header)
this.pageList = document.createElement('div') this.pageList = document.createElement('div')
this.pageList.classList.add('page-list') this.pageList.classList.add('page-list')
this.shadowRoot.append(this.pageList) this.shadowRoot.append(this.pageList)
this.renderPageList() this.renderPageList()
this.footer = document.createElement('div')
this.footer.classList.add('footer')
this.shadowRoot.append(this.footer)
this.renderFooter()
this.shadowRoot.addEventListener('click', e => { this.shadowRoot.addEventListener('click', e => {
if (e.target.classList.contains('page')) { if (e.target.classList.contains('page')) {
if (this.handleLinks) { if (this.handleLinks) {
@ -119,28 +63,14 @@ export class NavMenu extends HTMLElement {
set language(language) { set language(language) {
this._language = language this._language = language
this.text = this.textByLang[
this.langEs ? 'es' : 'en'
]
} }
get langEs() { get langEs() {
return /^es\b/.test(this.language) return /^es\b/.test(this.language)
} }
set location(location) { get pagesText() {
this._location = location return this.langEs ? 'Páginas' : 'Pages'
if (this.locationBox) {
this.renderLocation()
}
}
get location() {
return this._location
}
renderLocation() {
this.locationBox.innerText = this.location
} }
set pages(pages) { set pages(pages) {
@ -164,35 +94,4 @@ export class NavMenu extends HTMLElement {
}) })
this.pageList.replaceChildren(...els) this.pageList.replaceChildren(...els)
} }
set storageUse(n) {
this._storageUse = n
if (this.footer) {
this.renderFooter()
}
}
get storageUse() {
return this._storageUse
}
renderFooter() {
if (this.storageUse !== undefined) {
const fmt = new Intl.NumberFormat(
this.language,
{
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
)
this.footer.innerText = (
this.text.localStorage + ' ' +
fmt.format(
this.storageUse
).replaceAll(/\s+/g, '') + ' ' +
this.text.full
)
}
}
} }

@ -1,377 +0,0 @@
export class PageActions extends HTMLElement {
textEn = {
download: 'Download',
rename: 'Move/Rename',
duplicate: 'Duplicate',
delete_: 'Delete',
settings: 'Settings',
confirmDelete: f => (
`Are you sure you want to delete ${f}?`
),
cancel: 'Cancel',
alreadyExists: 'There is already a page with that name.',
save: 'Guardar',
close: 'Close',
}
textEs = {
download: 'Descargar',
rename: 'Mover/Renombrar',
duplicate: 'Duplicar',
delete_: 'Borrar',
settings: 'Configuración',
confirmDelete: f => (
`¿Desea borrar ${f}?`
),
cancel: 'Cancelar',
alreadyExists: 'Ya existe una página con ese nombre.',
save: 'Guardar',
close: 'Cerrar',
}
constructor() {
super()
this.attachShadow({mode: 'open'})
this.language = navigator.language
this.dialogWrap = document.createElement('div')
this.shadowRoot.append(this.dialogWrap)
}
connectedCallback() {
const style = document.createElement('style')
style.textContent = `
m-dialog::part(footer) {
padding-top: 15px;
}
`
this.shadowRoot.appendChild(style)
}
get menuActions() {
const baseActions = [
{
text: this.text.download,
click: this.download.bind(this),
},
{
text: this.text.rename,
click: this.rename.bind(this),
},
{
text: this.text.duplicate,
click: this.duplicate.bind(this),
},
{
text: this.text.delete_,
click: this.delete_.bind(this),
},
]
if (this.page.isGroup) {
return [
...baseActions,
{
text: this.text.settings,
click: this.settings.bind(this),
},
]
} else {
return baseActions
}
}
download() {
const text = this.storage.getItem(this.path)
const sp = this.path.split('/')
const filename = sp[sp.length - 1]
const el = document.createElement('a')
el.setAttribute(
'href',
'data:text/plain;charset=utf-8,' +
encodeURIComponent(text)
)
el.setAttribute('download', filename)
el.style.display = 'none'
this.shadowRoot.appendChild(el)
el.click()
this.shadowRoot.removeChild(el)
}
rename() {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = this.path
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
let errorEl
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.rename, () => {
const newPath = input.value
const v = this.storage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
errorEl.style.color = 'red'
const errText = this.text.alreadyExists
errorEl.innerText = errText
dialog.bodyEl.appendChild(errorEl)
}
return
}
const sKeyOld = 'settings/page:' + this.path
const sKeyNew = 'settings/page:' + newPath
const settingsJson = this.storage.getItem(sKeyOld)
if (settingsJson ?? true === true) {
this.storage.setItem(sKeyNew, settingsJson)
this.storage.removeItem(sKeyOld)
let settingsData
try {
settingsData = JSON.parse(settingsJson)
} catch (err) {
settingsData = {}
}
if (settingsData?.connections) {
for (const dir of ['outbound', 'inbound']) {
const otherDir = (
dir === 'outbound' ? 'inbound' : 'outbound'
)
this.applyInverseRename(
settingsData, dir, otherDir, this.path, newPath
)
}
}
} else {
this.storage.removeItem(sKeyNew)
}
this.storage.setItem(
newPath,
this.storage.getItem(this.path)
)
this.storage.removeItem(this.path)
dialog.close()
location.hash = newPath
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
duplicate() {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = this.path
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
let errorEl
const bGroup = document.createElement(
'm-forms-button-group'
)
const btnText = this.text.duplicate
bGroup.addPrimary(btnText, () => {
const newPath = input.value
const v = this.storage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
errorEl.style.color = 'red'
const errText = this.text.alreadyExists
errorEl.innerText = errText
dialog.bodyEl.appendChild(errorEl)
}
return
}
this.storage.setItem(
newPath,
this.storage.getItem(this.path)
)
this.storage.setItem(
'settings/page:' + newPath,
this.storage.getItem(this.path) ?? '{}'
)
dialog.close()
location.hash = newPath
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
delete_() {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const p = document.createElement('p')
p.innerText = this.text.confirmDelete(
JSON.stringify(this.path)
)
dialog.bodyEl.appendChild(p)
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.delete_, () => {
this.storage.removeItem(this.path)
this.storage.removeItem(
'settings/page:' + this.path
)
location.hash = '/'
dialog.close()
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
settings() {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const settingsEl = document.createElement(
'm-settings-page-settings'
)
settingsEl.cspProfiles = this.cspProfiles
settingsEl.path = this.path
settingsEl.checkExists = path => (
this.storage.getItem(path) !== null
)
settingsEl.data = this.page.settings
const h = document.createElement('h2')
h.innerText = this.text.settings
dialog.headerEl.append(h)
dialog.bodyEl.append(settingsEl)
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.save, () => {
const settingsData = settingsEl.data
this.page.settings = settingsData
for (const dir of ['outbound', 'inbound']) {
const otherDir = (
dir === 'outbound' ? 'inbound' : 'outbound'
)
this.applyInverseSettings(
settingsData, dir, otherDir
)
}
this.applyTemplateSettings()
dialog.close()
this.dispatchEvent(new CustomEvent(
'settings-change', {bubbles: true, composed: true}
))
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
applyInverseSettings(settingsData, dir, otherDir) {
const selfEntries = Object.entries(
settingsData.connections[dir]
)
for (const [path, access] of selfEntries) {
const key = 'settings/page:' + path
let val = this.storage.getItem(key)
try {
if (val !== null) {
val = JSON.parse(val)
}
} catch (err) {
// ignore
}
const data = val ?? {}
data.connections = data.connections ?? {}
data.connections[otherDir] = (
data.connections[otherDir] ?? {}
)
data.connections[otherDir][this.path] = access
this.storage.setItem(
key, JSON.stringify(data)
)
}
}
applyInverseRename(
settingsData, dir, otherDir, oldPath, newPath
) {
const selfEntries = Object.entries(
settingsData.connections[dir] ?? {}
)
for (const [path, access] of selfEntries) {
const key = 'settings/page:' + path
let val = this.storage.getItem(key)
try {
if (val !== null) {
val = JSON.parse(val)
}
} catch (err) {
// ignore
}
const data = val ?? {}
data.connections = data.connections ?? {}
data.connections[otherDir] = (
data.connections[otherDir] ?? {}
)
const accessValue = data.connections[otherDir][oldPath]
data.connections[otherDir][newPath] = accessValue
data.connections[otherDir][oldPath] = undefined
this.storage.setItem(
key, JSON.stringify(data)
)
}
}
applyTemplateSettings() {
const templates = {}
for (const key of this.storage.keys()) {
try {
if (key.startsWith('settings/page:')) {
const data = JSON.parse(
this.storage.getItem(key)
)
if (data.template) {
const name = key.slice(
'settings/page:'.length
)
templates[name] = {name}
}
}
} catch (err) {
// do nothing
}
}
this.storage.setItem(
'settings:templates',
JSON.stringify({templates}),
)
}
get language() {
return this._language
}
set language(language) {
this._language = language
this.text = this.langEs ? this.textEs : this.textEn
}
get langEs() {
return /^es\b/.test(this.language)
}
}

@ -58,7 +58,6 @@ export class Page extends HTMLElement {
div.classList.add('twrap') div.classList.add('twrap')
div.appendChild(this.textArea) div.appendChild(this.textArea)
this.shadowRoot.appendChild(div) this.shadowRoot.appendChild(div)
this.isGroup = false
} }
connectedCallback() { connectedCallback() {
@ -74,7 +73,7 @@ export class Page extends HTMLElement {
align-items: stretch; align-items: stretch;
} }
div.twrap { div.twrap {
padding: 10px; padding: 10px 10px;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
@ -85,6 +84,9 @@ export class Page extends HTMLElement {
font-size: 0.90em; font-size: 0.90em;
height: 100%; height: 100%;
} }
textarea:focus {
height: 45vh;
}
iframe { iframe {
border: none; border: none;
margin: 0; margin: 0;
@ -141,7 +143,7 @@ export class Page extends HTMLElement {
set body(value) { set body(value) {
try { try {
this.storage.setItem(this.path, value) localStorage.setItem(this.path, value)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -149,7 +151,7 @@ export class Page extends HTMLElement {
get body() { get body() {
try { try {
return this.storage.getItem(this.path) return localStorage.getItem(this.path)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return '' return ''

17
sw.js

@ -7,25 +7,16 @@ async function initCache() {
'/components/layout.js', '/components/layout.js',
'/components/nav-menu.js', '/components/nav-menu.js',
'/components/page.js', '/components/page.js',
'/components/page-actions.js',
'/dialog/dialog.js', '/dialog/dialog.js',
'/editor/app.js', '/editor/app.js',
'/editor/file-group.js', '/editor/file-group.js',
'/editor/file-view.js', '/editor/file-view.js',
'/editor/text-edit.js', '/editor/text-edit.js',
'/editor/code-edit.js',
'/editor/header.js',
'/forms/button-group.js', '/forms/button-group.js',
'/index.html', '/index.html',
'/loader/builder.js', '/loader/builder.js',
'/loader/editor-build.js', '/loader/editor-build.js',
'/menu/dropdown.js', '/menu/dropdown.js',
'/settings/page-settings.js',
'/settings/network-settings.js',
'/settings/connections.js',
'/settings/connection-edit.js',
'/editor-lib-codemirror/codemirror-bundle.js',
'/storage/storage.js', //2
]) ])
} }
@ -42,7 +33,7 @@ async function cacheFirst(request) {
const csp = url.searchParams.get('csp') const csp = url.searchParams.get('csp')
return new Response(html, { return new Response(html, {
headers: { headers: {
'Content-Type': 'text/html; charset=utf-8', 'Content-Type': 'text/html',
'Content-Security-Policy': csp, 'Content-Security-Policy': csp,
} }
}) })
@ -63,9 +54,3 @@ self.addEventListener('fetch', event => {
self.addEventListener('activate', event => { self.addEventListener('activate', event => {
event.waitUntil(clients.claim()) event.waitUntil(clients.claim())
}) })
self.addEventListener('message', e => {
if (Array.isArray(e.data) && e.data[0] === 'skipWaiting') {
self.skipWaiting()
}
})
Loading…
Cancel
Save