diff --git a/online-editor.js b/online-editor.js index 547cfda..baef5ac 100644 --- a/online-editor.js +++ b/online-editor.js @@ -158,13 +158,11 @@ class BesEditor { } } - const { clientRects, highlight } = this.addMistakeMarkup( - range, - this.scrollPanel - ) + const { clientRects, highlights } = + this.addMistakeMarkup(range) matches.push({ rects: clientRects, - highlight: highlight, + highlights: highlights, range: range, match: match }) @@ -238,9 +236,8 @@ class BesEditor { ) }) blockElements.forEach(block => { - editor.clearProofed(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. setTimeout(() => { @@ -251,78 +248,84 @@ class BesEditor { }, 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) { - let filteredChildren = this.children.filter(child => child.elements === el) - return filteredChildren[0]?.isProofed + return this.children.find(child => child.elements === el)?.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) { - let newChild = { + this.removeChild(el) + this.children.push({ 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. 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) { - this.children - .filter(child => child.elements === el) - .forEach(child => { - child.isProofed = false - }) - } - - // Remove all grammar mistakes markup for given block element. + /** + * Clears given block element as not grammar-proofed and removes all its grammar mistakes. + * + * @param {Element} el DOM element that we should re-grammar-proof + */ clearMistakeMarkup(el) { - this.children - .filter(child => child.elements === el) - .forEach(child => { - child.matches.forEach(match => { - match.highlight.remove() - delete match.highlight - }) - }) + let child = this.children.find(child => child.elements === el) + if (!child) return + child.isProofed = false + child.matches.forEach(match => { + match.highlights.forEach(h => h.remove()) + delete match.highlights + }) } - // Remove all children from this.children array - clearChildren(el) { - if (el?.classList.contains('bes-online-editor')) return - else this.children = this.children.filter(child => child.elements !== el) + /** + * Removes given block element from this.children array + * + * @param {Element} el DOM element for removal + */ + removeChild(el) { + this.children = this.children.filter(child => child.elements !== el) } + /** + * Updates grammar mistake markup positions. + */ repositionMistakes() { this.children.forEach(child => { this.clearMistakeMarkup(child.elements) child.matches.forEach(match => { - const { clientRects, highlight } = this.addMistakeMarkup( - match.range, - this.scrollPanel - ) + const { clientRects, highlights } = this.addMistakeMarkup(match.range) 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() // 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 scrollPanelRect = scrollPanel.getBoundingClientRect() - const highlight = document.createElement('div') + const scrollPanelRect = this.scrollPanel.getBoundingClientRect() + let highlights = [] for (let i = 0, n = clientRects.length; i < n; ++i) { const rect = clientRects[i] + const highlight = document.createElement('div') highlight.classList.add('bes-typo-mistake') const topPosition = rect.top - scrollPanelRect.top const leftPosition = rect.left - scrollPanelRect.left @@ -331,11 +334,17 @@ class BesEditor { highlight.style.width = `${rect.width}px` highlight.style.height = `${rect.height}px` 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) { switch ( 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) { for (; el && el !== this.el; el = el.parentNode) { if (el.nodeType === Node.ELEMENT_NODE && BesEditor.isBlockElement(el)) @@ -360,6 +374,12 @@ class BesEditor { return el } + /** + * Returns next node in the DOM text flow. + * + * @param {Node} node DOM node + * @returns {Node} Next node + */ static getNextNode(node) { if (node.firstChild) return node.firstChild 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) { let parents = [] do { @@ -377,6 +403,12 @@ class BesEditor { return parents.reverse() } + /** + * Returns all nodes marked by a range. + * + * @param {Range} range DOM range + * @returns {Array} Array of nodes + */ static getNodesInRange(range) { var start = range.startContainer var end = range.endContainer @@ -425,15 +457,7 @@ class BesEditor { 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 matches = editor.children.find( - child => child.elements === target - )?.matches - if ( - !matches || - !editor.renderPopup(target, matches, popup, event.clientX, event.clientY) - ) - popup.hide() + editor.renderPopup(target, event.clientX, event.clientY) } /** @@ -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. */ - static findParent(target) { - for (let el = target; el; el = el.parentNode) { + static findParent(el) { + for (; el; el = el.parentNode) { if (el.classList?.contains('bes-online-editor')) { return el } @@ -470,24 +494,41 @@ class BesEditor { return null } - 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, this) - }) - popup.show(clientX, clientY) - return true + /** + * Displays grammar mistake explanation popup. + * + * @param {*} el DOM element we have grammar proofing available for + * @param {*} clientX Client X coordinate of the pointer event + * @param {*} clientY Client Y coordinate of the pointer event + */ + renderPopup(el, clientX, clientY) { + const popup = document.querySelector('bes-popup-el') + const matches = this.children.find( + 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. @@ -592,12 +633,9 @@ window.onresize = () => { editor.children.forEach(child => { editor.clearMistakeMarkup(child.elements) child.matches.forEach(match => { - const { clientRects, highlight } = editor.addMistakeMarkup( - match.range, - editor.scrollPanel - ) + const { clientRects, highlights } = editor.addMistakeMarkup(match.range) match.rects = clientRects - match.highlight = highlight + match.highlights = highlights }) }) })