Split BesService to isolate CKEditor specifics in BesCKService

This commit is contained in:
Simon Rozman 2024-03-13 13:03:14 +01:00
parent 63584185ae
commit 87c7f79ebe
2 changed files with 141 additions and 63 deletions

View File

@ -15,7 +15,7 @@
<script> <script>
ClassicEditor.create(document.querySelector('#editor')) ClassicEditor.create(document.querySelector('#editor'))
.then(newEditor => { .then(newEditor => {
BesService.register(newEditor.ui.view.editable.element, newEditor) BesCKService.register(newEditor.ui.view.editable.element, newEditor)
}) })
.catch(error => { .catch(error => {
console.error(error) console.error(error)

View File

@ -4,21 +4,27 @@ let besServices = [] // Collection of all grammar checking services in the docum
// TODO: Add support for <textarea> // TODO: Add support for <textarea>
///
/// Grammar checking service base class
///
class BesService { class BesService {
constructor(hostElement, ckEditorInstance) { constructor(hostElement) {
this.hostElement = hostElement this.hostElement = hostElement
this.timer = null this.timer = null
this.children = [] this.children = []
const { correctionPanel, scrollPanel } = this.createCorrectionPanel(hostElement) const { correctionPanel, scrollPanel } =
this.createCorrectionPanel(hostElement)
this.correctionPanel = correctionPanel this.correctionPanel = correctionPanel
this.scrollPanel = scrollPanel this.scrollPanel = scrollPanel
this.offsetTop = null this.offsetTop = null
this.ckEditorInstance = ckEditorInstance
this.originalSpellcheck = hostElement.spellcheck this.originalSpellcheck = hostElement.spellcheck
hostElement.spellcheck = false
this.abortController = new AbortController() this.abortController = new AbortController()
this.proof(hostElement) hostElement.spellcheck = false
hostElement.addEventListener('beforeinput', BesService.handleBeforeInput, false) hostElement.addEventListener(
'beforeinput',
BesService.handleBeforeInput,
false
)
hostElement.addEventListener('click', BesService.handleClick) hostElement.addEventListener('click', BesService.handleClick)
hostElement.addEventListener('scroll', BesService.handleScroll) hostElement.addEventListener('scroll', BesService.handleScroll)
besServices.push(this) besServices.push(this)
@ -28,11 +34,12 @@ class BesService {
* Registers grammar checking service * Registers grammar checking service
* *
* @param {Element} hostElement DOM element to register grammar checking service for * @param {Element} hostElement DOM element to register grammar checking service for
* @param {CKEditorInstance} ckEditorInstance Enable CKEditor tweaks
* @returns {BesService} Grammar checking service instance * @returns {BesService} Grammar checking service instance
*/ */
static register(hostElement, ckEditorInstance) { static register(hostElement) {
return new BesService(hostElement, ckEditorInstance) let service = new BesService(hostElement)
service.proof(hostElement)
return service
} }
/** /**
@ -47,6 +54,7 @@ class BesService {
false false
) )
if (this.timer) clearTimeout(this.timer) if (this.timer) clearTimeout(this.timer)
service.abortController.abort()
besServices = besServices.filter(item => item !== this) besServices = besServices.filter(item => item !== this)
this.hostElement.spellcheck = this.originalSpellcheck this.hostElement.spellcheck = this.originalSpellcheck
this.correctionPanel.remove() this.correctionPanel.remove()
@ -255,9 +263,6 @@ class BesService {
element: el, element: el,
matches: matches matches: matches
}) })
// This is a solution for displaying mistakes in CKEditor. It is not the best solution, but it works for now.
if (this.ckEditorInstance) window.dispatchEvent(new Event('resize'))
} }
/** /**
@ -307,7 +312,6 @@ class BesService {
* @returns {Object} Client rectangles and grammar mistake highlight elements * @returns {Object} Client rectangles and grammar mistake highlight elements
*/ */
addMistakeMarkup(range) { addMistakeMarkup(range) {
// In CKEditor case, the highlight element is not shown for some reason. But after resizing the window it is shown.
const clientRects = range.getClientRects() const clientRects = range.getClientRects()
const scrollPanelRect = this.scrollPanel.getBoundingClientRect() const scrollPanelRect = this.scrollPanel.getBoundingClientRect()
let highlights = [] let highlights = []
@ -521,36 +525,8 @@ class BesService {
replaceText(el, match, replacement) { replaceText(el, match, replacement) {
if (this.timer) clearTimeout(this.timer) if (this.timer) clearTimeout(this.timer)
this.abortController.abort() this.abortController.abort()
// const tags = this.getTagsAndText(el)
if (!this.ckEditorInstance) {
match.range.deleteContents() match.range.deleteContents()
match.range.insertNode(document.createTextNode(replacement)) match.range.insertNode(document.createTextNode(replacement))
} else {
const { ckEditorInstance } = this
ckEditorInstance.model.change(writer => {
const viewElement =
ckEditorInstance.editing.view.domConverter.mapDomToView(el)
const modelElement =
ckEditorInstance.editing.mapper.toModelElement(viewElement)
if (modelElement) {
const elementRange = writer.createRangeIn(modelElement)
// TODO: This logic should work once the HTML tags are removed from match.match.offset and match.match.length if is possible.
if (
elementRange.start.offset <= match.match.offset &&
elementRange.end.offset >= match.match.offset + match.match.length
) {
const start = writer.createPositionAt(modelElement, match.match.offset)
const end = writer.createPositionAt(
modelElement,
match.match.offset + match.match.length
)
const range = writer.createRange(start, end)
writer.remove(range)
writer.insertText(replacement, start)
}
}
})
}
this.clearMistakeMarkup(el) this.clearMistakeMarkup(el)
// In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change. // In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change.
// It maintains reasonable performance as it only checks the block element that has been modified, // It maintains reasonable performance as it only checks the block element that has been modified,
@ -588,31 +564,108 @@ class BesService {
} }
} }
window.onload = () => { ///
document /// Grammar checking service for CKEditor
.querySelectorAll('.bes-service') ///
.forEach(hostElement => BesService.register(hostElement)) class BesCKService extends BesService {
constructor(hostElement, ckEditorInstance) {
super(hostElement)
this.ckEditorInstance = ckEditorInstance
} }
window.onresize = () => { /**
besServices.forEach(service => { * Registers grammar checking service
service.setCorrectionPanelSize( *
service.hostElement, * @param {Element} hostElement DOM element to register grammar checking service for
service.correctionPanel, * @param {CKEditorInstance} ckEditorInstance Enable CKEditor tweaks
service.scrollPanel * @returns {BesCKService} Grammar checking service instance
) */
service.children.forEach(child => { static register(hostElement, ckEditorInstance) {
service.clearMistakeMarkup(child.element) let service = new BesCKService(hostElement, ckEditorInstance)
service.proof(hostElement)
return service
}
/**
* Marks given block element as grammar-proofed.
*
* @param {Element} el DOM element that was checked
* @param {Array} matches Grammar mistakes
*/
markProofed(el, matches) {
super.markProofed(el, matches)
// This is a solution for displaying mistakes in CKEditor. It is not the best solution, but it works for now.
if (this.ckEditorInstance) window.dispatchEvent(new Event('resize'))
}
/**
* Removes given block element from this.children array
*
* @param {Element} el DOM element for removal
*/
removeChild(el) {
this.children = this.children.filter(child => child.element !== el)
}
/**
* Updates grammar mistake markup positions.
*/
repositionMistakes() {
this.children.forEach(child => {
this.clearMistakeMarkup(child.element)
child.matches.forEach(match => { child.matches.forEach(match => {
const { clientRects, highlights } = service.addMistakeMarkup(match.range) const { clientRects, highlights } = this.addMistakeMarkup(match.range)
match.rects = clientRects match.rects = clientRects
match.highlights = highlights match.highlights = highlights
}) })
}) })
})
} }
// This is popup element // TODO: In rich HTML texts, match.offset has different value than in plain text.
// This function should be able to handle both cases or find a way that works for both.
replaceText(el, match, replacement) {
if (this.timer) clearTimeout(this.timer)
this.abortController.abort()
const { ckEditorInstance } = this
ckEditorInstance.model.change(writer => {
const viewElement =
ckEditorInstance.editing.view.domConverter.mapDomToView(el)
const modelElement =
ckEditorInstance.editing.mapper.toModelElement(viewElement)
if (modelElement) {
const elementRange = writer.createRangeIn(modelElement)
// TODO: This logic should work once the HTML tags are removed from match.match.offset and match.match.length if is possible.
if (
elementRange.start.offset <= match.match.offset &&
elementRange.end.offset >= match.match.offset + match.match.length
) {
const start = writer.createPositionAt(
modelElement,
match.match.offset
)
const end = writer.createPositionAt(
modelElement,
match.match.offset + match.match.length
)
const range = writer.createRange(start, end)
writer.remove(range)
writer.insertText(replacement, start)
}
}
})
this.clearMistakeMarkup(el)
// In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change.
// It maintains reasonable performance as it only checks the block element that has been modified,
// rather than re-evaluating the entire document or a larger set of elements.
this.abortController = new AbortController()
this.proof(el)
}
}
///
/// Grammar mistake popup dialog
///
class BesPopupEl extends HTMLElement { class BesPopupEl extends HTMLElement {
constructor() { constructor() {
super() super()
@ -731,12 +784,37 @@ class BesPopupEl extends HTMLElement {
replacementBtn.textContent = replacement replacementBtn.textContent = replacement
replacementBtn.classList.add('bes-replacement') replacementBtn.classList.add('bes-replacement')
replacementBtn.addEventListener('click', () => { replacementBtn.addEventListener('click', () => {
if (allowReplacements) if (allowReplacements) service.replaceText(el, match, replacement)
service.replaceText(el, match, replacement)
// TODO: Close popup // TODO: Close popup
}) })
replacementDiv.appendChild(replacementBtn) replacementDiv.appendChild(replacementBtn)
} }
} }
window.onload = () => {
document
.querySelectorAll('.bes-service')
.forEach(hostElement => BesService.register(hostElement))
}
window.onresize = () => {
besServices.forEach(service => {
service.setCorrectionPanelSize(
service.hostElement,
service.correctionPanel,
service.scrollPanel
)
service.children.forEach(child => {
service.clearMistakeMarkup(child.element)
child.matches.forEach(match => {
const { clientRects, highlights } = service.addMistakeMarkup(
match.range
)
match.rects = clientRects
match.highlights = highlights
})
})
})
}
customElements.define('bes-popup-el', BesPopupEl) customElements.define('bes-popup-el', BesPopupEl)