service2.js: Port CKEditor service
This commit is contained in:
305
service2.js
305
service2.js
@@ -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)
|
||||
|
Reference in New Issue
Block a user