Split BesService to isolate CKEditor specifics in BesCKService
This commit is contained in:
parent
63584185ae
commit
87c7f79ebe
@ -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)
|
||||||
|
202
service.js
202
service.js
@ -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)
|
match.range.deleteContents()
|
||||||
if (!this.ckEditorInstance) {
|
match.range.insertNode(document.createTextNode(replacement))
|
||||||
match.range.deleteContents()
|
|
||||||
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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is popup element
|
///
|
||||||
|
/// 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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user