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