From bc0a3f790505afb6c080b561f729290e308b7597 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Tue, 12 Mar 2024 11:23:11 +0100 Subject: [PATCH] Revise event handling, document, cleanup --- online-editor.js | 184 +++++++++++++++++++++++++++++------------------ 1 file changed, 113 insertions(+), 71 deletions(-) diff --git a/online-editor.js b/online-editor.js index e8b849c..547cfda 100644 --- a/online-editor.js +++ b/online-editor.js @@ -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
...
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