diff --git a/app.js b/app.js index b012e03..3eb43e4 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ 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" @@ -14,6 +15,9 @@ customElements.define( customElements.define( 'm-editor-text-edit', TextEdit ) +customElements.define( + 'm-editor-code-edit', CodeEdit +) customElements.define( 'm-forms-button-group', ButtonGroup ) @@ -33,9 +37,7 @@ class EditorApp extends HTMLElement { const message = event.data if (Array.isArray(message)) { if (message[0] === 'doc' && !this.loaded) { - this.load(message[1]) - this.loaded = true - this.shadowRoot.appendChild(this.el) + this.load(message[1], message[2]) } else if (message[0] === 'request-html') { const files = this.el.files.map( ({name, data}) => ({name, data}) @@ -45,6 +47,9 @@ class EditorApp extends HTMLElement { } }) parent.postMessage(['ready'], '*') + this.shadowRoot.addEventListener('code-input', (e) => { + this.handleInput() + }) this.shadowRoot.addEventListener('input', (e) => { this.handleInput() }) @@ -63,12 +68,18 @@ class EditorApp extends HTMLElement { this.shadowRoot.append(style) } - load(doc) { + 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.display(files) + this.loaded = true + this.shadowRoot.appendChild(this.el) } save(e) { @@ -102,6 +113,67 @@ class EditorApp extends HTMLElement { }, 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( diff --git a/code-edit.js b/code-edit.js new file mode 100644 index 0000000..f1afc81 --- /dev/null +++ b/code-edit.js @@ -0,0 +1,136 @@ +export class CodeEdit extends HTMLElement { + constructor() { + super() + this.attachShadow({mode: 'open'}) + this.shadowRoot.adoptedStyleSheets = [ + this.constructor.styleSheet + ] + } + + connectedCallback() { + this.initEditor() + } + + static get styleSheet() { + if (this._styleSheet === undefined) { + this._styleSheet = new CSSStyleSheet() + this._styleSheet.replaceSync(this.css) + } + return this._styleSheet + } + + set value(value) { + if (this.view) { + this.view.dispatch({changes: { + from: 0, + to: this.view.state.doc.length, + insert: value + }}) + } else { + this._value = value + } + } + + get value() { + if (this.view) { + return this.view.state.doc.toString() + } else { + return this._value ?? '' + } + } + + set fileType(value) { + console.log('filetype', value) + this._fileType = value + if (this.view) { + const langPlugins = this.langPlugins + console.log({langPlugins}) + this.view.dispatch({ + effects: + this.languageCompartment.reconfigure(langPlugins) + }) + } + } + + get fileType() { + return this._fileType + } + + get langPlugins() { + const cm = window.CodeMirrorBasic + const langPlugins = [] + if (['js', 'javascript'].includes(this.fileType)) { + langPlugins.push(cm.javascriptLanguage) + } else if (this.fileType === 'css') { + langPlugins.push(cm.cssLanguage) + } else if (this.fileType === 'html') { + langPlugins.push(cm.htmlLanguage) + } else if (this.fileType === 'json') { + langPlugins.push(cm.jsonLanguage) + } + return langPlugins + } + + initEditor() { + const cm = window.CodeMirrorBasic + this.languageCompartment = new cm.Compartment() + const langPlugins = this.langPlugins + console.log({langPlugins}) + const basicSetup = [ + cm.lineNumbers(), + cm.highlightActiveLineGutter(), + cm.highlightSpecialChars(), + cm.history(), + cm.foldGutter(), + cm.drawSelection(), + cm.dropCursor(), + cm.EditorState.allowMultipleSelections.of(true), + cm.indentOnInput(), + cm.syntaxHighlighting( + cm.defaultHighlightStyle, {fallback: true} + ), + cm.bracketMatching(), + cm.closeBrackets(), + cm.autocompletion(), + cm.rectangularSelection(), + cm.crosshairCursor(), + cm.highlightActiveLine(), + cm.highlightSelectionMatches(), + cm.keymap.of([ + ...cm.closeBracketsKeymap, + ...cm.defaultKeymap, + ...cm.searchKeymap, + ...cm.historyKeymap, + ...cm.foldKeymap, + ...cm.completionKeymap, + ...cm.lintKeymap + ]), + ] + this.view = new cm.EditorView({ + doc: this._value ?? '', + extensions: [ + ...basicSetup, + this.languageCompartment.of(langPlugins), + cm.EditorView.updateListener.of(e => { + this.dispatchEvent(new CustomEvent( + 'code-input', {bubbles: true, composed: true} + )) + }), + ], + root: this.shadowRoot, + }) + this.shadowRoot.append(this.view.dom) + } + + static css = ` + :host { + display: flex; + flex-direction: column; + align-items: stretch; + background-color: #fff; + } + :host > * { + flex-grow: 1; + } + ` +} \ No newline at end of file diff --git a/file-group.js b/file-group.js index 380102d..8f67bf2 100644 --- a/file-group.js +++ b/file-group.js @@ -36,6 +36,7 @@ export class FileGroup extends HTMLElement { const el = document.createElement( 'm-editor-file-view' ) + el.codeMirror = this.codeMirror e.target.insertAdjacentElement( 'beforebegin', el ) @@ -47,6 +48,7 @@ export class FileGroup extends HTMLElement { const el = document.createElement( 'm-editor-file-view' ) + el.codeMirror = this.codeMirror e.target.insertAdjacentElement( 'afterend', el ) @@ -81,6 +83,7 @@ export class FileGroup extends HTMLElement { addFile({name, data, collapsed} = {}) { const el = document.createElement('m-editor-file-view') + el.codeMirror = this.codeMirror if (name !== undefined) { el.name = name } diff --git a/file-view.js b/file-view.js index 1ec5bad..63b4569 100644 --- a/file-view.js +++ b/file-view.js @@ -42,9 +42,10 @@ export class FileView extends HTMLElement { this.nameEl = document.createElement('input') this.nameEl.classList.add('name') this.nameEl.setAttribute('spellcheck', 'false') + this.nameEl.addEventListener('input', e => { + this.setFileType(e.target.value) + }) this.headerEl.appendChild(this.nameEl) - this.editEl = document.createElement('m-editor-text-edit') - this.contentEl.appendChild(this.editEl) this.collapseBtn = document.createElement( 'button' ) @@ -126,8 +127,23 @@ export class FileView extends HTMLElement { this.shadowRoot.appendChild(style) } + set codeMirror(value) { + this._codeMirror = value + const tagName = ( + this.codeMirror ? + 'm-editor-code-edit' : 'm-editor-text-edit' + ) + this.editEl = document.createElement(tagName) + this.contentEl.replaceChildren(this.editEl) + } + + get codeMirror() { + return this._codeMirror + } + set name(name) { this.nameEl.value = name + this.setFileType(name) } get name() { @@ -161,6 +177,22 @@ export class FileView extends HTMLElement { ) } + setFileType(value) { + if (this.codeMirror && this.editEl) { + let fileType + if (value.endsWith('.js')) { + fileType = 'js' + } else if (value.endsWith('.html')) { + fileType = 'html' + } else if (value.endsWith('.css')) { + fileType = 'css' + } else if (value.endsWith('.json')) { + fileType = 'json' + } + this.editEl.fileType = fileType + } + } + get language() { return this._language } diff --git a/header.js b/header.js new file mode 100644 index 0000000..06d7405 Binary files /dev/null and b/header.js differ