Compare commits

..

No commits in common. "bf7b844e1cf34548b45a85a90c8bad847e70cd03" and "0ed93fb9b29664ca759567b97724ea3797bd7fd7" have entirely different histories.

3 changed files with 182 additions and 230 deletions

View File

@ -8,33 +8,9 @@
<link rel="stylesheet" href="styles.css" />
<script>const besUrl = 'http://localhost:225/api/v2';</script>
<script src="../service.js"></script>
<style>
.mock-resizable {
overflow-x: scroll;
position: relative;
display: inline-block;
border: 1px solid #ccc;
width: 300px;
height: 400px;
border-radius: 3px;
background-color: #f9f9f9;
}
.mock-content {
width: 1000px;
height: 600px;
background-color: #f9f9f9;
}
.mock-flex {
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div class="mock-flex">
<div class="flex">
<div class="mock-content">
<p>Element to delete</p>
<p>Element to delete</p>
@ -42,7 +18,7 @@
<p>Element to delete</p>
<p>Element to delete</p>
</div>
<div class="mock-resizable">
<div class="resizable">
<div class="my-block my-control bes-service" contenteditable="true">
<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>

View File

@ -73,9 +73,6 @@ class BesService {
})
besServices.push(this)
// Initial sync the scroll as hostElement may be scrolled by non-(0, 0) at the time of BesService registration.
this.onScroll()
}
/**
@ -201,7 +198,7 @@ class BesService {
*/
setMarkupStyle(style) {
this.markupStyle = style
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
}
/**
@ -357,19 +354,17 @@ class BesService {
}
/**
* Draws grammar mistake markup on canvas and populates collection of highlight rectangles.
* Creates grammar mistake markup in DOM and populates collection of highlight rectangles.
*
* @param {*} match Grammar checking rule match
*/
drawMistakeMarkup(match) {
addMistakeMarkup(match) {
const range = match.range
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
const scrollX = canvasPanelRect.left
const scrollY = canvasPanelRect.top
match.highlights = Array.from(range.getClientRects())
if (match.highlights.length === 0) return
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
for (let rect of match.highlights) {
rect.x -= canvasPanelRect.x
rect.y -= canvasPanelRect.y
}
const dpr = window.devicePixelRatio
this.ctx.lineWidth = 2 * dpr // Use 2 for clearer visibility
const ruleId = match.match.rule.id
@ -391,7 +386,7 @@ class BesService {
const scale = (markerY2 - markerY1) / 18
const x = match.highlights[0].left
const y = match.highlights[0].bottom
this.drawMissingComma(x, y, scale, '?')
this.drawMissingComma(x - scrollX, y - scrollY, scale, '?')
break
}
@ -418,20 +413,25 @@ class BesService {
// in another line, making a confusing UX.
const x = match.highlights[0].left
const y = match.highlights[0].bottom
this.drawMissingComma(x, y, scale)
this.drawMissingComma(x - scrollX, y - scrollY, scale)
} else if (/^\s+$/.test(toInsert)) {
const x = match.highlights[0].left
const y1 = match.highlights[0].bottom - 2 * scale
const y2 = match.highlights[0].top + 2 * scale
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
const x = match.highlights[0].left - 1 * scale
const y1 = match.highlights[0].bottom
const y2 = match.highlights[0].top
this.drawMissingText(
x,
y1,
y2,
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale,
replacement.substr(lengthDiff).trim()
)
@ -446,20 +446,25 @@ class BesService {
if (toInsert === ',') {
const x = match.highlights.at(-1).right
const y = match.highlights.at(-1).bottom
this.drawMissingComma(x, y, scale)
this.drawMissingComma(x - scrollX, y - scrollY, scale)
} else if (/^\s+$/.test(toInsert)) {
const x = match.highlights.at(-1).right
const y1 = match.highlights.at(-1).bottom - 2 * scale
const y2 = match.highlights.at(-1).top + 2 * scale
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
const x = match.highlights.at(-1).right + 1 * scale
const y1 = match.highlights.at(-1).bottom
const y2 = match.highlights.at(-1).top
this.drawMissingText(
x,
y1,
y2,
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale,
replacement.substr(-lengthDiff).trim()
)
@ -483,7 +488,12 @@ class BesService {
const x = (rect.left + rect.right) / 2
const y1 = rect.top
const y2 = rect.bottom
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
for (let rect of this.makeRange(
match.data,
@ -491,10 +501,10 @@ class BesService {
match.match.offset - lengthDiff
)?.getClientRects())
this.drawExcessiveText(
rect.left,
rect.bottom,
rect.right,
rect.top
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY
)
}
} else if (context.substr(0, replacement.length) === replacement) {
@ -513,7 +523,12 @@ class BesService {
const x = (rect.left + rect.right) / 2
const y1 = rect.top
const y2 = rect.bottom
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
for (let rect of this.makeRange(
match.data,
@ -521,10 +536,10 @@ class BesService {
match.match.offset + match.match.length
)?.getClientRects())
this.drawExcessiveText(
rect.left,
rect.bottom,
rect.right,
rect.top
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY
)
}
} else {
@ -544,20 +559,20 @@ class BesService {
for (let rect of match.highlights) {
if (first) {
this.drawWrongText(
rect.left,
rect.bottom,
rect.right,
rect.top,
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY,
scale,
replacement
)
first = false
} else {
this.drawExcessiveText(
rect.left,
rect.bottom,
rect.right,
rect.top
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY
)
}
}
@ -585,11 +600,22 @@ class BesService {
if (/^\s+$/.test(toInsert)) {
const y1 = rects[0].bottom - 2 * scale
const y2 = rects[0].top + 2 * scale
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
const y1 = rects[0].bottom
const y2 = rects[0].top
this.drawMissingText(x, y1, y2, scale, toInsert.trim())
this.drawMissingText(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale,
toInsert.trim()
)
}
} else if (lengthL + lengthR === replacement.length) {
// Something to remove
@ -603,14 +629,19 @@ class BesService {
const x = (rects[0].left + rects[0].right) / 2
const y1 = rects[0].top
const y2 = rects[0].bottom
this.drawWrongSpacing(x, y1, y2, scale)
this.drawWrongSpacing(
x - scrollX,
y1 - scrollY,
y2 - scrollY,
scale
)
} else {
for (let rect of rects)
this.drawExcessiveText(
rect.left,
rect.bottom,
rect.right,
rect.top
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY
)
}
} else {
@ -624,20 +655,20 @@ class BesService {
for (let rect of rects) {
if (first) {
this.drawWrongText(
rect.left,
rect.bottom,
rect.right,
rect.top,
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY,
scale,
toReplace
)
first = false
} else {
this.drawExcessiveText(
rect.left,
rect.bottom,
rect.right,
rect.top
rect.left - scrollX,
rect.bottom - scrollY,
rect.right - scrollX,
rect.top - scrollY
)
}
}
@ -653,22 +684,36 @@ class BesService {
const x2 = rect.right
const y = rect.bottom
const scale = (rect.bottom - rect.top) / 18
this.drawAttentionRequired(x1, x2, y, scale)
this.drawAttentionRequired(
x1 - scrollX,
x2 - scrollX,
y - scrollY,
scale
)
}
markerY1 = Math.min(...match.highlights.map(rect => rect.top))
markerY2 = Math.max(...match.highlights.map(rect => rect.bottom))
}
this.drawSideMarker(markerY1, markerY2)
this.drawSideMarker(markerY1 - scrollY, markerY2 - scrollY)
}
static commonPrefixLength(s1, s2) {
let i = 0
let len = Math.min(s1.length, s2.length)
while (i < len && s1[i] === s2[i]) i++
return i
}
static commonSuffixLength(s1, s2) {
let i = 0
let i1 = s1.length
let i2 = s2.length
while (0 < i1-- && 0 < i2-- && s1[i1] === s2[i2]) i++
return i
}
/**
* Draws the marker that helps visualize lines of text where grammar mistakes were detected
*
* @param {Number} y1 Marker top [px]
* @param {Number} y2 Marker bottom [px]
*/
drawSideMarker(y1, y2) {
const dpr = window.devicePixelRatio
const markerX = this.canvasPanel.width - 5 * dpr
@ -678,14 +723,6 @@ class BesService {
this.ctx.stroke()
}
/**
* Draws the missing comma sign
*
* @param {Number} x Sign center [px]
* @param {Number} y Sign bottom [px]
* @param {Number} scale Sign scale
* @param {String} comment Text to display above the marker
*/
drawMissingComma(x, y, scale, comment) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -702,14 +739,6 @@ class BesService {
}
}
/**
* Draws the wrong spacing sign. Control direction of chevrons by reversing y1 and y2.
*
* @param {Number} x Sign center [px]
* @param {Number} y1 Sign top/bottom [px]
* @param {Number} y2 Sign bottom/top [px]
* @param {Number} scale Sign scale
*/
drawWrongSpacing(x, y1, y2, scale) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -724,14 +753,6 @@ class BesService {
this.ctx.stroke()
}
/**
* Strikes out the excessive text
*
* @param {Number} x1 Strike line start X [px]
* @param {Number} y1 Strike line start Y [px]
* @param {Number} x2 Strike line end X [px]
* @param {Number} y2 Strike line end Y [px]
*/
drawExcessiveText(x1, y1, x2, y2) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -740,16 +761,6 @@ class BesService {
this.ctx.stroke()
}
/**
* Strikes out the text and draws the replacement text above
*
* @param {Number} x1 Strike line start X [px]
* @param {Number} y1 Strike line start Y [px]
* @param {Number} x2 Strike line end X [px]
* @param {Number} y2 Strike line end Y [px]
* @param {Number} scale Sign scale
* @param {String} text Text to display above
*/
drawWrongText(x1, y1, x2, y2, scale, text) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -774,15 +785,6 @@ class BesService {
)
}
/**
* Draws the sign some text is missing
*
* @param {Number} x Sign center [px]
* @param {Number} y1 Sign bottom [px]
* @param {Number} y2 Sign top [px]
* @param {Number} scale Sign scale
* @param {String} text Text to display above
*/
drawMissingText(x, y1, y2, scale, text) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -809,14 +811,6 @@ class BesService {
)
}
/**
* Draws zig-zag line
*
* @param {Number} x1 Sign left [px]
* @param {Number} x2 Sign right [px]
* @param {Number} y Sign baseline [px]
* @param {Number} scale Sign scale
*/
drawAttentionRequired(x1, x2, y, scale) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
@ -830,35 +824,6 @@ class BesService {
this.ctx.stroke()
}
/**
* Calculates common string prefix length
*
* @param {String} s1 First string
* @param {String} s2 Second string
* @returns Number of characters the beginnings of the strings are equal
*/
static commonPrefixLength(s1, s2) {
let i = 0
let len = Math.min(s1.length, s2.length)
while (i < len && s1[i] === s2[i]) i++
return i
}
/**
* Calculates common string suffix length
*
* @param {String} s1 First string
* @param {String} s2 Second string
* @returns Number of characters the endings of the strings are equal
*/
static commonSuffixLength(s1, s2) {
let i = 0
let i1 = s1.length
let i2 = s2.length
while (0 < i1-- && 0 < i2-- && s1[i1] === s2[i2]) i++
return i
}
/**
* Tests if given coordinate is inside of a rectangle.
*
@ -875,21 +840,21 @@ class BesService {
* Creates auxiliary DOM elements for text adornments.
*/
createCorrectionPanel() {
this.panelParent = document.createElement('div')
this.panelParent.classList.add('bes-correction-panel-parent')
this.correctionPanel = document.createElement('div')
this.correctionPanel.classList.add('bes-correction-panel')
this.scrollPanel = document.createElement('div')
this.scrollPanel.classList.add('bes-scroll-panel')
this.canvasPanel = document.createElement('canvas')
this.canvasPanel.classList.add('bes-canvas')
this.ctx = this.canvasPanel.getContext('2d')
this.ctx.scale(1, 1)
this.correctionPanel.appendChild(this.scrollPanel)
this.scrollPanel.appendChild(this.canvasPanel)
this.panelParent.appendChild(this.correctionPanel)
this.correctionPanel.appendChild(this.canvasPanel)
this.textElement.parentElement.insertBefore(
this.correctionPanel,
this.panelParent,
this.textElement
)
this.setCorrectionPanelSize()
@ -899,7 +864,7 @@ class BesService {
* Clears auxiliary DOM elements for text adornments.
*/
clearCorrectionPanel() {
this.correctionPanel.remove()
this.panelParent.remove()
}
/**
@ -907,11 +872,15 @@ class BesService {
*/
setCorrectionPanelSize() {
this.disableMutationObserver()
const styles = window.getComputedStyle(this.hostElement)
this.textFont = styles.fontFamily
// Resize canvas if needed.
const hostRect = this.hostElement.getBoundingClientRect()
// Sync margins one by one. Firefox is not happy when syncing all at once.
this.correctionPanel.style.marginLeft = styles.marginLeft
this.correctionPanel.style.marginTop = styles.marginTop
this.correctionPanel.style.marginRight = styles.marginRight
this.correctionPanel.style.marginBottom = styles.marginBottom
this.correctionPanel.style.boxSizing = styles.boxSizing
this.correctionPanel.style.scrollBehavior = styles.scrollBehavior
this.canvasPanel.style.width = `${this.hostElement.scrollWidth}px`
this.canvasPanel.style.height = `${this.hostElement.scrollHeight}px`
const dpr = window.devicePixelRatio
@ -924,16 +893,8 @@ class BesService {
) {
this.canvasPanel.width = newCanvasWidth
this.canvasPanel.height = newCanvasHeight
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
}
// Note: Firefox is not happy when syncing all margins at once.
this.scrollPanel.style.marginLeft = styles.marginLeft
this.scrollPanel.style.marginTop = styles.marginTop
this.scrollPanel.style.marginRight = styles.marginRight
this.scrollPanel.style.marginBottom = styles.marginBottom
this.scrollPanel.style.boxSizing = styles.boxSizing
this.scrollPanel.style.scrollBehavior = styles.scrollBehavior
if (this.isHostElementInline()) {
const totalWidth =
parseFloat(styles.paddingLeft) +
@ -941,14 +902,12 @@ class BesService {
parseFloat(styles.width) +
parseFloat(styles.marginRight) +
parseFloat(styles.paddingRight)
this.scrollPanel.style.width = `${totalWidth}px`
this.scrollPanel.style.height = styles.height
this.correctionPanel.style.width = `${totalWidth}px`
this.correctionPanel.style.height = styles.height
} else {
const hostRect = this.hostElement.getBoundingClientRect()
this.scrollPanel.style.width = `${hostRect.width}px`
this.scrollPanel.style.height = `${hostRect.height}px`
this.correctionPanel.style.width = `${hostRect.width}px`
this.correctionPanel.style.height = `${hostRect.height}px`
}
this.enableMutationObserver()
}
@ -970,12 +929,11 @@ class BesService {
}
/**
* Highlights given grammar mistake.
* Removes previously highlighted grammar mistake and highlights new one.
*
* @param {*} match Grammar checking rule match
*/
highlightMistake(match) {
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
match.highlights.forEach(rect => {
const el = document.createElement('div')
el.classList.add('bes-highlight-rect')
@ -984,8 +942,8 @@ class BesService {
? 'bes-highlight-spelling-rect'
: 'bes-highlight-grammar-rect'
)
el.style.left = `${rect.x + canvasPanelRect.x + window.scrollX}px`
el.style.top = `${rect.y + canvasPanelRect.y + window.scrollY}px`
el.style.left = `${rect.x + window.scrollX}px`
el.style.top = `${rect.y + window.scrollY}px`
el.style.width = `${rect.width}px`
el.style.height = `${rect.height}px`
document.body.appendChild(el)
@ -1017,12 +975,12 @@ class BesService {
}
/**
* Redraws all grammar mistake markup.
* Updates all grammar mistake markup positions.
*/
redrawAllMistakeMarkup() {
repositionAllMarkup() {
this.ctx.clearRect(0, 0, this.canvasPanel.width, this.canvasPanel.height)
this.results.forEach(result => {
result.matches.forEach(match => this.drawMistakeMarkup(match))
result.matches.forEach(match => this.addMistakeMarkup(match))
})
}
@ -1178,7 +1136,7 @@ class BesTreeService extends BesService {
),
match: match
}
this.drawMistakeMarkup(m)
this.addMistakeMarkup(m)
matches.push(m)
})
this.markProofed(node, matches)
@ -1263,7 +1221,7 @@ class BesTreeService extends BesService {
this.results = this.results.filter(
result => !BesTreeService.isSameParagraph(result.element, el)
)
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
}
/**
@ -1399,16 +1357,12 @@ class BesTreeService extends BesService {
const source = event?.detail !== 1 ? event?.detail : event
const el = this.getBlockParent(source.targetElement || source.target)
if (!el) return
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
let x = source.clientX - canvasPanelRect.x
let y = source.clientY - canvasPanelRect.y
const pointsInRect = []
for (let result of this.results) {
for (let m of result.matches) {
for (let rect of m.highlights) {
if (BesService.isPointInRect(x, y, rect)) {
if (BesService.isPointInRect(source.clientX, source.clientY, rect)) {
pointsInRect.push({ el, match: m })
break
}
}
}
@ -1498,7 +1452,7 @@ class BesDOMService extends BesTreeService {
*/
onInput() {
// Now that the text is done changing, we can correctly calculate markup position.
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
this.dismissPopup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.scheduleProofing(1000)
@ -1580,7 +1534,7 @@ class BesCKService extends BesTreeService {
// element, it will not be updated immediately.
setTimeout(() => {
// Now that the text is done changing, we can correctly calculate markup position.
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.scheduleProofing(1000)
@ -1747,7 +1701,7 @@ class BesQuillService extends BesTreeService {
this.clearProofing(domElement)
setTimeout(() => {
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
this.scheduleProofing(1000)
}, 0)
}
@ -1933,7 +1887,7 @@ class BesPlainTextService extends BesService {
),
match: match
}
this.drawMistakeMarkup(m)
this.addMistakeMarkup(m)
matches.push(m)
})
this.markProofed(paragraphRange, matches)
@ -2024,7 +1978,7 @@ class BesPlainTextService extends BesService {
this.results = this.results.filter(
result => !BesPlainTextService.isSameParagraph(result.range, range)
)
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
}
/**
@ -2067,16 +2021,12 @@ class BesPlainTextService extends BesService {
const source = event?.detail !== 1 ? event?.detail : event
const el = source.targetElement || source.target || this.hostElement
if (!el) return
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
let x = source.clientX - canvasPanelRect.x
let y = source.clientY - canvasPanelRect.y
const pointsInRect = []
for (let result of this.results) {
for (let m of result.matches) {
for (let rect of m.highlights) {
if (BesService.isPointInRect(x, y, rect)) {
if (BesService.isPointInRect(source.clientX, source.clientY, rect)) {
pointsInRect.push({ el: result.range, match: m })
break
}
}
}
@ -2196,7 +2146,7 @@ class BesDOMPlainTextService extends BesPlainTextService {
delete this.textBeforeChange
// Now that the text is done changing, we can correctly calculate markup position.
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
this.dismissPopup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.scheduleProofing(1000)
@ -2274,6 +2224,9 @@ class BesTAService extends BesPlainTextService {
*/
constructor(hostElement, eventSink) {
super(hostElement, BesTAService.createTextElement(hostElement), eventSink)
this.textElement.replaceChildren(
document.createTextNode(this.hostElement.value)
)
}
/**
@ -2313,7 +2266,6 @@ class BesTAService extends BesPlainTextService {
static createTextElement(hostElement) {
const textElement = document.createElement('div')
textElement.classList.add('bes-text-panel')
textElement.replaceChildren(document.createTextNode(hostElement.value))
BesTAService.setTextElementSize(hostElement, textElement)
hostElement.parentNode.insertBefore(textElement, hostElement)
return textElement
@ -2463,7 +2415,7 @@ class BesTAService extends BesPlainTextService {
})
// Now that the text is done changing, we can correctly calculate markup position.
this.redrawAllMistakeMarkup()
this.repositionAllMarkup()
// Defer grammar-checking to reduce stress on grammar-checking server.
this.scheduleProofing(1000)

View File

@ -22,8 +22,14 @@
background: rgb(255, 115, 0);
}
.bes-canvas {
position: relative;
z-index: 3;
cursor: text;
}
/* Styles required to ensure full functionality and optimal user experience. */
.bes-correction-panel {
.bes-correction-panel-parent {
position: relative;
overflow: visible;
float: left;
@ -34,7 +40,7 @@
z-index: 1;
}
.bes-scroll-panel {
.bes-correction-panel {
position: relative;
overflow: hidden;
border-color: transparent;
@ -42,12 +48,6 @@
pointer-events: none;
}
.bes-canvas {
position: relative;
z-index: 3;
cursor: text;
}
.bes-text-panel {
position: absolute;
overflow: hidden;
@ -56,3 +56,27 @@
border-color: transparent;
background: none;
}
/* TODO: Styles below should be removed after testing period is over */
.resizable {
overflow-x: scroll;
position: relative;
display: inline-block;
border: 1px solid #ccc;
width: 300px;
height: 400px;
border-radius: 3px;
background-color: #f9f9f9;
}
.mock-content {
width: 1000px;
height: 600px;
background-color: #f9f9f9;
}
.flex {
display: flex;
justify-content: center;
align-items: center;
}