Add some more documentation, upgrade addMistakeMarkup for multiline

This commit is contained in:
Simon Rozman 2024-03-12 12:58:26 +01:00
parent bc0a3f7905
commit 259fc25f6f

View File

@ -158,13 +158,11 @@ class BesEditor {
} }
} }
const { clientRects, highlight } = this.addMistakeMarkup( const { clientRects, highlights } =
range, this.addMistakeMarkup(range)
this.scrollPanel
)
matches.push({ matches.push({
rects: clientRects, rects: clientRects,
highlight: highlight, highlights: highlights,
range: range, range: range,
match: match match: match
}) })
@ -238,9 +236,8 @@ class BesEditor {
) )
}) })
blockElements.forEach(block => { blockElements.forEach(block => {
editor.clearProofed(block)
editor.clearMistakeMarkup(block) editor.clearMistakeMarkup(block)
editor.clearChildren(block) editor.removeChild(block)
}) })
// Not a 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(() => { setTimeout(() => {
@ -251,78 +248,84 @@ class BesEditor {
}, 1000) }, 1000)
} }
// Test if given block element has already been grammar-proofed. /**
* Tests if given block element has already been grammar-proofed.
*
* @param {Element} el DOM element to check
* @returns {Boolean} true if the element has already been grammar-proofed; false otherwise.
*/
isProofed(el) { isProofed(el) {
let filteredChildren = this.children.filter(child => child.elements === el) return this.children.find(child => child.elements === el)?.isProofed
return filteredChildren[0]?.isProofed
} }
// Mark given block element as grammar-proofed. /**
* Marks given block element as grammar-proofed.
*
* @param {Element} el DOM element that was checked
* @param {Array} matches Grammar mistakes
*/
markProofed(el, matches) { markProofed(el, matches) {
let newChild = { this.removeChild(el)
this.children.push({
isProofed: true, isProofed: true,
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. 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
} })
this.children = this.children.map(child =>
child.elements === newChild.elements ? newChild : child
)
if (!this.children.some(child => child.elements === newChild.elements)) {
this.children.push(newChild)
}
} }
// Mark given block element as not grammar-proofed. /**
clearProofed(el) { * Clears given block element as not grammar-proofed and removes all its grammar mistakes.
this.children *
.filter(child => child.elements === el) * @param {Element} el DOM element that we should re-grammar-proof
.forEach(child => { */
child.isProofed = false
})
}
// Remove all grammar mistakes markup for given block element.
clearMistakeMarkup(el) { clearMistakeMarkup(el) {
this.children let child = this.children.find(child => child.elements === el)
.filter(child => child.elements === el) if (!child) return
.forEach(child => { child.isProofed = false
child.matches.forEach(match => { child.matches.forEach(match => {
match.highlight.remove() match.highlights.forEach(h => h.remove())
delete match.highlight delete match.highlights
}) })
})
} }
// Remove all children from this.children array /**
clearChildren(el) { * Removes given block element from this.children array
if (el?.classList.contains('bes-online-editor')) return *
else this.children = this.children.filter(child => child.elements !== el) * @param {Element} el DOM element for removal
*/
removeChild(el) {
this.children = this.children.filter(child => child.elements !== el)
} }
/**
* Updates grammar mistake markup positions.
*/
repositionMistakes() { repositionMistakes() {
this.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, highlights } = this.addMistakeMarkup(match.range)
match.range,
this.scrollPanel
)
match.rects = clientRects match.rects = clientRects
match.highlight = highlight match.highlights = highlights
}) })
}) })
} }
// Adds grammar mistake markup /**
addMistakeMarkup(range, scrollPanel) { * Adds grammar mistake markup.
*
* @param {Range} range Grammar mistake range
* @returns {Object} Client rectangles and grammar mistake highlight elements
*/
addMistakeMarkup(range) {
// TODO: Consider using range.getClientRects() instead of range.getBoundingClientRect() // TODO: Consider using range.getClientRects() instead of range.getBoundingClientRect()
// In CKEditor case, the highlight element is not shown for some reason. But after resizing the window it is shown. // In CKEditor case, the highlight element is not shown for some reason. But after resizing the window it is shown.
const clientRects = range.getClientRects() const clientRects = range.getClientRects()
const scrollPanelRect = scrollPanel.getBoundingClientRect() const scrollPanelRect = this.scrollPanel.getBoundingClientRect()
const highlight = document.createElement('div') let highlights = []
for (let i = 0, n = clientRects.length; i < n; ++i) { for (let i = 0, n = clientRects.length; i < n; ++i) {
const rect = clientRects[i] const rect = clientRects[i]
const highlight = document.createElement('div')
highlight.classList.add('bes-typo-mistake') highlight.classList.add('bes-typo-mistake')
const topPosition = rect.top - scrollPanelRect.top const topPosition = rect.top - scrollPanelRect.top
const leftPosition = rect.left - scrollPanelRect.left const leftPosition = rect.left - scrollPanelRect.left
@ -331,11 +334,17 @@ class BesEditor {
highlight.style.width = `${rect.width}px` highlight.style.width = `${rect.width}px`
highlight.style.height = `${rect.height}px` highlight.style.height = `${rect.height}px`
this.scrollPanel.appendChild(highlight) this.scrollPanel.appendChild(highlight)
highlights.push(highlight)
} }
return { clientRects, highlight } return { clientRects, highlights }
} }
// Tests if given element is block element. /**
* Tests if given element is block element.
*
* @param {Element} el DOM element
* @returns false if CSS display property is inline or inline-block; true otherwise.
*/
static isBlockElement(el) { static isBlockElement(el) {
switch ( switch (
document.defaultView document.defaultView
@ -351,7 +360,12 @@ class BesEditor {
} }
} }
// Returns first block parent element /**
* Returns first block parent element of a node.
*
* @param {Node} el DOM node
* @returns {Element} Innermost block element containing given node
*/
getBlockParent(el) { getBlockParent(el) {
for (; el && el !== this.el; el = el.parentNode) { for (; el && el !== this.el; el = el.parentNode) {
if (el.nodeType === Node.ELEMENT_NODE && BesEditor.isBlockElement(el)) if (el.nodeType === Node.ELEMENT_NODE && BesEditor.isBlockElement(el))
@ -360,6 +374,12 @@ class BesEditor {
return el return el
} }
/**
* Returns next node in the DOM text flow.
*
* @param {Node} node DOM node
* @returns {Node} Next node
*/
static getNextNode(node) { static getNextNode(node) {
if (node.firstChild) return node.firstChild if (node.firstChild) return node.firstChild
while (node) { while (node) {
@ -368,6 +388,12 @@ class BesEditor {
} }
} }
/**
* Returns all ancestors of a node.
*
* @param {Node} node DOM node
* @returns {Array} Array of all ancestors in reverse order: node, immediate parent, ..., document
*/
static getParents(node) { static getParents(node) {
let parents = [] let parents = []
do { do {
@ -377,6 +403,12 @@ class BesEditor {
return parents.reverse() return parents.reverse()
} }
/**
* Returns all nodes marked by a range.
*
* @param {Range} range DOM range
* @returns {Array} Array of nodes
*/
static getNodesInRange(range) { static getNodesInRange(range) {
var start = range.startContainer var start = range.startContainer
var end = range.endContainer var end = range.endContainer
@ -425,15 +457,7 @@ class BesEditor {
let editor = besEditors.find(e => e.el === edit) let editor = besEditors.find(e => e.el === edit)
if (!editor) return if (!editor) return
const target = editor.getBlockParent(event.target) const target = editor.getBlockParent(event.target)
const popup = document.querySelector('bes-popup-el') editor.renderPopup(target, event.clientX, event.clientY)
const matches = editor.children.find(
child => child.elements === target
)?.matches
if (
!matches ||
!editor.renderPopup(target, matches, popup, event.clientX, event.clientY)
)
popup.hide()
} }
/** /**
@ -456,13 +480,13 @@ class BesEditor {
} }
/** /**
* Finds the editor with grammar checking service the given DOM node is a child of. * Finds the editor with grammar checking service a DOM node is child of.
* *
* @param {Node} target DOM node * @param {Node} el DOM node
* @returns {Element} Editor DOM element; null if DOM node is not a descendant of an editor. * @returns {Element} Editor DOM element; null if DOM node is not a descendant of an editor.
*/ */
static findParent(target) { static findParent(el) {
for (let el = target; el; el = el.parentNode) { for (; el; el = el.parentNode) {
if (el.classList?.contains('bes-online-editor')) { if (el.classList?.contains('bes-online-editor')) {
return el return el
} }
@ -470,24 +494,41 @@ class BesEditor {
return null return null
} }
renderPopup(el, matches, popup, clientX, clientY) { /**
for (let m of matches) { * Displays grammar mistake explanation popup.
if (m.rects) { *
for (let r of m.rects) { * @param {*} el DOM element we have grammar proofing available for
if (BesEditor.isPointInRect(clientX, clientY, r)) { * @param {*} clientX Client X coordinate of the pointer event
popup.changeText(m.match.message) * @param {*} clientY Client Y coordinate of the pointer event
m.match.replacements.forEach(replacement => { */
popup.appendReplacements(el, r, m.match, replacement.value, this) renderPopup(el, clientX, clientY) {
}) const popup = document.querySelector('bes-popup-el')
popup.show(clientX, clientY) const matches = this.children.find(
return true child => child.elements === el
)?.matches
if (matches) {
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,
this
)
})
popup.show(clientX, clientY)
return
}
} }
} }
} else {
popup.hide()
} }
} }
return false popup.hide()
} }
// TODO: In rich HTML texts, match.offset has different value than in plain text. // TODO: In rich HTML texts, match.offset has different value than in plain text.
@ -592,12 +633,9 @@ window.onresize = () => {
editor.children.forEach(child => { editor.children.forEach(child => {
editor.clearMistakeMarkup(child.elements) editor.clearMistakeMarkup(child.elements)
child.matches.forEach(match => { child.matches.forEach(match => {
const { clientRects, highlight } = editor.addMistakeMarkup( const { clientRects, highlights } = editor.addMistakeMarkup(match.range)
match.range,
editor.scrollPanel
)
match.rects = clientRects match.rects = clientRects
match.highlight = highlight match.highlights = highlights
}) })
}) })
}) })