Compare commits

...

88 Commits

Author SHA1 Message Date
bat 2bd207835a Merge pull request 'create from template' (#77) from templates into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/77
3 years ago
bat b212e91413 bump 3 years ago
bat a333dcd9f1 Create from template 3 years ago
bat 84d4836daa Index templates 3 years ago
bat 45956133ea Merge pull request 'use menu to add files' (#76) from use-menu-to-add-files into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/76
3 years ago
bat 711720f49d use menu to add files 3 years ago
bat 2c3b1d8e14 Merge pull request 'missing file in serviceworker' (#75) from missing-file-in-serviceworker into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/75
3 years ago
bat 5ecd17fc6a missing file in serviceworker 3 years ago
bat 84e31943a2 Merge pull request 'storage class' (#74) from storage-class into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/74
3 years ago
bat d5138ae491 add storage.js 3 years ago
bat b66e298ac8 set up instance of storage class 3 years ago
bat 8696ad8228 use storage instance 3 years ago
bat 3cc25e6d37 Merge pull request 'use correct reference to registration' (#73) from fix-registration-reference into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/73
3 years ago
bat 806a44c107 use correct reference to registration 3 years ago
bat 3081900ec8 Merge pull request 'skip waiting for serviceworker if found to be waiting' (#72) from skip-waiting into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/72
3 years ago
bat da46a345f8 skip waiting for serviceworker if found to be waiting 3 years ago
bat ef8a5fbf35 Merge pull request 'add codemirror editor' (#71) from codemirror-editor into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/71
3 years ago
bat f7495eb9e4 add bundle 3 years ago
bat e7b3f4ee86 add codemirror editor 3 years ago
bat e58f74b016 add codemirror editor 3 years ago
bat 00beac7bd4 Merge pull request 'use correct 5mb local storage cap' (#70) from localstorage-cap into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/70
3 years ago
bat 3d25ccd34c bump 3 years ago
bat 4e45e94934 use correct 5mb local storage cap 3 years ago
bat 646fa8e0af Merge pull request 'connections' (#69) from connections-settings into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/69
3 years ago
bat 3e2aa1925e support put and get messages 3 years ago
bat acfd969c31 support put and get messages 3 years ago
bat efe43fd7e1 add support for get/put of connected pages 3 years ago
bat a37c2e3203 remove file w/ incorrect name 3 years ago
bat e7e5cdc500 support put and get messages 3 years ago
bat c0113e8c71 fix renaming settings & update inverse settings on rename 3 years ago
bat 03885398c5 apply inverse connections 3 years ago
bat 9c67f0c741 add new settings components 3 years ago
bat e8857a5406 give settings the path 3 years ago
bat bc99762851 Merge pull request 'release color change' (#68) from release-change-color into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/68
3 years ago
bat ab343c44c3 release color change 3 years ago
bat 977c329f3a Merge pull request 'simplify CSP and add title to settings dialog' (#67) from csp-and-dialog-settings into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/67
3 years ago
bat 67fc165526 Bump 3 years ago
bat 346428989f simplify CSP and add title to settings dialog 3 years ago
bat d28d932a20 Merge pull request 'Add location box with navigate' (#66) from location-box into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/66
3 years ago
bat 4ac49ff357 Bump 3 years ago
bat 55910452e3 Add location box with navigate 3 years ago
bat bc759e5197 Merge pull request 'show % storage full' (#65) from storage-full-percent into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/65
3 years ago
bat 622f6a113d show % storage full 3 years ago
bat a15fcc921d Show space 3 years ago
bat 453477b9da Merge pull request 'Bump' (#64) from bump into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/64
3 years ago
bat 7873c51d48 Bump 3 years ago
bat a090a11940 Merge pull request 'Don't show settings and other localStorage items not beginning with /' (#63) from filter-pages-in-nav into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/63
3 years ago
bat 89e6d855c9 Don't show settings and other localStorage items not beginning with / 3 years ago
bat 17587dbdc2 Merge pull request 'switch to viewing on page change' (#62) from view-on-nav into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/62
3 years ago
bat f7cca256a6 switch to viewing on page change 3 years ago
bat 0c42e5c9a8 in wrong folder, remove 3 years ago
bat d7beb37682 switch to viewing on nav 3 years ago
bat 8893008e94 Merge pull request 'update editor: insert before/after' (#61) from update-editor-insert-before-after into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/61
3 years ago
bat 2886878ab6 update editor: insert before/after 3 years ago
bat 960bfb0220 Merge pull request 'fix jsDelivr URL in CSP' (#60) from fix-jsdelivr-url-in-csp into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/60
3 years ago
bat aea9b90dc8 fix jsDelivr URL in CSP 3 years ago
bat cdd1cba1e6 fix jsDelivr URL in CSP 3 years ago
bat 466845e65a Merge pull request 'fix single quote imports in loading' (#59) from single-quote-imports-external into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/59
3 years ago
bat dc7716b4e5 fix single quote imports in loading
Currently it only handles double quote imports, but mangled single quote imports. Keeping double quote imports for now so it can load single quote ones from external and double quote from internal.
3 years ago
bat 8a3f084936 Merge pull request 'fix removing csp' (#58) from csp-setting-fix into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/58
3 years ago
Benjamin Atkin 934e6616c4 fix removing csp 3 years ago
bat 1a9f5e9ce4 Merge pull request 'add csp setting' (#57) from csp-setting into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/57
3 years ago
Benjamin Atkin 9e90b8a3b8 add csp setting 3 years ago
bat 0622d10db4 Merge pull request 'move page-actions to a component; fix downloading' (#56) from page-actions-component into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/56
3 years ago
Benjamin Atkin 810924b677 move page-actions to a component; fix downloading 3 years ago
bat cd467c4443 Merge pull request 'add app.js changes missing from previous PR' (#55) from page-actions into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/55
3 years ago
Benjamin Atkin 287e737c11 add app.js changes missing from previous PR 3 years ago
bat 34fb5b07cc Merge pull request 'split page actions into separate module' (#54) from page-actions into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/54
3 years ago
Benjamin Atkin aae3ba6d57 split page actions into separate module 3 years ago
bat 331b85048b Merge pull request 'make hamburger menu width responsive (fix on desktop)' (#53) from hamburger-menu-width into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/53
3 years ago
bat 07fd06f339 make hamburger menu width responsive 3 years ago
bat cec083ef39 bump 3 years ago
bat 895c79130f Merge pull request 'bump for builder running replace on intro' (#52) from replace-on-intro into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/52
3 years ago
bat ba12ecc7c7 bump for builder running replace on intro 3 years ago
bat 9e034e2e5e Merge pull request 'Bump for builder change to support app building itself' (#51) from builder-app into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/51
3 years ago
bat 23a0f309b4 Bump for builder change to support app building itself 3 years ago
bat e767ed78d7 Merge pull request 'Collapse/expand' (#50) from collapse-expand into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/50
3 years ago
bat a9f0e0bd67 Bump 3 years ago
bat 0abc623a00 Merge pull request 'Add duplicate' (#49) from duplicate into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/49
3 years ago
bat 6e107c9f87 Subir archivos a '' 3 years ago
bat 44ac8dd595 Add duplicate 3 years ago
bat 60a04047b1 Merge pull request 'Fix flicker of undefined' (#47) from fix-flicker-undefined into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/47
3 years ago
bat a5883d746b Bump 3 years ago
bat 9c6213c3e5 Bump 3 years ago
bat 911f90714d Fix flicker of undefined 3 years ago
bat 4782c72726 Actualizar 'sw.js' 3 years ago
bat 92365e5d82 Merge pull request 'integrate editor' (#46) from integrate-editor into main
Reviewed-on: https://codeberg.org/macchiato/pages/pulls/46
3 years ago
Benjamin Atkin c9a021057b integrate editor 3 years ago

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

@ -25,6 +25,15 @@ html {
${'<'}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') {
@ -34,12 +43,18 @@ addEventListener('message', event => {
frame.sandbox = "allow-scripts allow-top-navigation"
}
frame.srcdoc = d[1]
} else if (frame !== undefined) {
frame.postMessage(event.data)
}
if (isNew) {
document.body.appendChild(frame)
}
} else if (frame !== undefined) {
const transferArg = (
event.ports?.length ? [[...event.ports]] : []
)
frame.contentWindow.postMessage(
event.data, '*', ...transferArg
)
}
}
})
${'</'}script>
</body>
@ -49,7 +64,9 @@ 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
this.isGroup = true
}
connectedCallback() {
@ -71,26 +88,33 @@ export class FileGroupPage extends HTMLElement {
width: 100%;
}
:host(.editing) iframe.view {
display: none;
visibility: hidden;
}
:host(.viewing) iframe.edit {
display: none;
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')
if (this.csp !== undefined) {
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', this.csp)
url.searchParams.set('csp', csp)
url.searchParams.set('html', frameHtml)
frame.src = url.href
} else {
@ -100,18 +124,23 @@ export class FileGroupPage extends HTMLElement {
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')
if (this.csp !== undefined) {
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', this.csp)
url.searchParams.set('csp', csp)
url.searchParams.set('html', frameHtml)
frame.src = url.href
} else {
@ -119,31 +148,160 @@ export class FileGroupPage extends HTMLElement {
frame.srcdoc = frameHtml
}
frame.addEventListener('load', () => {
this.displayView()
this.viewLoaded = true
if (this.onceOnLoaded) {
this.onceOnLoaded()
this.onceOnLoaded = undefined
}
})
this.viewFrame = frame
this.shadowRoot.append(frame)
}
displayView() {
let doc = 'view here'
displayView(doc) {
const msg = ['srcdoc', doc]
if (this.viewLoaded) {
this.viewFrame.contentWindow.postMessage(
msg, '*'
)
} else {
this.onceOnLoaded = () => {
this.viewFrame.contentWindow.postMessage(
msg, '*'
)
}
}
}
displayEdit() {
let doc = 'edit here'
const msg = ['srcdoc', doc]
this.viewFrame.contentWindow.postMessage(
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 {
localStorage.setItem(this.path, value)
this.storage.setItem(this.path, value)
} catch (err) {
console.error(err)
}
@ -151,13 +309,56 @@ export class FileGroupPage extends HTMLElement {
get body() {
try {
return localStorage.getItem(this.path)
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) {
@ -168,6 +369,11 @@ export class FileGroupPage extends HTMLElement {
} else {
classes.add('viewing')
classes.remove('editing')
if (this.editFrame) {
this.editFrame.contentWindow.postMessage(
['request-html'], '*'
)
}
}
}
}

@ -19,27 +19,29 @@ export class Header extends HTMLElement {
}
textEn = {
download: 'Download',
rename: 'Move/Rename',
delete: 'Delete',
confirmDelete: f => (
`Are you sure you want to delete ${f}?`
),
cancel: 'Cancel',
alreadyExists: 'There is already a page with that name.',
createPage: 'Create Page',
htmlCss: 'HTML/CSS',
singleFile: 'Single File',
newPage: 'New Page',
go: 'Go',
}
textEs = {
download: 'Descargar',
rename: 'Mover/Renombrar',
delete: 'Borrar',
confirmDelete: f => (
`¿Desea borrar ${f}?`
),
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',
go: 'Ir',
}
constructor() {
@ -50,11 +52,12 @@ export class Header extends HTMLElement {
this.appBar = document.createElement('div')
this.appBar.classList.add('app-bar')
this.shadowRoot.appendChild(this.appBar)
this.addButton(this.icons.menu, 'nav', () => {
this.menu.pages = this.getPages()
this.menuPanel.classList.add('open')
this.overlay.classList.add('open')
}).classList.add('left-end')
const navBtn = this.addButton(
this.icons.menu,
'nav',
() => this.openNav(),
)
navBtn.classList.add('left-end')
this.addButton(this.icons.add, 'add', () => {
this.addPage()
})
@ -68,14 +71,16 @@ export class Header extends HTMLElement {
this.icons.dot,
'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.pageButton.classList.add('right-end')
this.addPageMenu()
this.addMenu()
this.dialogWrap = document.createElement('div')
this.shadowRoot.appendChild(this.dialogWrap)
}
connectedCallback() {
@ -112,9 +117,9 @@ export class Header extends HTMLElement {
div.menu {
position: fixed;
top: 0;
left: -90vw;
left: max(-90vw, -480px);
height: 100vh;
width: 90vw;
width: min(90vw, 480px);
background-color: #fff;
transition: left .25s ease-in-out;
}
@ -142,11 +147,9 @@ export class Header extends HTMLElement {
width: 20px;
height: 20px;
}
m-dialog::part(footer) {
padding-top: 15px;
}
`
this.shadowRoot.append(style)
this.addPageMenu()
}
encodePath(path) {
@ -185,94 +188,28 @@ export class Header extends HTMLElement {
this.menu.addEventListener('close-menu', () => {
this.close()
})
this.menu.addEventListener(
'click-location',
() => {
this.close()
this.navPage()
},
)
}
addPageMenu() {
this.pageMenu = document.createElement(
'm-menu-dropdown'
)
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
this.shadowRoot.appendChild(this.pageMenu)
}
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)
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.delete, () => {
localStorage.removeItem(this.path)
location.hash = '/'
dialog.close()
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
})
this.shadowRoot.appendChild(this.pageMenu)
openNav() {
this.menu.location = this.path
this.menu.pages = this.getPages()
this.menu.storageUse = this.getStorageUse()
this.menuPanel.classList.add('open')
this.overlay.classList.add('open')
}
close() {
@ -285,7 +222,21 @@ export class Header extends HTMLElement {
}
getPages() {
return Object.keys(localStorage).slice().sort()
return (
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) {
@ -303,6 +254,22 @@ export class Header extends HTMLElement {
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() {
const dialog = document.createElement('m-dialog')
this.dialogWrap.replaceChildren(dialog)
@ -310,13 +277,36 @@ 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
}))
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'
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'
)
bGroup.addPrimary(this.text.createPage, () => {
const newPath = this.encodePath(input.value)
const v = localStorage.getItem(newPath)
const v = this.storage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
@ -328,7 +318,30 @@ export class Header extends HTMLElement {
}
return
}
localStorage.setItem(newPath, '')
let value
if (select.value === 'htmlCss') {
value = JSON.stringify({
type: 'm-file-group',
files: [
{
"name": "index.html",
"data": `<h1>${this.text.newPage}</h1>`,
},
{
"name": "style.css",
"data": 'h1 { color: dodgerblue; }',
},
],
})
} else {
value = this.storage.getItem(
select.value
) ?? ''
}
this.storage.setItem(newPath, value)
this.storage.setItem(
'settings/page:' + newPath, '{}'
)
location.hash = newPath
dialog.close()
this.dispatchEvent(new CustomEvent(
@ -342,6 +355,30 @@ export class Header extends HTMLElement {
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() {
return this._language
}

@ -1,9 +1,29 @@
import { EditorBuild } from "/loader/editor-build.js"
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() {
super()
this.attachShadow({mode: 'open'})
this.csp = "default-src 'self' 'unsafe-inline' 'unsafe-eval'"
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() {
@ -19,9 +39,12 @@ export class Layout extends HTMLElement {
overflow-y: hidden;
position: relative;
}
m-page {
m-page, m-file-group-page {
flex-grow: 1;
}
m-dialog::part(footer) {
padding-top: 15px;
}
`
this.shadowRoot.appendChild(style)
this.header.editing = this.editing
@ -31,7 +54,6 @@ export class Layout extends HTMLElement {
this.header.addEventListener('create-page', () => {
this.editing = true
})
this.shadowRoot.appendChild(this.header)
this.load()
addEventListener('hashchange', () => {
this.load()
@ -39,13 +61,17 @@ export class Layout extends HTMLElement {
this.addEventListener('hash-change', () => {
this.load()
})
this.addEventListener('settings-change', () => {
this.load()
})
}
load() {
const path = this.path
this.editing = false
const prevPage = this.page
let isGroup = false
const body = localStorage.getItem(path) || ''
const body = this.storage.getItem(path) || ''
if (body.match(/^\s*{/)) {
try {
const bodyData = JSON.parse(body)
@ -60,7 +86,16 @@ export class Layout extends HTMLElement {
this.page = document.createElement(
isGroup ? 'm-file-group-page' : 'm-page'
)
this.page.storage = this.storage
if (isGroup) {
this.page.cspOff = this.csp === undefined
this.page.cspProfiles = this.cspProfiles
} else {
this.page.csp = this.csp
}
if (isGroup) {
this.page.editorBuild = this.editorBuild
}
this.page.path = path
this.editing = this.editing
if (prevPage !== undefined) {
@ -68,6 +103,8 @@ export class Layout extends HTMLElement {
}
this.shadowRoot.appendChild(this.page)
this.header.path = path
this.pageActions.path = path
this.pageActions.page = this.page
}
get path() {
@ -81,7 +118,7 @@ export class Layout extends HTMLElement {
}
set editing(value) {
sessionStorage.setItem(
this.storage.session.setItem(
'editing', value ? 'true' : 'false'
)
this.header.editing = this.editing
@ -93,10 +130,27 @@ export class Layout extends HTMLElement {
get editing() {
try {
return (
sessionStorage.getItem('editing') === 'true'
this.storage.session.getItem('editing') === 'true'
)
} catch (e) {
return false
}
}
get editorBuild() {
if (this._editorBuild === undefined) {
this._editorBuild = new EditorBuild()
}
return this._editorBuild
}
set storage(value) {
this._storage = value
this.header.storage = value
this.pageActions.storage = value
}
get storage() {
return this._storage
}
}

@ -1,4 +1,17 @@
export class NavMenu extends HTMLElement {
textByLang = {
en: {
pages: 'Pages',
localStorage: 'Local storage',
full: 'full',
},
es: {
pages: 'Páginas',
localStorage: 'Almacenamiento local',
full: 'lleno',
},
}
constructor() {
super()
this.attachShadow({mode: 'open'})
@ -14,8 +27,21 @@ export class NavMenu extends HTMLElement {
display: flex;
flex-direction: column;
color: #000;
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 {
width: 100%;
text-align: center;
@ -32,15 +58,45 @@ export class NavMenu extends HTMLElement {
display: block;
margin: 8px 10px;
}
.footer {
padding: 3px 5px;
}
`
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.innerText = this.pagesText
this.header.innerText = this.text.pages
this.shadowRoot.append(this.header)
this.pageList = document.createElement('div')
this.pageList.classList.add('page-list')
this.shadowRoot.append(this.pageList)
this.renderPageList()
this.footer = document.createElement('div')
this.footer.classList.add('footer')
this.shadowRoot.append(this.footer)
this.renderFooter()
this.shadowRoot.addEventListener('click', e => {
if (e.target.classList.contains('page')) {
if (this.handleLinks) {
@ -63,14 +119,28 @@ export class NavMenu extends HTMLElement {
set language(language) {
this._language = language
this.text = this.textByLang[
this.langEs ? 'es' : 'en'
]
}
get langEs() {
return /^es\b/.test(this.language)
}
get pagesText() {
return this.langEs ? 'Páginas' : 'Pages'
set location(location) {
this._location = location
if (this.locationBox) {
this.renderLocation()
}
}
get location() {
return this._location
}
renderLocation() {
this.locationBox.innerText = this.location
}
set pages(pages) {
@ -94,4 +164,35 @@ export class NavMenu extends HTMLElement {
})
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
)
}
}
}

@ -0,0 +1,377 @@
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)
}
}

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

34
sw.js

@ -1,21 +1,31 @@
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',
'/components/page-actions.js',
'/dialog/dialog.js',
'/editor/app.js',
'/editor/file-group.js',
'/editor/file-view.js',
'/editor/text-edit.js',
'/editor/code-edit.js',
'/editor/header.js',
'/forms/button-group.js',
'/menu/dropdown.js',
'/index.html',
'/loader/builder.js',
'/loader/editor-build.js',
'/editor/file-group.js',
'/editor/file-page.js',
'/editor/text-edit.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
])
}
@ -32,7 +42,7 @@ async function cacheFirst(request) {
const csp = url.searchParams.get('csp')
return new Response(html, {
headers: {
'Content-Type': 'text/html',
'Content-Type': 'text/html; charset=utf-8',
'Content-Security-Policy': csp,
}
})
@ -53,3 +63,9 @@ self.addEventListener('fetch', event => {
self.addEventListener('activate', event => {
event.waitUntil(clients.claim())
})
self.addEventListener('message', e => {
if (Array.isArray(e.data) && e.data[0] === 'skipWaiting') {
self.skipWaiting()
}
})
Loading…
Cancel
Save