Compare commits

..

11 Commits

@ -1,6 +1,7 @@
import { FileGroup } from "/editor/file-group.js" import { FileGroup } from "/editor/file-group.js"
import { FileView } from "/editor/file-view.js" import { FileView } from "/editor/file-view.js"
import { TextEdit } from "/editor/text-edit.js" import { TextEdit } from "/editor/text-edit.js"
import { CodeEdit } from "/editor/code-edit.js"
import { ButtonGroup } from "/forms/button-group.js" import { ButtonGroup } from "/forms/button-group.js"
import { Dropdown } from "/menu/dropdown.js" import { Dropdown } from "/menu/dropdown.js"
import { Builder } from "/loader/builder.js" import { Builder } from "/loader/builder.js"
@ -14,6 +15,9 @@ customElements.define(
customElements.define( customElements.define(
'm-editor-text-edit', TextEdit 'm-editor-text-edit', TextEdit
) )
customElements.define(
'm-editor-code-edit', CodeEdit
)
customElements.define( customElements.define(
'm-forms-button-group', ButtonGroup 'm-forms-button-group', ButtonGroup
) )
@ -33,9 +37,7 @@ class EditorApp extends HTMLElement {
const message = event.data const message = event.data
if (Array.isArray(message)) { if (Array.isArray(message)) {
if (message[0] === 'doc' && !this.loaded) { if (message[0] === 'doc' && !this.loaded) {
this.load(message[1]) this.load(message[1], message[2])
this.loaded = true
this.shadowRoot.appendChild(this.el)
} else if (message[0] === 'request-html') { } else if (message[0] === 'request-html') {
const files = this.el.files.map( const files = this.el.files.map(
({name, data}) => ({name, data}) ({name, data}) => ({name, data})
@ -45,6 +47,9 @@ class EditorApp extends HTMLElement {
} }
}) })
parent.postMessage(['ready'], '*') parent.postMessage(['ready'], '*')
this.shadowRoot.addEventListener('code-input', (e) => {
this.handleInput()
})
this.shadowRoot.addEventListener('input', (e) => { this.shadowRoot.addEventListener('input', (e) => {
this.handleInput() this.handleInput()
}) })
@ -63,12 +68,18 @@ class EditorApp extends HTMLElement {
this.shadowRoot.append(style) this.shadowRoot.append(style)
} }
load(doc) { async load(doc, settings) {
const files = JSON.parse(doc).files 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) { for (const file of files) {
this.el.addFile(file) this.el.addFile(file)
} }
this.display(files) this.loaded = true
this.shadowRoot.appendChild(this.el)
} }
save(e) { save(e) {
@ -102,6 +113,67 @@ class EditorApp extends HTMLElement {
}, 100) }, 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( customElements.define(

@ -0,0 +1,133 @@
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) {
this._fileType = value
if (this.view) {
const langPlugins = this.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
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;
}
`
}

@ -1,14 +1,11 @@
export class FileGroup extends HTMLElement { export class FileGroup extends HTMLElement {
textEn = { textEn = {}
addFile: 'Add File',
}
textEs = { textEs = {}
addFile: 'Añadir archivo',
}
constructor() { constructor() {
super() super()
this.fileCount = {value: 0}
this.language = navigator.language this.language = navigator.language
this.attachShadow({mode: 'open'}) this.attachShadow({mode: 'open'})
this.headerEl = document.createElement('div') this.headerEl = document.createElement('div')
@ -20,16 +17,35 @@ export class FileGroup extends HTMLElement {
const bGroup = document.createElement( const bGroup = document.createElement(
'm-forms-button-group' 'm-forms-button-group'
) )
bGroup.addPrimary(this.text.addFile, () => {
this.addFile()
const btn = bGroup.primary
if (btn.scrollIntoViewIfNeeded) {
btn.scrollIntoViewIfNeeded()
} else {
btn.scrollIntoView()
}
})
this.shadowRoot.appendChild(bGroup) this.shadowRoot.appendChild(bGroup)
this.contentEl.addEventListener(
'click-add-above',
e => {
const el = document.createElement(
'm-editor-file-view'
)
el.fileCount = this.fileCount
el.codeMirror = this.codeMirror
e.target.insertAdjacentElement(
'beforebegin', el
)
this.fileCount.value += 1
},
)
this.contentEl.addEventListener(
'click-add-below',
e => {
const el = document.createElement(
'm-editor-file-view'
)
el.fileCount = this.fileCount
el.codeMirror = this.codeMirror
e.target.insertAdjacentElement(
'afterend', el
)
this.fileCount.value += 1
},
)
} }
connectedCallback() { connectedCallback() {
@ -59,6 +75,8 @@ export class FileGroup extends HTMLElement {
addFile({name, data, collapsed} = {}) { addFile({name, data, collapsed} = {}) {
const el = document.createElement('m-editor-file-view') const el = document.createElement('m-editor-file-view')
el.fileCount = this.fileCount
el.codeMirror = this.codeMirror
if (name !== undefined) { if (name !== undefined) {
el.name = name el.name = name
} }
@ -69,6 +87,7 @@ export class FileGroup extends HTMLElement {
el.collapsed = collapsed el.collapsed = collapsed
} }
this.contentEl.appendChild(el) this.contentEl.appendChild(el)
this.fileCount.value += 1
return el return el
} }

@ -18,10 +18,14 @@ export class FileView extends HTMLElement {
} }
textEn = { textEn = {
addAbove: 'Add above',
addBelow: 'Add below',
delete: 'Delete', delete: 'Delete',
} }
textEs = { textEs = {
addAbove: 'Añadir arriba',
addBelow: 'Añadir abajo',
delete: 'Borrar', delete: 'Borrar',
} }
@ -38,9 +42,10 @@ export class FileView extends HTMLElement {
this.nameEl = document.createElement('input') this.nameEl = document.createElement('input')
this.nameEl.classList.add('name') this.nameEl.classList.add('name')
this.nameEl.setAttribute('spellcheck', 'false') this.nameEl.setAttribute('spellcheck', 'false')
this.nameEl.addEventListener('input', e => {
this.setFileType(e.target.value)
})
this.headerEl.appendChild(this.nameEl) this.headerEl.appendChild(this.nameEl)
this.editEl = document.createElement('m-editor-text-edit')
this.contentEl.appendChild(this.editEl)
this.collapseBtn = document.createElement( this.collapseBtn = document.createElement(
'button' 'button'
) )
@ -52,15 +57,12 @@ export class FileView extends HTMLElement {
this.menuBtn = document.createElement('button') this.menuBtn = document.createElement('button')
this.menuBtn.innerHTML = this.icons.menu this.menuBtn.innerHTML = this.icons.menu
this.menuBtn.addEventListener('click', () => { this.menuBtn.addEventListener('click', () => {
this.menu.open(this.menuBtn) this.openMenu()
}) })
this.headerEl.appendChild(this.menuBtn) this.headerEl.appendChild(this.menuBtn)
this.menu = document.createElement( this.menu = document.createElement(
'm-menu-dropdown' 'm-menu-dropdown'
) )
this.menu.add(this.text.delete, () => {
this.remove()
})
this.shadowRoot.appendChild(this.menu) this.shadowRoot.appendChild(this.menu)
} }
@ -76,8 +78,8 @@ export class FileView extends HTMLElement {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
background-color: #111; background-color: #f2dbd8;
color: #ddd; color: #000;
padding: 3px 0; padding: 3px 0;
} }
div.header > * { div.header > * {
@ -112,8 +114,44 @@ export class FileView extends HTMLElement {
this.shadowRoot.appendChild(style) this.shadowRoot.appendChild(style)
} }
openMenu() {
this.menu.clear()
this.menu.add(this.text.addAbove, () => {
this.dispatchEvent(new CustomEvent(
'click-add-above', {bubbles: true}
))
})
this.menu.add(this.text.addBelow, () => {
this.dispatchEvent(new CustomEvent(
'click-add-below', {bubbles: true}
))
})
if (this.fileCount.value > 1) {
this.menu.add(this.text.delete, () => {
this.remove()
this.fileCount.value -= 1
})
}
this.menu.open(this.menuBtn)
}
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) { set name(name) {
this.nameEl.value = name this.nameEl.value = name
this.setFileType(name)
} }
get name() { get name() {
@ -147,6 +185,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() { get language() {
return this._language return this._language
} }

Binary file not shown.

@ -51,7 +51,7 @@ export class TextEdit extends HTMLElement {
set value(value) { set value(value) {
this.textEl.value = value this.textEl.value = value
this.stackEl.dataset.copy = this.textEl.value this.stackEl.dataset.copy = this.textEl.value
} }
get value() { get value() {
return this.textEl.value return this.textEl.value

Loading…
Cancel
Save