Revise event handling, document, cleanup

This commit is contained in:
Simon Rozman 2024-03-12 11:23:11 +01:00
parent 674e9498e6
commit bc0a3f7905

View File

@ -1,9 +1,9 @@
const besUrl = 'http://localhost:225/api/v2/check'
let besEditors = [] // Collection of all editors on page
let besEditors = [] // Collection of all grammar checking services in the document
class BesEditor {
constructor(edit) {
constructor(edit, isCkeditor) {
this.el = edit
this.timer = null
this.children = []
@ -11,31 +11,54 @@ class BesEditor {
this.correctionPanel = correctionPanel
this.scrollPanel = scrollPanel
this.offsetTop = null
this.isCKeditor = false
this.disableSpellcheck(edit)
this.proof(edit)
edit.addEventListener('beforeinput', e => this.handleBeforeInput(e), false)
edit.addEventListener('click', e => this.handleClick(e))
edit.addEventListener('scroll', e =>
this.handleScrollEvent(edit, this.scrollPanel)
)
}
// Register editor
static register(edit, isCkeditor) {
let editor = new BesEditor(edit)
besEditors.push(editor)
if (isCkeditor) editor.isCKeditor = true
return editor
}
// Set spellcheck to false
disableSpellcheck(edit) {
this.isCKeditor = !!isCkeditor
edit.classList.add('bes-online-editor')
this.originalSpellcheck = edit.spellcheck
edit.spellcheck = false
this.proof(edit)
edit.addEventListener('beforeinput', BesEditor.handleBeforeInput, false)
edit.addEventListener('click', BesEditor.handleClick)
edit.addEventListener('scroll', BesEditor.handleScroll)
besEditors.push(this)
}
/**
* Registers grammar checking service
*
* @param {Element} edit DOM element to register grammar checking service for
* @param {Boolean} isCkeditor Enable CKEditor tweaks
* @returns {BesEditor} Grammar checking service instance
*/
static register(edit, isCkeditor) {
return new BesEditor(edit, isCkeditor)
}
/**
* Unregisters grammar checking service
*/
unregister() {
this.el.removeEventListener('scroll', BesEditor.handleScroll)
this.el.removeEventListener('click', BesEditor.handleClick)
this.el.removeEventListener(
'beforeinput',
BesEditor.handleBeforeInput,
false
)
if (this.timer) clearTimeout(this.timer)
besEditors = besEditors.filter(item => item !== this)
this.el.spellcheck = this.originalSpellcheck
this.el.classList.remove('bes-online-editor')
this.correctionPanel.remove()
this.scrollPanel.remove()
}
// TODO: add support for textarea elements
// Recursively grammar-proofs one node.
/**
* Recursively grammar-proofs a DOM tree.
*
* @param {Node} el DOM root node to proof
* @returns {Array} Markup of text to proof using BesStr
*/
async proof(el) {
// If first child is not a block element, add a dummy <div>...</div> around it.
// This solution is still not fully tested and might need some improvements.
@ -196,27 +219,35 @@ class BesEditor {
return { correctionPanel, scrollPanel }
}
// Marks section of text that is about to change as not-yet-grammar-proofed.
handleBeforeInput(event) {
if (this.timer) clearTimeout(this.timer)
/**
* beforeinput event handler
*
* Marks section of the text that is about to change as not-yet-grammar-proofed.
*
* @param {InputEvent} event The event notifying the user of editable content changes
*/
static handleBeforeInput(event) {
const edit = event.target
let editor = besEditors.find(e => e.el === edit)
if (!editor) return
if (editor.timer) clearTimeout(editor.timer)
let blockElements = new Set()
event.getTargetRanges().forEach(range => {
BesEditor.getNodesInRange(range).forEach(el =>
blockElements.add(this.getBlockParent(el))
blockElements.add(editor.getBlockParent(el))
)
})
blockElements.forEach(block => {
this.clearProofed(block)
this.clearMistakeMarkup(block)
this.clearChildren(block)
editor.clearProofed(block)
editor.clearMistakeMarkup(block)
editor.clearChildren(block)
})
let editor = this
// Not the nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished.
// Not a nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished.
setTimeout(() => {
editor.repositionMistakes(editor)
editor.repositionMistakes()
}, 0)
this.timer = setTimeout(function () {
editor.proof(editor.el)
editor.timer = setTimeout(function () {
editor.proof(edit)
}, 1000)
}
@ -230,7 +261,7 @@ class BesEditor {
markProofed(el, matches) {
let newChild = {
isProofed: true,
elements: el,
elements: el, // TODO: Rename "elements" to "el" - 1. It contains only single element (plural elements is misleading), 2. BesEditor also uses "el" named field for DOM matching.
matches: matches
}
@ -269,8 +300,8 @@ class BesEditor {
else this.children = this.children.filter(child => child.elements !== el)
}
repositionMistakes(editor) {
editor.children.forEach(child => {
repositionMistakes() {
this.children.forEach(child => {
this.clearMistakeMarkup(child.elements)
child.matches.forEach(match => {
const { clientRects, highlight } = this.addMistakeMarkup(
@ -382,60 +413,71 @@ class BesEditor {
return nodes
}
handleClick(e) {
const targetEl = e.target
/**
* click event handler
*
* Displays or hides grammar mistake popup.
*
* @param {PointerEvent} event The event produced by a pointer such as the geometry of the contact point, the device type that generated the event, the amount of pressure that was applied on the contact surface, etc.
*/
static handleClick(event) {
const edit = BesEditor.findParent(event.target)
let editor = besEditors.find(e => e.el === edit)
if (!editor) return
const target = editor.getBlockParent(event.target)
const popup = document.querySelector('bes-popup-el')
// If target has not parent with class 'bes-online-editor', find target's parent whose parent is 'bes-online-editor'
const target = BesEditor.findParent(targetEl)
? BesEditor.findParent(targetEl)
: targetEl
const divIndex = this.children.findIndex(child => child.elements === target)
const matches = this.children[divIndex]?.matches
if (!matches) {
popup.hide()
return
}
const matches = editor.children.find(
child => child.elements === target
)?.matches
if (
BesEditor.renderPopup(target, matches, popup, e.clientX, e.clientY, this)
!matches ||
!editor.renderPopup(target, matches, popup, event.clientX, event.clientY)
)
return
else popup.hide()
popup.hide()
}
handleScrollEvent(editor, scrollPanel) {
scrollPanel.style.top = -editor.scrollTop + 'px'
this.offsetTop = editor.scrollTop
/**
* scroll event handler
*
* Syncs grammar mistake positions with editor scroll offset.
*
* @param {Event} event The event which takes place.
*/
static handleScroll(event) {
const edit = event.target
let editor = besEditors.find(e => e.el === edit)
if (!editor) return
editor.scrollPanel.style.top = -edit.scrollTop + 'px'
editor.offsetTop = edit.scrollTop
setTimeout(() => {
this.repositionMistakes(this)
editor.repositionMistakes()
}, 300)
// TODO: Move popup (if open) too.
}
/**
* Finds the editor with grammar checking service the given DOM node is a child of.
*
* @param {Node} target DOM node
* @returns {Element} Editor DOM element; null if DOM node is not a descendant of an editor.
*/
static findParent(target) {
let element = target
while (element && element.parentNode) {
if (element.parentNode.classList?.contains('bes-online-editor')) {
return element
for (let el = target; el; el = el.parentNode) {
if (el.classList?.contains('bes-online-editor')) {
return el
}
element = element.parentNode
}
return null
}
static renderPopup(el, matches, popup, clientX, clientY, editor) {
renderPopup(el, matches, popup, clientX, clientY) {
for (let m of matches) {
if (m.rects) {
for (let r of m.rects) {
if (BesEditor.isPointInRect(clientX, clientY, r)) {
popup.changeText(m.match.message)
m.match.replacements.forEach(replacement => {
popup.appendReplacements(
el,
r,
m.match,
replacement.value,
editor
)
popup.appendReplacements(el, r, m.match, replacement.value, this)
})
popup.show(clientX, clientY)
return true