js: make div code editable (#59608)
gitea/godo.js/pipeline/head There was a failure building this commit Details

This commit is contained in:
Thomas Jund 2024-03-05 15:05:56 +01:00
parent 5179282b9f
commit d36fc01459
6 changed files with 246 additions and 40 deletions

66
dist/css/godo.css vendored
View File

@ -1,35 +1,37 @@
.godo {
--menu-item-font-size: 14px;
--menu-item-height: calc( var(--menu-item-font-size) * 2);
}
.godo--editor {
border: 1px solid gray;
background-color: white;
padding: 1em;
min-height: 10em;
white-space: pre-wrap;
padding-bottom: 3em;
}
.godo--editor > :first-child {
margin-top: 0;
}
/*
* Menus
*/
.menuicons {
font-size: 14px;
font-size: var(--menu-item-font-size);
position: relative;
min-width: 2em;
display: inline-flex;
}
.menuicon, .menuicon:hover {
padding: .16em .33em;
--border-width: 0px;
padding: calc( var(--menu-item-height) / 4 - var(--border-width) ) .33em;
color: inherit;
background-color: #fff;
min-width: 2em;
border: 0 none;
min-width: var(--menu-item-height);
border: var(--border-width) none;
color: inherit;
font-family: sans-serif;
line-height: 1;
}
.menuicon {
font-weight: 500;
@ -48,13 +50,11 @@
top: 0;
background-color: white;
}
.godo-blocks-menu > .menuicons {
column-gap: .33em;
}
.godo-blocks-menu .menuicon {
--border-width: 1px;
color: #555;
background-color: white;
border: 1px solid #eee;
border: var(--border-width) solid #eee;
}
.godo-blocks-menu .menuicon:hover {
color: black;
@ -214,3 +214,43 @@ div.godo .ProseMirror.godo--editor ul li p {
div.godo .ProseMirror.godo--editor a {
text-decoration: underline;
}
/*
* Editor is form widget
*/
.is-form-widget .godo--editor {
border: 1px solid gray;
background-color: white;
padding: 1em;
padding-bottom: 3em;
}
/*
* Editor for HTML Element
*/
.html-edition {
position: relative;
}
.html-edition .godo--editor {
outline: none;
}
.html-edition--show {
--background-color: hsla(0, 0%, 100%, 0.5);
--padding: 1em;
outline: 1px solid;
background-color: var(--background-color);
margin: calc( -1 * var(--padding) );
margin-top: calc( -1 * var(--menu-item-height) - var(--padding) );
margin-bottom: 0;
padding: var(--padding);
padding-top: 0;
}
.html-edition--show .godo--editor {
padding-top: var(--padding);
}
.html-edition--show .godo-blocks-menu {
// transform: translateY(-1em);
background-color: transparent;
}

98
dist/index.html vendored
View File

@ -16,6 +16,7 @@
<style>
body {
font-family: sans-serif;
background-color: #fafafa;
}
.wrapper {
padding: 2em;
@ -56,7 +57,7 @@
<h2>Basic Godo</h2>
<p>Just p, strong, em & a</p>
<form>
<form class="form-test">
<div class="grid-2cols">
<div>
<h3>Textarea</h3>
@ -84,16 +85,16 @@
<godo-editor
schema="basic"
linked-source="basic-godo-source"
instant-update
update-event="wcs:change"
></godo-editor>
</div>
</div>
<button>Submit</button>
</form>
<h2>Full Godo</h2>
<p>p, h1 -&gt; h2, strong, em, a, ul</p>
<form action="">
<form class="form-test">
<div class="grid-2cols">
<div>
<h3>Textarea</h3>
@ -120,13 +121,48 @@
<h3>Editor</h3>
<godo-editor
linked-source="full-godo-source"
heading-level="1,2"
heading-levels="1,2"
instant-update
update-event="wcs:change"
></godo-editor>
</div>
</div>
</form>
<h2>Full Godo div as linked source</h2>
<h3>DIV</h3>
<div class="editableDiv-for-godo" id="div-godo-source">
<p>This text is div childs</p>
<h1>Quamquam haec quidem <strong>praeposita</strong></h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Haeret in salebra. <a href='http://loripsum.net/' target='_blank'>Sed tamen intellego quid velit</a>.</p>
<p>Qualem igitur <strong>hominem natura</strong> inchoavit ? Duo Reges: constructio interrete. Summum a vobis bonum voluptas dicitur. Huius, Lyco, oratione locuples, rebus ipsis ielunior. Cui Tubuli nomen odio non est? Ea possunt paria non esse. Aliter autem vobis placet. </p>
<h2>Recte et <em>reiecta dicere</em> licebit</h2>
<ul>
<li>Haec mihi <em>videtur delicatior</em>, ut ita dicam, molliorque ratio, quam virtutis vis gravitasque postulat.</li>
<li>Mihi quidem Antiochum, quem audis, satis belle videris attendere.</li>
<li>Nam illud quidem adduci vix possum, ut ea, quae senserit ille, tibi non vera videantur.</li>
<li>Sed id ne cogitari quidem potest quale sit, ut non repugnet ipsum sibi.</li>
<li>Cum autem in quo sapienter dicimus, id a primo rectissime dicitur.</li>
</ul>
</div>
<godo-editor
tabindex="0"
linked-source="div-godo-source"
heading-levels="1,2"
id="directHTMLEditor"
></godo-editor>
<div>
<!-- <button id="editDiv">Edit</button> -->
<button id="cancelEditDiv">Cancel</button>
<button id="SaveEdition">Save</button>
</div>
</main>
</div><!-- #wrapper -->
@ -142,6 +178,60 @@
textarea.dispatchEvent(new CustomEvent(btn.dataset.event))
})
})
const formsTest = document.querySelectorAll('.form-test')
formsTest.forEach((form) => {
form.addEventListener('submit', function(e) {
e.preventDefault()
})
})
// add buttons events for div editing
const editor = document.getElementById('directHTMLEditor')
const cancelBtn = document.getElementById('cancelEditDiv')
const saveBtn = document.getElementById('SaveEdition')
cancelBtn.hidden = true
saveBtn.hidden = true
editor.addEventListener('focusin', () => {
if (editor.view.hasFocus) {
editor.showEdition()
cancelBtn.hidden = false
saveBtn.hidden = false
}
})
cancelBtn.addEventListener('click', () => {
editor.cancelEdition()
cancelBtn.hidden = true
saveBtn.hidden = true
})
saveBtn.addEventListener('click', async () => {
const url = 'https://jsonplaceholder.typicode.com/posts'
const text = editor.getHTML()
const request = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'text': text,
'token': 'pasaran'
})
})
if (!request.ok) {
throw new Error(`HTTP error! status: ${request.status}`)
}
editor.validEdition()
cancelBtn.hidden = true
saveBtn.hidden = true
})
</script>
</body>

View File

@ -81,6 +81,14 @@ const languageContent = {
text: 'italique',
},
},
'edit': {
en: {
text: 'edit',
},
fr: {
text: 'éditer',
},
},
'ok': {
en: {
text: 'validate',

View File

@ -32,7 +32,7 @@ function changeEventPlugin (editor) {
if (tr.docChanged) {
clearTimeout(callback)
callback = setTimeout(() => {
editor.linkedSource.value = editor.getHTML(tr.doc.content)
editor.sourceContent = editor.getHTML(tr.doc.content)
editor.linkedSource.dispatchEvent(new Event('change', { 'bubbles': true }))
}, 300)
}
@ -45,13 +45,23 @@ function changeEventPlugin (editor) {
export default class Godo extends window.HTMLElement {
connectedCallback () {
const linkedSourceId = this.getAttribute('linked-source')
this.linkedSource = document.querySelector(`#${linkedSourceId}`)
this.linkedSource = document.getElementById(linkedSourceId)
if (this.linkedSource.form) {
this.on()
this.editorWrapper.classList.add('is-form-widget')
} else {
this.initHTMLEditor()
}
}
on () {
this.options = {
schema: this.getAttribute('schema') || 'full',
instantUpdate: this.getAttribute('instant-update') !== null,
updateEvent: this.getAttribute('update-event'),
headingLevels: this.getAttribute('heading-levels'),
editable: !this.linkedSource.readOnly,
}
const componentsDefinitions = [base, fontMarks, link]
@ -70,31 +80,31 @@ export default class Godo extends window.HTMLElement {
this.blocksMenu = menuPlugin(blocks(components, this.schema))
this.menuDialog = document.body.appendChild(dialog)
let pluginsList = [
this.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))
if (this.options.schema !== 'basic') this.pluginsList.push(this.blocksMenu)
if (this.options.instantUpdate) this.pluginsList.push(changeEventPlugin(this))
const stateConfig = () => {
this.stateConfig = () => {
return {
schema: this.schema,
doc: DOMParser.fromSchema(this.schema).parse(this.getSourceContent()),
plugins: pluginsList,
doc: DOMParser.fromSchema(this.schema).parse(this.sourceContent),
plugins: this.options.editable ? this.pluginsList : [],
}
}
this.state = EditorState.create(stateConfig())
this.state = EditorState.create(this.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,
editable: () => this.options.editable,
attributes: {
class: 'godo--editor',
role: 'textbox',
@ -105,26 +115,60 @@ export default class Godo extends window.HTMLElement {
if (this.linkedSource.form) {
this.linkedSource.form.addEventListener('submit', () => {
this.linkedSource.value = this.getHTML(this.view.state.doc.content)
this.sourceContent = this.getHTML()
})
}
const updateEvent = this.options.updateEvent
if (updateEvent) {
this.linkedSource.addEventListener(updateEvent, () => {
this.state = EditorState.create(stateConfig())
this.view.updateState(this.state)
this.resetFromSource()
})
}
}
getSourceContent () {
initHTMLEditor () {
this.on()
this.activePlugins = false
this.linkedSource.hidden = true
this.editorWrapper.classList.add('html-edition')
this.showEdition = () => {
if (!this.activePlugins) {
this.activePlugins = true
this.editorWrapper.classList.add('html-edition--show')
}
}
this.cancelEdition = () => {
this.resetFromSource()
this.activePlugins = false
this.editorWrapper.classList.remove('html-edition--show')
}
this.validEdition = () => {
this.sourceContent = this.getHTML()
this.activePlugins = false
this.editorWrapper.classList.remove('html-edition--show')
}
}
get sourceContent () {
const w = document.createElement('div')
w.innerHTML = this.linkedSource.value
w.innerHTML = this.linkedSource.form ? this.linkedSource.value : this.linkedSource.innerHTML
return w
}
set sourceContent (newContent) {
if (this.linkedSource.form) {
this.linkedSource.value = newContent
} else {
this.linkedSource.innerHTML = newContent
}
}
getHTML (from) {
from = from || this.view.state.doc.content
const div = document.createElement('div')
const fragment = DOMSerializer
.fromSchema(this.schema)
@ -132,6 +176,24 @@ export default class Godo extends window.HTMLElement {
div.appendChild(fragment)
return div.innerHTML
}
get activePlugins () {
return this._activePlugins
}
set activePlugins (bool) {
const newState = this.view.state.reconfigure({
plugins: (bool) ? this.pluginsList : [],
})
this.view.updateState(newState)
this.view.updatePluginViews()
this._activePlugins = bool
}
resetFromSource () {
this.state = EditorState.create(this.stateConfig())
this.view.updateState(this.state)
}
}
window.customElements.define('godo-editor', Godo)

View File

@ -19,9 +19,11 @@ function headingComponents () {
domTest('Heading levels option', ({appendToDom}) => {
const dom = appendToDom(`
<div>
<textarea hidden id="marshmallow">
<h1>Initial Content</h1>
</textarea>
<form>
<textarea hidden id="marshmallow">
<h1>Initial Content</h1>
</textarea>
</form>
<godo-editor linked-source="marshmallow" heading-levels="4,5,6" >
</godo-editor>
</div>

View File

@ -7,9 +7,11 @@ domTest('content is moved from linked source', ({appendToDom}) => {
// the input value
const dom = appendToDom(`
<div>
<textarea hidden id="marshmallow">
<invalid-tag>Initial Content</invalid-tag>
</textarea>
<form>
<textarea hidden id="marshmallow">
<invalid-tag>Initial Content</invalid-tag>
</textarea>
</form>
<godo-editor linked-source="marshmallow">
</godo-editor>
</div>
@ -23,8 +25,10 @@ domTest('godo content is updated from source on update-event', ({appendToDom}) =
// the text area value
const dom = appendToDom(`
<div>
<textarea hidden id="marshmallow">
</textarea>
<form>
<textarea hidden id="marshmallow">
</textarea>
</form>
<godo-editor update-event="update-me" linked-source="marshmallow" >
</godo-editor>
</div>