service2.js: Port CKEditor service

This commit is contained in:
Simon Rozman 2024-05-22 12:52:23 +02:00
parent e0e9f1a651
commit 89201ceaff
4 changed files with 256 additions and 91 deletions

28
samples/ckeditor2.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BesService CKEditor Example</title>
<link rel="stylesheet" href="../styles.css" />
<link rel="stylesheet" href="styles.css" />
<script src="https://cdn.ckeditor.com/ckeditor5/41.1.0/classic/ckeditor.js"></script>
<script>const besUrl = 'http://localhost:225/api/v2';</script>
<script src="../service2.js"></script>
</head>
<body>
<p class="my-block">This is an example of a CKEditor edit control. Edit the text, resize the control or browser window, scroll around, click...</p>
<div id="editor">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p>
</div>
<bes-popup-el/>
<script>
ClassicEditor.create(document.querySelector('#editor'))
.then(newEditor => BesCKService.register(newEditor.ui.view.editable.element, newEditor))
.catch(error => console.error(error))
</script>
</body>
</html>

View File

@ -8,7 +8,8 @@
.my-control {
overflow-y: auto;
height: 300px;
min-height: 100px;
max-height: 500px;
border-radius: 10px;
background-color: #f5f5f5;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);

View File

@ -806,16 +806,7 @@ class BesCKService extends BesService {
* Updates grammar mistake markup positions.
*/
repositionMistakes() {
this.children.forEach(child => {
this.clearMistakeMarkup(child.element)
if (child.matches) {
child.matches.forEach(match => {
const { clientRects, highlights } = this.addMistakeMarkup(match.range)
match.rects = clientRects
match.highlights = highlights
})
}
})
super.repositionMistakes()
// window.dispatchEvent(new Event('resize'))
}

View File

@ -9,6 +9,7 @@
*/
let besServices = []
// TODO: Replace with Resize observer to call onResize() for hostElement only.
window.addEventListener('resize', () =>
besServices.forEach(service => service.onResize())
)
@ -324,90 +325,18 @@ class BesService {
class BesTreeService extends BesService {
constructor(hostElement) {
super(hostElement)
}
/**
* Unregisters grammar checking service.
*/
unregister() {
super.unregister()
}
}
/*************************************************************************
*
* DOM grammar-checking service
*
*************************************************************************/
class BesDOMService extends BesTreeService {
constructor(hostElement) {
super(hostElement)
this.onBeforeInput = this.onBeforeInput.bind(this)
this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
this.onInput = this.onInput.bind(this)
this.hostElement.addEventListener('input', this.onInput)
this.onClick = this.onClick.bind(this)
this.hostElement.addEventListener('click', this.onClick)
}
/**
* Registers grammar checking service.
*
* @param {Element} hostElement DOM element to register grammar checking service for
* @returns {BesService} Grammar checking service instance
*/
static register(hostElement) {
let service = new BesDOMService(hostElement)
service.proofAll()
return service
}
/**
* Unregisters grammar checking service.
*/
unregister() {
this.hostElement.removeEventListener('click', this.onClick)
this.hostElement.removeEventListener('input', this.onInput)
this.hostElement.removeEventListener('beforeinput', this.onBeforeInput)
if (this.timer) clearTimeout(this.timer)
super.unregister()
}
/**
* Called to report the text is about to change
*
* Marks section of the text that is about to change as not-yet-grammar-checked.
*
* @param {InputEvent} event The event notifying the user of editable content changes
*/
onBeforeInput(event) {
if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort()
// Remove markup of all blocks of text that are about to change.
let blockElements = new Set()
event.getTargetRanges().forEach(range => {
BesDOMService.getNodesInRange(range).forEach(el =>
blockElements.add(this.getBlockParent(el))
)
})
blockElements.forEach(block => this.clearProofing(block))
}
/**
* Called to report the text has changed
*/
onInput() {
// Now that the text is done changing, we can correctly calculate markup position.
this.repositionAllMarkup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.timer = setTimeout(() => {
this.proofAll()
delete this.timer
}, 1000)
}
/**
* Recursively grammar-(re)checks our host DOM tree.
*/
@ -549,7 +478,7 @@ class BesDOMService extends BesTreeService {
*/
getProofing(el) {
return this.results.find(result =>
BesDOMService.isSameParagraph(result.element, el)
BesTreeService.isSameParagraph(result.element, el)
)
}
@ -574,7 +503,7 @@ class BesDOMService extends BesTreeService {
clearProofing(el) {
this.clearMarkup(el)
this.results = this.results.filter(
result => !BesDOMService.isSameParagraph(result.element, el)
result => !BesTreeService.isSameParagraph(result.element, el)
)
}
@ -599,7 +528,7 @@ class BesDOMService extends BesTreeService {
*/
clearMarkup(el) {
this.results
.filter(result => BesDOMService.isSameParagraph(result.element, el))
.filter(result => BesTreeService.isSameParagraph(result.element, el))
.forEach(result =>
result.matches.forEach(match => {
if (match.highlights) {
@ -698,8 +627,8 @@ class BesDOMService extends BesTreeService {
let end = range.endContainer
// Common ancestor is the last element common to both elements' DOM path.
let startAncestors = BesDOMService.getParents(start)
let endAncestors = BesDOMService.getParents(end)
let startAncestors = BesTreeService.getParents(start)
let endAncestors = BesTreeService.getParents(end)
let commonAncestor = null
for (
let i = 0;
@ -721,7 +650,7 @@ class BesDOMService extends BesTreeService {
nodes.reverse()
// Walk children and siblings from start until end node is found.
for (node = start; node; node = BesDOMService.getNextNode(node)) {
for (node = start; node; node = BesTreeService.getNextNode(node)) {
nodes.push(node)
if (node === end) break
}
@ -742,7 +671,7 @@ class BesDOMService extends BesTreeService {
if (!el) return
const result = this.results.find(child =>
BesDOMService.isSameParagraph(child.element, el)
BesTreeService.isSameParagraph(child.element, el)
)
if (result) {
for (let m of result.matches) {
@ -758,6 +687,222 @@ class BesDOMService extends BesTreeService {
}
}
/*************************************************************************
*
* DOM grammar-checking service
*
*************************************************************************/
class BesDOMService extends BesTreeService {
constructor(hostElement) {
super(hostElement)
this.onBeforeInput = this.onBeforeInput.bind(this)
this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
this.onInput = this.onInput.bind(this)
this.hostElement.addEventListener('input', this.onInput)
}
/**
* Registers grammar checking service.
*
* @param {Element} hostElement DOM element to register grammar checking service for
* @returns {BesDOMService} Grammar checking service instance
*/
static register(hostElement) {
let service = new BesDOMService(hostElement)
service.proofAll()
return service
}
/**
* Unregisters grammar checking service.
*/
unregister() {
this.hostElement.removeEventListener('input', this.onInput)
this.hostElement.removeEventListener('beforeinput', this.onBeforeInput)
if (this.timer) clearTimeout(this.timer)
super.unregister()
}
/**
* Called to report the text is about to change
*
* Marks section of the text that is about to change as not-yet-grammar-checked.
*
* @param {InputEvent} event The event notifying the user of editable content changes
*/
onBeforeInput(event) {
if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort()
// Remove markup of all blocks of text that are about to change.
let blockElements = new Set()
event.getTargetRanges().forEach(range => {
BesDOMService.getNodesInRange(range).forEach(el =>
blockElements.add(this.getBlockParent(el))
)
})
blockElements.forEach(block => this.clearProofing(block))
}
/**
* Called to report the text has changed
*/
onInput() {
// Now that the text is done changing, we can correctly calculate markup position.
this.repositionAllMarkup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.timer = setTimeout(() => {
this.proofAll()
delete this.timer
}, 1000)
}
}
/*************************************************************************
*
* CKEditor grammar-checking service
*
*************************************************************************/
class BesCKService extends BesTreeService {
constructor(hostElement, ckEditorInstance) {
super(hostElement)
this.ckEditorInstance = ckEditorInstance
this.disableCKEditorSpellcheck()
this.onChangeData = this.onChangeData.bind(this)
this.ckEditorInstance.model.document.on('change:data', this.onChangeData)
}
/**
* Registers grammar checking service.
*
* @param {Element} hostElement DOM element to register grammar checking service for
* @param {CKEditorInstance} ckEditorInstance Enable CKEditor tweaks
* @returns {BesCKService} Grammar checking service instance
*/
static register(hostElement, ckEditorInstance) {
let service = new BesCKService(hostElement, ckEditorInstance)
service.proofAll()
return service
}
/**
* Unregisters grammar checking service.
*/
unregister() {
// TODO: Undo `this.ckEditorInstance.model.document.on('change:data', this.onChangeData)`.
this.restoreCKEditorSpellcheck()
if (this.timer) clearTimeout(this.timer)
super.unregister()
}
/**
* Called to report the text has changed
*/
onChangeData() {
if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort()
const differ = this.ckEditorInstance.model.document.differ
for (const entry of Array.from(differ.getChanges())) {
let element =
entry.type === 'attribute'
? entry.range.start.parent
: entry._element || entry.position.parent
const domElement = this.getDomElement(element)
this.clearProofing(domElement)
}
// TODO: Research if input event or any other event that is called *after* the change is completed
// is possible with CKEditor, and move the code below this line there.
setTimeout(() => {
// Now that the text is done changing, we can correctly calculate markup position.
this.repositionAllMarkup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.timer = setTimeout(() => {
this.proofAll()
delete this.timer
}, 1000)
}, 0)
}
/**
* This function converts a CKEditor element to a DOM element.
*
* @param {CKEditor} element
* @returns domElement
*/
getDomElement(element) {
const viewElement =
this.ckEditorInstance.editing.mapper.toViewElement(element)
const domElement =
this.ckEditorInstance.editing.view.domConverter.mapViewToDom(viewElement)
return domElement
}
/**
* Disables the CKEditor spellcheck.
*/
disableCKEditorSpellcheck() {
this.ckEditorInstance.editing.view.change(writer => {
const root = this.ckEditorInstance.editing.view.document.getRoot()
// TODO: Get true original CKEditor spellcheck setting (writer.getAttribute('spellcheck', root)?).
this.originalCKSpellcheck = 'true'
writer.setAttribute('spellcheck', 'false', root)
})
}
/**
* Restores the CKEditor spellcheck.
*/
restoreCKEditorSpellcheck() {
this.ckEditorInstance.editing.view.change(writer => {
writer.setAttribute(
'spellcheck',
this.originalCKSpellcheck,
this.ckEditorInstance.editing.view.document.getRoot()
)
})
}
/**
* Replaces grammar checking match with a suggestion provided by grammar checking service.
*
* @param {*} el Block element/paragraph containing grammar checking rule match
* @param {*} match Grammar checking rule match
* @param {String} replacement Text to replace grammar checking match with
*/
replaceText(el, match, replacement) {
if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort()
this.clearProofing(el)
const viewRange =
this.ckEditorInstance.editing.view.domConverter.domRangeToView(
match.range
)
const modelRange =
this.ckEditorInstance.editing.mapper.toModelRange(viewRange)
this.ckEditorInstance.model.change(writer => {
const attributes =
this.ckEditorInstance.model.document.selection.getAttributes()
writer.remove(modelRange)
writer.insertText(replacement, attributes, modelRange.start)
})
this.proofAll()
}
/**
* Repositions status DIV element.
*/
setStatusDivPosition() {
// TODO: Position is not correct (SR6, Edge).
const rect = this.hostElement.getBoundingClientRect()
const scrollTop = window.scrollY || document.documentElement.scrollTop
this.statusDiv.style.left = `${rect.right - 50}px`
this.statusDiv.style.top = `${rect.top + rect.height - 100 + scrollTop}px`
}
}
/*************************************************************************
*
* Plain-text grammar-checking service
@ -779,7 +924,7 @@ class BesPlainTextService extends BesService {
* Registers grammar checking service.
*
* @param {Element} hostElement DOM element to register grammar checking service for
* @returns {BesService} Grammar checking service instance
* @returns {BesPlainTextService} Grammar checking service instance
*/
static register(hostElement) {
let service = new BesPlainTextService(hostElement)