service2.js: Fix corner cases and prepare for plain-text services
This commit is contained in:
parent
e05abce7d9
commit
67eee1015e
@ -3,14 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BesService Example</title>
|
<title>BesService <div contenteditable="true"> Example</title>
|
||||||
<link rel="stylesheet" href="../styles.css" />
|
<link rel="stylesheet" href="../styles.css" />
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
<script>const besUrl = 'http://localhost:225/api/v2';</script>
|
<script>const besUrl = 'http://localhost:225/api/v2';</script>
|
||||||
<script src="../service2.js"></script>
|
<script src="../service2.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p class="my-block">This is an example of a simple <code><div contenteditable="true"></code> edit control. Edit the text, resize the control or browser window, scroll around...</p>
|
<p class="my-block">This is an example of a simple <code><div contenteditable="true"></code> edit control. Edit the text, resize the control or browser window, scroll around, click...</p>
|
||||||
<div class="my-block my-control bes-service" contenteditable="true">
|
<div class="my-block my-control bes-service" contenteditable="true">
|
||||||
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
|
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
|
232
service2.js
232
service2.js
@ -25,6 +25,7 @@ window.addEventListener('scroll', () =>
|
|||||||
class BesService {
|
class BesService {
|
||||||
constructor(hostElement) {
|
constructor(hostElement) {
|
||||||
this.hostElement = hostElement
|
this.hostElement = hostElement
|
||||||
|
this.results = [] // Results of grammar-checking, one per each block/paragraph of text
|
||||||
this.createCorrectionPanel()
|
this.createCorrectionPanel()
|
||||||
|
|
||||||
// Disable browser built-in spell-checker to prevent collision with our grammar markup.
|
// 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
|
* Called initially when grammar-checking run is started
|
||||||
*/
|
*/
|
||||||
onStartProofing() {
|
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.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.proofingMatches = 0 // Number of grammar mistakes detected in entire grammar-checking run
|
||||||
this.updateStatusIcon('bes-status-loading', 'Besana preverja pravopis.')
|
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.
|
// 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.top = `${-this.hostElement.scrollTop}px`
|
||||||
this.scrollPanel.style.left = `${-this.hostElement.scrollLeft}px`
|
this.scrollPanel.style.left = `${-this.hostElement.scrollLeft}px`
|
||||||
|
|
||||||
|
// Markup is in a "position:absolute" <div> 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() {
|
onResize() {
|
||||||
this.setCorrectionPanelSize()
|
this.setCorrectionPanelSize()
|
||||||
this.setStatusDivPosition()
|
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 {
|
class BesDOMService extends BesService {
|
||||||
constructor(hostElement) {
|
constructor(hostElement) {
|
||||||
super(hostElement)
|
super(hostElement)
|
||||||
this.results = [] // Results of grammar-checking, one per each block of text
|
|
||||||
this.onBeforeInput = this.onBeforeInput.bind(this)
|
this.onBeforeInput = this.onBeforeInput.bind(this)
|
||||||
this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
|
this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
|
||||||
this.onInput = this.onInput.bind(this)
|
this.onInput = this.onInput.bind(this)
|
||||||
@ -271,33 +320,6 @@ class BesDOMService extends BesService {
|
|||||||
super.unregister()
|
super.unregister()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to report scrolling
|
|
||||||
*/
|
|
||||||
onScroll() {
|
|
||||||
super.onScroll()
|
|
||||||
|
|
||||||
// Markup is in a "position:absolute" <div> 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
|
* 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.
|
// Remove markup of all blocks of text that are about to change.
|
||||||
let blockElements = new Set()
|
let blockElements = new Set()
|
||||||
event.getTargetRanges().forEach(range => {
|
event.getTargetRanges().forEach(range => {
|
||||||
BesDOMService.getNodesInRange(range).forEach(el => {
|
BesDOMService.getNodesInRange(range).forEach(el =>
|
||||||
if (
|
blockElements.add(this.getBlockParent(el))
|
||||||
el === this.hostElement ||
|
)
|
||||||
Array.from(this.hostElement.childNodes).includes(el)
|
|
||||||
)
|
|
||||||
blockElements.add(this.getBlockParent(el))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
blockElements.forEach(block => this.clearProofing(block))
|
blockElements.forEach(block => this.clearProofing(block))
|
||||||
}
|
}
|
||||||
@ -343,11 +361,7 @@ class BesDOMService extends BesService {
|
|||||||
proofAll() {
|
proofAll() {
|
||||||
this.onStartProofing()
|
this.onStartProofing()
|
||||||
this.proofNode(this.hostElement, this.abortController)
|
this.proofNode(this.hostElement, this.abortController)
|
||||||
if (this.proofingCount == 0) {
|
this.onProofingProgress(0)
|
||||||
// No text blocks were discovered for proofing. onProofingProgress() will not be called
|
|
||||||
// and we need to notify manually.
|
|
||||||
this.onEndProofing()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -365,15 +379,18 @@ class BesDOMService extends BesService {
|
|||||||
case Node.ELEMENT_NODE:
|
case Node.ELEMENT_NODE:
|
||||||
if (this.isBlockElement(node)) {
|
if (this.isBlockElement(node)) {
|
||||||
// Block elements are grammar-checked independently.
|
// 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 }]
|
return [{ text: `<${node.tagName}/>`, node: node, markup: true }]
|
||||||
|
}
|
||||||
|
|
||||||
let data = []
|
let data = []
|
||||||
for (const el2 of node.childNodes)
|
for (const el2 of node.childNodes)
|
||||||
data = data.concat(this.proofNode(el2, abortController))
|
data = data.concat(this.proofNode(el2, abortController))
|
||||||
if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) {
|
if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) {
|
||||||
// Block element contains some text.
|
// Block element contains some text.
|
||||||
this.onProofing()
|
|
||||||
const signal = abortController.signal
|
const signal = abortController.signal
|
||||||
fetch(
|
fetch(
|
||||||
new Request(besUrl + '/check', {
|
new Request(besUrl + '/check', {
|
||||||
@ -477,10 +494,12 @@ class BesDOMService extends BesService {
|
|||||||
* Tests if given block element has already been grammar-checked.
|
* Tests if given block element has already been grammar-checked.
|
||||||
*
|
*
|
||||||
* @param {Element} el DOM element to check
|
* @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) {
|
getProofing(el) {
|
||||||
return this.results?.find(result => result.element === el) != null
|
return this.results.find(result =>
|
||||||
|
BesDOMService.isSameParagraph(result.element, el)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -504,51 +523,9 @@ class BesDOMService extends BesService {
|
|||||||
*/
|
*/
|
||||||
clearProofing(el) {
|
clearProofing(el) {
|
||||||
this.clearMarkup(el)
|
this.clearMarkup(el)
|
||||||
this.results = this.results?.filter(result => result.element !== el)
|
this.results = this.results.filter(
|
||||||
}
|
result => !BesDOMService.isSameParagraph(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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -571,30 +548,27 @@ class BesDOMService extends BesService {
|
|||||||
* @param {Element} el DOM element we want to clean markup for
|
* @param {Element} el DOM element we want to clean markup for
|
||||||
*/
|
*/
|
||||||
clearMarkup(el) {
|
clearMarkup(el) {
|
||||||
let result = this.results?.find(result => result.element === el)
|
this.results
|
||||||
if (!result) return
|
.filter(result => BesDOMService.isSameParagraph(result.element, el))
|
||||||
result.matches.forEach(match => {
|
.forEach(result =>
|
||||||
if (match.highlights) {
|
result.matches.forEach(match => {
|
||||||
match.highlights.forEach(h => h.remove())
|
if (match.highlights) {
|
||||||
delete 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() {
|
static isSameParagraph(el1, el2) {
|
||||||
this.results.forEach(result => {
|
return el1 === el2
|
||||||
result.matches.forEach(match => {
|
|
||||||
if (match.highlights) {
|
|
||||||
match.highlights.forEach(h => h.remove())
|
|
||||||
delete match.highlights
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -714,23 +688,21 @@ class BesDOMService extends BesService {
|
|||||||
*/
|
*/
|
||||||
onClick(event) {
|
onClick(event) {
|
||||||
const source = event?.detail !== 1 ? event?.detail : event
|
const source = event?.detail !== 1 ? event?.detail : event
|
||||||
const target = this.getBlockParent(source.targetElement || source.target)
|
const el = this.getBlockParent(source.targetElement || source.target)
|
||||||
if (!target) return
|
if (!el) return
|
||||||
|
|
||||||
const matches = this.results?.find(
|
const matches = this.results.find(child =>
|
||||||
child => child.element === target
|
BesDOMService.isSameParagraph(child.element, el)
|
||||||
)?.matches
|
)?.matches
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const popup = document.querySelector('bes-popup-el')
|
|
||||||
for (let m of matches) {
|
for (let m of matches) {
|
||||||
if (m.rects) {
|
if (m.rects) {
|
||||||
for (let r of m.rects) {
|
for (let r of m.rects) {
|
||||||
if (
|
if (BesService.isPointInRect(source.clientX, source.clientY, r)) {
|
||||||
BesDOMService.isPointInRect(source.clientX, source.clientY, r)
|
const popup = document.querySelector('bes-popup-el')
|
||||||
) {
|
|
||||||
popup.changeMessage(m.match.message)
|
popup.changeMessage(m.match.message)
|
||||||
popup.appendReplacements(
|
popup.appendReplacements(
|
||||||
target,
|
el,
|
||||||
m,
|
m,
|
||||||
this,
|
this,
|
||||||
this.hostElement.contentEditable !== 'false'
|
this.hostElement.contentEditable !== 'false'
|
||||||
@ -745,18 +717,6 @@ class BesDOMService extends BesService {
|
|||||||
BesPopup.hide()
|
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.
|
* Replaces grammar checking match with a suggestion provided by grammar checking service.
|
||||||
*
|
*
|
||||||
@ -770,13 +730,7 @@ class BesDOMService extends BesService {
|
|||||||
this.clearProofing(el)
|
this.clearProofing(el)
|
||||||
match.range.deleteContents()
|
match.range.deleteContents()
|
||||||
match.range.insertNode(document.createTextNode(replacement))
|
match.range.insertNode(document.createTextNode(replacement))
|
||||||
this.onStartProofing()
|
this.proofAll()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -919,7 +873,7 @@ class BesPopup extends HTMLElement {
|
|||||||
/**
|
/**
|
||||||
* Adds a grammar mistake suggestion.
|
* 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 {*} match Grammar checking rule match
|
||||||
* @param {BesService} service Grammar checking service
|
* @param {BesService} service Grammar checking service
|
||||||
* @param {Boolean} allowReplacements Host element is mutable and grammar mistake may be replaced by suggestion
|
* @param {Boolean} allowReplacements Host element is mutable and grammar mistake may be replaced by suggestion
|
||||||
|
Loading…
x
Reference in New Issue
Block a user