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()