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' 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 { class BesEditor {
constructor(edit) { constructor(edit, isCkeditor) {
this.el = edit this.el = edit
this.timer = null this.timer = null
this.children = [] this.children = []
@ -11,31 +11,54 @@ class BesEditor {
this.correctionPanel = correctionPanel this.correctionPanel = correctionPanel
this.scrollPanel = scrollPanel this.scrollPanel = scrollPanel
this.offsetTop = null this.offsetTop = null
this.isCKeditor = false this.isCKeditor = !!isCkeditor
this.disableSpellcheck(edit) edit.classList.add('bes-online-editor')
this.proof(edit) this.originalSpellcheck = edit.spellcheck
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) {
edit.spellcheck = false 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 // 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) { async proof(el) {
// If first child is not a block element, add a dummy <div>...</div> around it. // 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. // This solution is still not fully tested and might need some improvements.
@ -196,27 +219,35 @@ class BesEditor {
return { correctionPanel, scrollPanel } return { correctionPanel, scrollPanel }
} }
// Marks section of text that is about to change as not-yet-grammar-proofed. /**
handleBeforeInput(event) { * beforeinput event handler
if (this.timer) clearTimeout(this.timer) *
* 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() let blockElements = new Set()
event.getTargetRanges().forEach(range => { event.getTargetRanges().forEach(range => {
BesEditor.getNodesInRange(range).forEach(el => BesEditor.getNodesInRange(range).forEach(el =>
blockElements.add(this.getBlockParent(el)) blockElements.add(editor.getBlockParent(el))
) )
}) })
blockElements.forEach(block => { blockElements.forEach(block => {
this.clearProofed(block) editor.clearProofed(block)
this.clearMistakeMarkup(block) editor.clearMistakeMarkup(block)
this.clearChildren(block) editor.clearChildren(block)
}) })
let editor = this // Not a nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished.
// Not the nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished.
setTimeout(() => { setTimeout(() => {
editor.repositionMistakes(editor) editor.repositionMistakes()
}, 0) }, 0)
this.timer = setTimeout(function () { editor.timer = setTimeout(function () {
editor.proof(editor.el) editor.proof(edit)
}, 1000) }, 1000)
} }
@ -230,7 +261,7 @@ class BesEditor {
markProofed(el, matches) { markProofed(el, matches) {
let newChild = { let newChild = {
isProofed: true, 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 matches: matches
} }
@ -269,8 +300,8 @@ class BesEditor {
else this.children = this.children.filter(child => child.elements !== el) else this.children = this.children.filter(child => child.elements !== el)
} }
repositionMistakes(editor) { repositionMistakes() {
editor.children.forEach(child => { this.children.forEach(child => {
this.clearMistakeMarkup(child.elements) this.clearMistakeMarkup(child.elements)
child.matches.forEach(match => { child.matches.forEach(match => {
const { clientRects, highlight } = this.addMistakeMarkup( const { clientRects, highlight } = this.addMistakeMarkup(
@ -382,60 +413,71 @@ class BesEditor {
return nodes 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') 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 matches = editor.children.find(
const target = BesEditor.findParent(targetEl) child => child.elements === target
? BesEditor.findParent(targetEl) )?.matches
: targetEl
const divIndex = this.children.findIndex(child => child.elements === target)
const matches = this.children[divIndex]?.matches
if (!matches) {
popup.hide()
return
}
if ( if (
BesEditor.renderPopup(target, matches, popup, e.clientX, e.clientY, this) !matches ||
!editor.renderPopup(target, matches, popup, event.clientX, event.clientY)
) )
return popup.hide()
else popup.hide()
} }
handleScrollEvent(editor, scrollPanel) { /**
scrollPanel.style.top = -editor.scrollTop + 'px' * scroll event handler
this.offsetTop = editor.scrollTop *
* 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(() => { setTimeout(() => {
this.repositionMistakes(this) editor.repositionMistakes()
}, 300) }, 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) { static findParent(target) {
let element = target for (let el = target; el; el = el.parentNode) {
while (element && element.parentNode) { if (el.classList?.contains('bes-online-editor')) {
if (element.parentNode.classList?.contains('bes-online-editor')) { return el
return element
} }
element = element.parentNode
} }
return null return null
} }
static renderPopup(el, matches, popup, clientX, clientY, editor) { renderPopup(el, matches, popup, clientX, clientY) {
for (let m of matches) { for (let m of matches) {
if (m.rects) { if (m.rects) {
for (let r of m.rects) { for (let r of m.rects) {
if (BesEditor.isPointInRect(clientX, clientY, r)) { if (BesEditor.isPointInRect(clientX, clientY, r)) {
popup.changeText(m.match.message) popup.changeText(m.match.message)
m.match.replacements.forEach(replacement => { m.match.replacements.forEach(replacement => {
popup.appendReplacements( popup.appendReplacements(el, r, m.match, replacement.value, this)
el,
r,
m.match,
replacement.value,
editor
)
}) })
popup.show(clientX, clientY) popup.show(clientX, clientY)
return true return true