From aa3f025c7d4e7f55e08b738f87f98d35d16a31b4 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Thu, 1 Feb 2024 12:56:36 +0100 Subject: [PATCH] Support HTML editing with inline grammar markup --- index.html | 4 ++- online-editor.js | 80 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/index.html b/index.html index 1c6517b..dd28ed5 100644 --- a/index.html +++ b/index.html @@ -11,8 +11,10 @@ +
Popravite kar želite.
Popravite kar želite.
Na mizo nisem položil knjigo. Popravite kar želite.
+
To je preiskus.
--> +

Madžarski premier Orban je tako očitno vendarle pristal na nadaljnjo makrofinančno pomoč Ukrajini v okviru revizije dolgoročnega proračuna unije 2021-2027. Ta vključuje 50 milijard evrov za Ukrajino za prihodnja štiri leta, od tega 33 milijard evrov posojil in 17 milijard evrov nepovratnih sredstev.

diff --git a/online-editor.js b/online-editor.js index 7f4d776..21d1fd1 100644 --- a/online-editor.js +++ b/online-editor.js @@ -6,13 +6,12 @@ window.onload = () => { // Search and prepare all our editors found in the document. document.querySelectorAll('.online-editor').forEach(edit => { let editor = { - ignoreInput: false, timer: null } besEditors[edit.id] = editor besCheckText(edit) - //edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false) + edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false) edit.addEventListener('click', 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) { switch (el.nodeType) { @@ -27,12 +33,21 @@ async function besCheckText(el) return [{ text: el.textContent, el: el, markup: false }] 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 = [] for (const el2 of el.childNodes) { data = data.concat(await besCheckText(el2)) } let defaultView = document.defaultView; - switch (defaultView.getComputedStyle(el, null).getPropertyValue('display')) + switch (defaultView.getComputedStyle(el, null).getPropertyValue('display').toLowerCase()) { case 'inline': case 'inline-block': @@ -62,6 +77,20 @@ async function besCheckText(el) return response.json() }) .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. // 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); @@ -96,6 +125,8 @@ async function besCheckText(el) // but it doesnt work when the range has partially selected a non-Text node. // range.surroundContents(span) }) + + el.setAttribute('besChecked', 'true') }) .catch(error => { // 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) { let editor = besEditors[editorId] - if (editor.ignoreInput) return - 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) - event.getTargetRanges().forEach(range => { - 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 - } - }) + event.getTargetRanges().forEach(range => besGetBlockParent(range.startContainer, edit)?.removeAttribute('besChecked')) } - function besHandleClick(e) { switch (e.target) { case e.target.closest('span'):