|
|
|
|
import { FileGroup } from "/editor/file-group.js"
|
|
|
|
|
import { FileView } from "/editor/file-view.js"
|
|
|
|
|
import { TextEdit } from "/editor/text-edit.js"
|
|
|
|
|
import { CodeEdit } from "/editor/code-edit.js"
|
|
|
|
|
import { ButtonGroup } from "/forms/button-group.js"
|
|
|
|
|
import { Dropdown } from "/menu/dropdown.js"
|
|
|
|
|
import { Builder } from "/loader/builder.js"
|
|
|
|
|
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-editor-file-group', FileGroup
|
|
|
|
|
)
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-editor-file-view', FileView
|
|
|
|
|
)
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-editor-text-edit', TextEdit
|
|
|
|
|
)
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-editor-code-edit', CodeEdit
|
|
|
|
|
)
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-forms-button-group', ButtonGroup
|
|
|
|
|
)
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-menu-dropdown', Dropdown
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class EditorApp extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super()
|
|
|
|
|
this.attachShadow({mode: 'open'})
|
|
|
|
|
this.loaded = false
|
|
|
|
|
this.el = document.createElement(
|
|
|
|
|
'm-editor-file-group'
|
|
|
|
|
)
|
|
|
|
|
addEventListener('message', event => {
|
|
|
|
|
const message = event.data
|
|
|
|
|
if (Array.isArray(message)) {
|
|
|
|
|
if (message[0] === 'doc' && !this.loaded) {
|
|
|
|
|
this.load(message[1], message[2])
|
|
|
|
|
} else if (message[0] === 'request-html') {
|
|
|
|
|
const files = this.el.files.map(
|
|
|
|
|
({name, data}) => ({name, data})
|
|
|
|
|
)
|
|
|
|
|
this.display(files)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
parent.postMessage(['ready'], '*')
|
|
|
|
|
this.shadowRoot.addEventListener('code-input', (e) => {
|
|
|
|
|
this.handleInput()
|
|
|
|
|
})
|
|
|
|
|
this.shadowRoot.addEventListener('input', (e) => {
|
|
|
|
|
this.handleInput()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
const style = document.createElement('style')
|
|
|
|
|
style.textContent = `
|
|
|
|
|
:host {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
margin: 8px;
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
this.shadowRoot.append(style)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async load(doc, settings) {
|
|
|
|
|
const files = JSON.parse(doc).files
|
|
|
|
|
this.display(files)
|
|
|
|
|
this.el.codeMirror = !!(settings?.codeMirror)
|
|
|
|
|
if (this.el.codeMirror) {
|
|
|
|
|
await this.loadCodeMirror()
|
|
|
|
|
}
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
this.el.addFile(file)
|
|
|
|
|
}
|
|
|
|
|
this.loaded = true
|
|
|
|
|
this.shadowRoot.appendChild(this.el)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
save(e) {
|
|
|
|
|
const files = this.el.files.map(
|
|
|
|
|
({name, data, collapsed}) =>
|
|
|
|
|
({name, data, collapsed})
|
|
|
|
|
)
|
|
|
|
|
const data = JSON.stringify({
|
|
|
|
|
type: 'm-file-group',
|
|
|
|
|
files,
|
|
|
|
|
})
|
|
|
|
|
parent.postMessage(['save', data], '*')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display(files) {
|
|
|
|
|
const builder = new Builder(files)
|
|
|
|
|
const html = builder.build(files)
|
|
|
|
|
parent.postMessage(['html', html], '*')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleInput(e) {
|
|
|
|
|
this.lastInputEvent = e
|
|
|
|
|
if (!this.inputTimeout) {
|
|
|
|
|
this.save(this.lastInputEvent)
|
|
|
|
|
this.lastInputEvent = undefined
|
|
|
|
|
this.inputTimeout = setTimeout(() => {
|
|
|
|
|
this.inputTimeout = undefined
|
|
|
|
|
if (this.lastInputEvent) {
|
|
|
|
|
this.handleInput(this.lastInputEvent)
|
|
|
|
|
}
|
|
|
|
|
}, 100)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sha(ab) {
|
|
|
|
|
const hash = await crypto.subtle.digest(
|
|
|
|
|
"SHA-256", ab
|
|
|
|
|
)
|
|
|
|
|
return 'sha256-' + btoa(
|
|
|
|
|
String.fromCharCode(
|
|
|
|
|
...new Uint8Array(hash)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkIntegrity(text, integrity) {
|
|
|
|
|
const ab = new TextEncoder().encode(text)
|
|
|
|
|
const sha = await this.sha(ab)
|
|
|
|
|
if (sha !== integrity) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'failed integrity check: ' +
|
|
|
|
|
`${sha} !== ${integrity}`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return ab
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req(method, path, value = undefined) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const ch = new MessageChannel()
|
|
|
|
|
const port = ch.port1
|
|
|
|
|
port.onmessage = e => {
|
|
|
|
|
resolve(e.data)
|
|
|
|
|
port.close()
|
|
|
|
|
}
|
|
|
|
|
window.parent.postMessage(
|
|
|
|
|
(
|
|
|
|
|
method === 'get' ?
|
|
|
|
|
[method, path] :
|
|
|
|
|
[method, path, value]
|
|
|
|
|
),
|
|
|
|
|
'*',
|
|
|
|
|
[ch.port2]
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadCodeMirror() {
|
|
|
|
|
const resp = await this.req(
|
|
|
|
|
'load', '/editor-lib-codemirror/codemirror-bundle.js'
|
|
|
|
|
)
|
|
|
|
|
const passed = await this.checkIntegrity(
|
|
|
|
|
resp.body,
|
|
|
|
|
'sha256-5RlM/RBsaGHWVkn1pTnI7jUm9KPsI+SLglUwtbWroEA=',
|
|
|
|
|
)
|
|
|
|
|
if (!passed) {
|
|
|
|
|
throw new Error('Failed integrity check')
|
|
|
|
|
}
|
|
|
|
|
const s = document.createElement('script')
|
|
|
|
|
s.type = 'module'
|
|
|
|
|
s.textContent = resp.body
|
|
|
|
|
document.head.append(s)
|
|
|
|
|
await new Promise(res => setTimeout(() => res(), 50))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define(
|
|
|
|
|
'm-editor-app', EditorApp
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Setup {
|
|
|
|
|
async run() {
|
|
|
|
|
document.body.appendChild(
|
|
|
|
|
document.createElement(
|
|
|
|
|
'm-editor-app'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new Setup().run()
|