service2.js: Port CKEditor service

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

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)