From 67eee1015eec76451dbcfa1d1eacb3a045e4d2a9 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Tue, 21 May 2024 11:43:52 +0200 Subject: [PATCH] service2.js: Fix corner cases and prepare for plain-text services --- samples/div-contenteditable.html | 4 +- service2.js | 232 +++++++++++++------------------ 2 files changed, 95 insertions(+), 141 deletions(-) diff --git a/samples/div-contenteditable.html b/samples/div-contenteditable.html index 5baf6cb..93a9bcf 100644 --- a/samples/div-contenteditable.html +++ b/samples/div-contenteditable.html @@ -3,14 +3,14 @@ - BesService Example + BesService <div contenteditable="true"> Example -

This is an example of a simple <div contenteditable="true"> edit control. Edit the text, resize the control or browser window, scroll around...

+

This is an example of a simple <div contenteditable="true"> edit control. Edit the text, resize the control or browser window, scroll around, click...

Tukaj vpišite besedilo ki ga želite popraviti.

Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.

diff --git a/service2.js b/service2.js index 2ca3ed3..b51f7d1 100644 --- a/service2.js +++ b/service2.js @@ -25,6 +25,7 @@ window.addEventListener('scroll', () => class BesService { constructor(hostElement) { this.hostElement = hostElement + this.results = [] // Results of grammar-checking, one per each block/paragraph of text this.createCorrectionPanel() // Disable browser built-in spell-checker to prevent collision with our grammar markup. @@ -52,7 +53,7 @@ class BesService { * Called initially when grammar-checking run is started */ onStartProofing() { - this.proofingCount = 0 // Ref-count how many grammar-checking blocks of text are active + this.proofingCount = 1 // Ref-count how many grammar-checking blocks of text are active this.proofingError = null // The first non-fatal error in grammar-checking run this.proofingMatches = 0 // Number of grammar mistakes detected in entire grammar-checking run this.updateStatusIcon('bes-status-loading', 'Besana preverja pravopis.') @@ -126,6 +127,15 @@ class BesService { // Scroll panel is "position: absolute", we need to keep it aligned with the host element. this.scrollPanel.style.top = `${-this.hostElement.scrollTop}px` this.scrollPanel.style.left = `${-this.hostElement.scrollLeft}px` + + // Markup is in a "position:absolute"
element requiring repositioning when scrolling host element or window. + // It is defered to reduce stress in a flood of scroll events. + // TODO: We could technically just update scrollTop and scrollLeft of all markup rects for even better performance? + if (this.scrollTimeout) clearTimeout(this.scrollTimeout) + this.scrollTimeout = setTimeout(() => { + this.repositionAllMarkup() + delete this.scrollTimeout + }, 500) } /** @@ -134,6 +144,46 @@ class BesService { onResize() { this.setCorrectionPanelSize() this.setStatusDivPosition() + + // When window is resized, host element might resize too. + // This may cause text to re-wrap requiring markup repositioning. + this.repositionAllMarkup() + } + + /** + * Creates grammar mistake markup in DOM. + * + * @param {Range} range Grammar mistake range + * @returns {Object} Client rectangles and grammar mistake highlight elements + */ + addMistakeMarkup(range) { + const clientRects = range.getClientRects() + 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') + highlight.style.left = `${rect.left - scrollPanelRect.left}px` + highlight.style.top = `${rect.top - scrollPanelRect.top}px` + highlight.style.width = `${rect.width}px` + highlight.style.height = `${rect.height}px` + this.scrollPanel.appendChild(highlight) + highlights.push(highlight) + } + return { clientRects, highlights } + } + + /** + * Tests if given coordinate is inside of a rectangle. + * + * @param {Number} x X coordinate + * @param {Number} y Y coordinate + * @param {DOMRect} rect Rectangle + * @returns + */ + static isPointInRect(x, y, rect) { + return rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom } /** @@ -239,7 +289,6 @@ class BesService { class BesDOMService extends BesService { constructor(hostElement) { super(hostElement) - this.results = [] // Results of grammar-checking, one per each block of text this.onBeforeInput = this.onBeforeInput.bind(this) this.hostElement.addEventListener('beforeinput', this.onBeforeInput) this.onInput = this.onInput.bind(this) @@ -271,33 +320,6 @@ class BesDOMService extends BesService { super.unregister() } - /** - * Called to report scrolling - */ - onScroll() { - super.onScroll() - - // Markup is in a "position:absolute"
element requiring repositioning when scrolling host element or window. - // It is defered to reduce stress in a flood of scroll events. - // TODO: We could technically just update scrollTop and scrollLeft of all markup rects for even better performance? - if (this.scrollTimeout) clearTimeout(this.scrollTimeout) - this.scrollTimeout = setTimeout(() => { - this.repositionAllMarkup() - delete this.scrollTimeout - }, 500) - } - - /** - * Called to report resizing - */ - onResize() { - super.onResize() - - // When window is resized, host element might resize too. - // This may cause text to re-wrap requiring markup re - this.repositionAllMarkup() - } - /** * Called to report the text is about to change * @@ -312,13 +334,9 @@ class BesDOMService extends BesService { // Remove markup of all blocks of text that are about to change. let blockElements = new Set() event.getTargetRanges().forEach(range => { - BesDOMService.getNodesInRange(range).forEach(el => { - if ( - el === this.hostElement || - Array.from(this.hostElement.childNodes).includes(el) - ) - blockElements.add(this.getBlockParent(el)) - }) + BesDOMService.getNodesInRange(range).forEach(el => + blockElements.add(this.getBlockParent(el)) + ) }) blockElements.forEach(block => this.clearProofing(block)) } @@ -343,11 +361,7 @@ class BesDOMService extends BesService { proofAll() { this.onStartProofing() this.proofNode(this.hostElement, this.abortController) - if (this.proofingCount == 0) { - // No text blocks were discovered for proofing. onProofingProgress() will not be called - // and we need to notify manually. - this.onEndProofing() - } + this.onProofingProgress(0) } /** @@ -365,15 +379,18 @@ class BesDOMService extends BesService { case Node.ELEMENT_NODE: if (this.isBlockElement(node)) { // Block elements are grammar-checked independently. - if (this.isProofed(node)) + this.onProofing() + let result = this.getProofing(node) + if (result != null) { + this.onProofingProgress(result.matches.length) return [{ text: `<${node.tagName}/>`, node: node, markup: true }] + } let data = [] for (const el2 of node.childNodes) data = data.concat(this.proofNode(el2, abortController)) if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) { // Block element contains some text. - this.onProofing() const signal = abortController.signal fetch( new Request(besUrl + '/check', { @@ -477,10 +494,12 @@ class BesDOMService extends BesService { * Tests if given block element has already been grammar-checked. * * @param {Element} el DOM element to check - * @returns {Boolean} true if the element has already been grammar-checked; false otherwise. + * @returns {*} Result of grammar check if the element has already been grammar-checked; null otherwise. */ - isProofed(el) { - return this.results?.find(result => result.element === el) != null + getProofing(el) { + return this.results.find(result => + BesDOMService.isSameParagraph(result.element, el) + ) } /** @@ -504,51 +523,9 @@ class BesDOMService extends BesService { */ clearProofing(el) { this.clearMarkup(el) - this.results = this.results?.filter(result => result.element !== el) - } - - /** - * Creates grammar mistake markup in DOM. - * - * @param {Range} range Grammar mistake range - * @returns {Object} Client rectangles and grammar mistake highlight elements - */ - addMistakeMarkup(range) { - const clientRects = range.getClientRects() - 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 - highlight.style.left = `${leftPosition}px` - highlight.style.top = `${topPosition}px` - highlight.style.width = `${rect.width}px` - highlight.style.height = `${rect.height}px` - this.scrollPanel.appendChild(highlight) - highlights.push(highlight) - } - return { clientRects, highlights } - } - - /** - * Updates grammar mistake markup positions. - * - * @param {Element} el DOM element we want to update markup for - * - * TODO: Unused - */ - repositionMarkup(el) { - let result = this.results?.find(result => result.element === el) - if (!result) return - result.matches.forEach(match => { - const { clientRects, highlights } = this.addMistakeMarkup(match.range) - match.rects = clientRects - if (match.highlights) match.highlights.forEach(h => h.remove()) - match.highlights = highlights - }) + this.results = this.results.filter( + result => !BesDOMService.isSameParagraph(result.element, el) + ) } /** @@ -571,30 +548,27 @@ class BesDOMService extends BesService { * @param {Element} el DOM element we want to clean markup for */ clearMarkup(el) { - let result = this.results?.find(result => result.element === el) - if (!result) return - result.matches.forEach(match => { - if (match.highlights) { - match.highlights.forEach(h => h.remove()) - delete match.highlights - } - }) + this.results + .filter(result => BesDOMService.isSameParagraph(result.element, el)) + .forEach(result => + result.matches.forEach(match => { + if (match.highlights) { + match.highlights.forEach(h => h.remove()) + delete match.highlights + } + }) + ) } /** - * Clears all grammar mistake markup. + * Tests if given block elements represent the same block of text * - * TODO: Unused + * @param {Element} el1 DOM element + * @param {Element} el2 DOM element + * @returns {Boolean} true if block elements are the same */ - clearAllMarkup() { - this.results.forEach(result => { - result.matches.forEach(match => { - if (match.highlights) { - match.highlights.forEach(h => h.remove()) - delete match.highlights - } - }) - }) + static isSameParagraph(el1, el2) { + return el1 === el2 } /** @@ -714,23 +688,21 @@ class BesDOMService extends BesService { */ onClick(event) { const source = event?.detail !== 1 ? event?.detail : event - const target = this.getBlockParent(source.targetElement || source.target) - if (!target) return + const el = this.getBlockParent(source.targetElement || source.target) + if (!el) return - const matches = this.results?.find( - child => child.element === target + const matches = this.results.find(child => + BesDOMService.isSameParagraph(child.element, el) )?.matches if (matches) { - const popup = document.querySelector('bes-popup-el') for (let m of matches) { if (m.rects) { for (let r of m.rects) { - if ( - BesDOMService.isPointInRect(source.clientX, source.clientY, r) - ) { + if (BesService.isPointInRect(source.clientX, source.clientY, r)) { + const popup = document.querySelector('bes-popup-el') popup.changeMessage(m.match.message) popup.appendReplacements( - target, + el, m, this, this.hostElement.contentEditable !== 'false' @@ -745,18 +717,6 @@ class BesDOMService extends BesService { BesPopup.hide() } - /** - * Tests if given coordinate is inside of a rectangle. - * - * @param {Number} x X coordinate - * @param {Number} y Y coordinate - * @param {DOMRect} rect Rectangle - * @returns - */ - static isPointInRect(x, y, rect) { - return rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom - } - /** * Replaces grammar checking match with a suggestion provided by grammar checking service. * @@ -770,13 +730,7 @@ class BesDOMService extends BesService { this.clearProofing(el) match.range.deleteContents() match.range.insertNode(document.createTextNode(replacement)) - this.onStartProofing() - this.proofNode(el, this.abortController) - if (this.proofingCount == 0) { - // No text blocks were discovered for proofing. onProofingProgress() will not be called - // and we need to notify manually. - this.onEndProofing() - } + this.proofAll() } } @@ -919,7 +873,7 @@ class BesPopup extends HTMLElement { /** * Adds a grammar mistake suggestion. * - * @param {Element} el Block element containing the grammar mistake + * @param {*} el Block element/paragraph containing the grammar mistake * @param {*} match Grammar checking rule match * @param {BesService} service Grammar checking service * @param {Boolean} allowReplacements Host element is mutable and grammar mistake may be replaced by suggestion