add codemirror editor

pages
bat 3 years ago
parent 3c5f87783f
commit bf8191aa58

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

@ -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;
}
`
}

@ -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
}

@ -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
}

Binary file not shown.
Loading…
Cancel
Save