diff --git a/service.js b/service.js index 2858163..670e27e 100644 --- a/service.js +++ b/service.js @@ -60,6 +60,8 @@ class BesService { this.hostElement.setAttribute('data-gramm', 'false') this.hostElement.setAttribute('data-gramm_editor', 'false') this.hostElement.setAttribute('data-enable-grammarly', 'false') + this.onTab = this.onTab.bind(this) + this.hostElement.addEventListener('keydown', this.onTab) this.onScroll = this.onScroll.bind(this) this.hostElement.addEventListener('scroll', this.onScroll) @@ -276,6 +278,9 @@ class BesService { */ onProofingProgress(numberOfMatches) { this.proofingMatches += numberOfMatches + // Sorting the array here is preferable to sorting only in onEndProofing.This way it allows users to interact + // with and navigate newly detected mistakes as soon as they appear. + this.sortMatchesArray() if (this.eventSink && 'proofingProgress' in this.eventSink) this.eventSink.proofingProgress(this) if (--this.proofingCount <= 0) this.onEndProofing() @@ -324,6 +329,19 @@ class BesService { } } + /** + * Called to report tab event + * + * @param {Event} e + */ + onTab(e) { + if (e.key === 'Tab' && this.highlightElements.length) { + e.preventDefault() + e.stopPropagation() + this.findNextMistake(e.shiftKey ? -1 : 1) + } + } + /** * Called to report repositioning */ @@ -1120,19 +1138,79 @@ class BesService { el.style.width = `${rect.width}px` el.style.height = `${rect.height}px` document.body.appendChild(el) - this.highlightElements.push(el) + const matchSorted = + this.sortedMatches.find(entry => entry.match === match) || null + this.highlightElements.push({ el, matchSorted }) }) } + /** + * This function calculates / finds the next mistake. + * @param {Number} direction Navigation direction: 1 for next, -1 for previous + * @returns + */ + findNextMistake(direction = 1) { + if (!this.sortedMatches || !this.sortedMatches.length) return + const active = this.highlightElements.find(({ matchSorted }) => matchSorted) + let current = -1 + if (active && active.matchSorted) { + current = this.sortedMatches.findIndex( + entry => entry.match === active.matchSorted.match + ) + } + + const len = this.sortedMatches.length + const next = (current + direction + len) % len + this.activeMatchIndex = next + const { el, match } = this.sortedMatches[next] + + // TODO: find out why scrollintoview does not work well + this.dismissPopup() + const popup = document.querySelector('bes-popup-el') + BesPopup.clearReplacements() + popup.setContent(el, match, this, this.isContentEditable()) + this.highlightMistake(match) + popup.show(match.highlights[0].x, match.highlights[0].y) + } + /** * Clears highlight and hides popup */ dismissPopup() { BesPopup.hide() - this.highlightElements.forEach(el => el.remove()) + this.highlightElements.forEach(obj => { + if (obj.el && typeof obj.el.remove === 'function') { + obj.el.remove() + } + }) this.highlightElements = [] } + /** + * This function collects all matches from the results array, flattens them into a single array, + * and sorts them in order: first by their Y axis, then by X axis. + */ + sortMatchesArray() { + this.sortedMatches = [] + this.results.forEach(element => { + element.matches.forEach(match => { + if (!match.highlights || !match.highlights.length) return + this.sortedMatches.push({ + el: element.element, + match, + top: match.highlights[0].top + }) + }) + }) + this.sortedMatches.sort((a, b) => { + if (a.top !== b.top) return a.top - b.top + + const aLeft = a.match.highlights[0].left + const bLeft = b.match.highlights[0].left + return aLeft - bLeft + }) + } + /** * Checks if host element content is editable. *