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'
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user