godo.js/src-js/godo.js

138 lines
4.2 KiB
JavaScript

import {EditorState, Plugin} from 'prosemirror-state'
import {EditorView} from 'prosemirror-view'
import {history} from 'prosemirror-history'
import {keymap} from 'prosemirror-keymap'
import {baseKeymap} from 'prosemirror-commands'
import {DOMParser, DOMSerializer} from 'prosemirror-model'
import {Menu, blocks, marks} from './godo-menus.mjs'
import {dialog} from './godo-dialog.mjs'
import {loadSchema, buildKeymap} from './utils.mjs'
import base from './components/base.mjs'
import fontMarks from './components/font-marks.mjs'
import heading from './components/heading.mjs'
import link from './components/link.mjs'
import list from './components/list.mjs'
function menuPlugin (menu) {
return new Plugin({
view (editorView) {
let menuView = new Menu(menu, editorView)
editorView.dom.parentNode.insertBefore(menuView.el, editorView.dom)
return menuView
},
})
}
function changeEventPlugin (editor) {
return new Plugin({
state: {
init () {},
apply (tr, callback) {
if (tr.docChanged) {
clearTimeout(callback)
callback = setTimeout(() => {
editor.linkedSource.value = editor.getHTML(tr.doc.content)
editor.linkedSource.dispatchEvent(new Event('change', { 'bubbles': true }))
}, 300)
}
return callback
},
},
})
}
export default class Godo extends window.HTMLElement {
connectedCallback () {
const linkedSourceId = this.getAttribute('linked-source')
this.linkedSource = document.querySelector(`#${linkedSourceId}`)
this.options = {
schema: this.getAttribute('schema') || 'full',
instantUpdate: this.getAttribute('instant-update') !== null,
updateEvent: this.getAttribute('update-event'),
headingLevels: this.getAttribute('heading-levels'),
}
const componentsDefinitions = [base, fontMarks, link]
if (this.options.schema === 'full') {
componentsDefinitions.push(heading, list)
}
const components = componentsDefinitions.map(c => c(this.options))
this.schema = loadSchema(components)
this.editorWrapper = document.createElement('div')
this.editorWrapper.className = 'godo'
this.appendChild(this.editorWrapper)
this.marksMenu = menuPlugin(marks(components, this.schema))
this.blocksMenu = menuPlugin(blocks(components, this.schema))
this.menuDialog = document.body.appendChild(dialog)
let pluginsList = [
history(),
keymap(buildKeymap(components, this.schema, null)),
keymap(baseKeymap),
this.marksMenu,
]
if (this.options.schema !== 'basic' && !this.linkedSource.readOnly) pluginsList.push(this.blocksMenu)
if (this.options.instantUpdate) pluginsList.push(changeEventPlugin(this))
const stateConfig = () => {
return {
schema: this.schema,
doc: DOMParser.fromSchema(this.schema).parse(this.getSourceContent()),
plugins: pluginsList,
}
}
this.state = EditorState.create(stateConfig())
const labelSelector = `label[for=${this.linkedSource.id}]`
const label = this.linkedSource.id ? document.querySelector(labelSelector) : undefined
this.view = new EditorView(this.editorWrapper, {
state: this.state,
editable: () => !this.linkedSource.readOnly,
attributes: {
class: 'godo--editor',
role: 'textbox',
'aria-multiline': 'true',
'aria-labelledby': label ? label.id : undefined,
},
})
if (this.linkedSource.form) {
this.linkedSource.form.addEventListener('submit', () => {
this.linkedSource.value = this.getHTML(this.view.state.doc.content)
})
}
const updateEvent = this.options.updateEvent
if (updateEvent) {
this.linkedSource.addEventListener(updateEvent, () => {
this.state = EditorState.create(stateConfig())
this.view.updateState(this.state)
})
}
}
getSourceContent () {
const w = document.createElement('div')
w.innerHTML = this.linkedSource.value
return w
}
getHTML (from) {
const div = document.createElement('div')
const fragment = DOMSerializer
.fromSchema(this.schema)
.serializeFragment(from)
div.appendChild(fragment)
return div.innerHTML
}
}
window.customElements.define('godo-editor', Godo)