Support HTML editing with inline <span> grammar markup

This commit is contained in:
Simon Rozman 2024-02-01 12:56:36 +01:00
parent 0d8d6e165a
commit aa3f025c7d
2 changed files with 65 additions and 19 deletions

View File

@ -11,8 +11,10 @@
<body> <body>
<!--<div id="ed1" class="online-editor" contenteditable="true">Tukaj vpišite besedilo ki ga želite popraviti.</div> <!--<div id="ed1" class="online-editor" contenteditable="true">Tukaj vpišite besedilo ki ga želite popraviti.</div>
<div id="ed2" class="online-editor" contenteditable="true"></div> <div id="ed2" class="online-editor" contenteditable="true"></div>
<div id="ed3" class="online-editor" contenteditable="true"><div>Popravite kar želite.</div></div>--> <div id="ed3" class="online-editor" contenteditable="true"><div>Popravite kar želite.</div></div>
<div id="ed4" class="online-editor" contenteditable="true"><div>Popravite <a href=".">kar želite</a>.</div><div>Na mizo nisem položil knjigo. Popravite kar želite.</div></div> <div id="ed4" class="online-editor" contenteditable="true"><div>Popravite <a href=".">kar želite</a>.</div><div>Na mizo nisem položil knjigo. Popravite kar želite.</div></div>
<div id="ed5" class="online-editor" contenteditable="true">To je preiskus.</div>-->
<div id="ed6" class="online-editor" contenteditable="true"><div class="contextual"><p beschecked="true">Madžarski premier Orban je tako očitno vendarle <span class="typo-mistake">pristal na nadaljnjo makrofinančno pomoč Ukrajini</span> v okviru revizije dolgoročnega proračuna unije 2021-2027<span class="typo-mistake">.</span> Ta vključuje 50 milijard evrov za Ukrajino za prihodnja štiri leta, od tega 33 milijard evrov posojil in 17 milijard evrov nepovratnih sredstev.</p></div></div>
<my-component></my-component> <my-component></my-component>
</body> </body>
</html> </html>

View File

@ -6,13 +6,12 @@ window.onload = () => {
// Search and prepare all our editors found in the document. // Search and prepare all our editors found in the document.
document.querySelectorAll('.online-editor').forEach(edit => { document.querySelectorAll('.online-editor').forEach(edit => {
let editor = { let editor = {
ignoreInput: false,
timer: null timer: null
} }
besEditors[edit.id] = editor besEditors[edit.id] = editor
besCheckText(edit) besCheckText(edit)
//edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false) edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false)
edit.addEventListener('click', e => { edit.addEventListener('click', e => {
besHandleClick(e) besHandleClick(e)
@ -20,6 +19,13 @@ window.onload = () => {
}) })
} }
function besIntervalsOverlap(start1, end1, start2, end2)
{
return (
start2 <= start1 && start1 < end2 ||
start1 <= start2 && start2 < end1)
}
async function besCheckText(el) async function besCheckText(el)
{ {
switch (el.nodeType) { switch (el.nodeType) {
@ -27,12 +33,21 @@ async function besCheckText(el)
return [{ text: el.textContent, el: el, markup: false }] return [{ text: el.textContent, el: el, markup: false }]
case Node.ELEMENT_NODE: case Node.ELEMENT_NODE:
if (el.getAttribute('besChecked') === 'true') {
return [{ text: '<'+el.tagName+'/>', el: el, markup: true }]
}
for (const el2 of el.childNodes) {
if (el2.tagName === 'SPAN' && el2.classList.contains('typo-mistake')) {
el2.replaceWith(...el2.childNodes)
el2.remove()
}
}
let data = [] let data = []
for (const el2 of el.childNodes) { for (const el2 of el.childNodes) {
data = data.concat(await besCheckText(el2)) data = data.concat(await besCheckText(el2))
} }
let defaultView = document.defaultView; let defaultView = document.defaultView;
switch (defaultView.getComputedStyle(el, null).getPropertyValue('display')) switch (defaultView.getComputedStyle(el, null).getPropertyValue('display').toLowerCase())
{ {
case 'inline': case 'inline':
case 'inline-block': case 'inline-block':
@ -62,6 +77,20 @@ async function besCheckText(el)
return response.json() return response.json()
}) })
.then(responseData => { .then(responseData => {
if (!responseData.matches.length) return
// Remove overlapping grammar mistakes for simplicity.
for (let idx1 = 0; idx1 < responseData.matches.length - 1; ++idx1) {
for (let idx2 = idx1 + 1; idx2 < responseData.matches.length;) {
if (besIntervalsOverlap(
responseData.matches[idx1].offset, responseData.matches[idx1].offset + responseData.matches[idx1].length,
responseData.matches[idx2].offset, responseData.matches[idx2].offset + responseData.matches[idx2].length)) {
responseData.matches.splice(idx2, 1)
}
else idx2++
}
}
// Reverse sort grammar mistakes for easier markup insertion later. // Reverse sort grammar mistakes for easier markup insertion later.
// When we start inserting grammar mistakes at the back, indexes before that remain valid. // When we start inserting grammar mistakes at the back, indexes before that remain valid.
responseData.matches.sort((a, b) => a.offset < b.offset ? +1 : a.offset > b.offset ? -1 : 0); responseData.matches.sort((a, b) => a.offset < b.offset ? +1 : a.offset > b.offset ? -1 : 0);
@ -96,6 +125,8 @@ async function besCheckText(el)
// but it doesnt work when the range has partially selected a non-Text node. // but it doesnt work when the range has partially selected a non-Text node.
// range.surroundContents(span) // range.surroundContents(span)
}) })
el.setAttribute('besChecked', 'true')
}) })
.catch(error => { .catch(error => {
// TODO: Make parsing issues non-fatal. // TODO: Make parsing issues non-fatal.
@ -110,29 +141,42 @@ async function besCheckText(el)
} }
} }
function besGetBlockParent(el, edit)
{
const defaultView = document.defaultView
while (el && el !== edit) {
switch (el.nodeType) {
case Node.TEXT_NODE:
el = el.parentNode
continue
case Node.ELEMENT_NODE:
switch (defaultView.getComputedStyle(el, null).getPropertyValue('display').toLowerCase())
{
case 'inline':
case 'inline-block':
el = el.parentNode
continue
default:
return el
}
}
}
return el
}
function besBeforeInput(editorId, event) function besBeforeInput(editorId, event)
{ {
let editor = besEditors[editorId] let editor = besEditors[editorId]
if (editor.ignoreInput) return
if (editor.timer) clearTimeout(editor.timer) if (editor.timer) clearTimeout(editor.timer)
editor.timer = setTimeout(function(){ besCheckText(editorId) }, 1000) editor.timer = setTimeout(function(){ besCheckText(edit) }, 1000)
// No need to invalidate elements after range.startContainer since they will
// get either deleted or replaced.
let edit = document.getElementById(editorId) let edit = document.getElementById(editorId)
event.getTargetRanges().forEach(range => { event.getTargetRanges().forEach(range => besGetBlockParent(range.startContainer, edit)?.removeAttribute('besChecked'))
if (range.startContainer === edit)
return
for (var el = range.startContainer; el ; el = el.nextSibling) {
for (var el2 = el; el2 && el2 !== edit; el2 = el2.parentElement) {
if (!(el2 instanceof HTMLElement) || el2.tagName !== 'DIV') continue
el2.removeAttribute('data-info')
}
if (el === range.endContainer) break
}
})
} }
function besHandleClick(e) { function besHandleClick(e) {
switch (e.target) { switch (e.target) {
case e.target.closest('span'): case e.target.closest('span'):