Revise event handling, document, cleanup
This commit is contained in:
parent
674e9498e6
commit
bc0a3f7905
184
online-editor.js
184
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 <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
|
||||
|
Loading…
x
Reference in New Issue
Block a user