Implement tab navigation for grammar mistakes

- The feature is still not fully finished yet.
- Navigation does not work in Quill, textarea and static-content examples.
- But it works well in CKEditor and contenteditable examples.

#4
This commit is contained in:
Aljaž Grilc 2025-06-03 14:59:25 +02:00
parent b9ab9b6a64
commit c7c90101a2

View File

@ -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.
*