...
*
- * Note: Chrome and Edge only, as Firefox reverts contenteditable=
- * "plaintext-only" to "false". The grammar mistakes will be highlighted
- * nevertheless, but consider using BesDOMService on Firefox instead.
- *
- *************************************************************************/
+ *************************************************************************************************/
class BesPlainTextService extends BesService {
/**
* Constructs class.
*
- * @param {Element} hostElement The element in DOM tree we are providing grammar-checking service for
+ * @param {Element} hostElement The element in DOM tree we are providing grammar-checking service
+ * for
+ * @param {Element} textElement The element in DOM tree that hosts coordinate-measurable clone of
+ * the text to proof. Same as hostElement for
, separate for
+ *
:1???
- range.setStart(nodesB[nodeIdxB].node, j - nodesB[nodeIdxB].start)
- range.setEndAfter(nodesB[nodesB.length - 1].node)
- ranges.push(range)
- break
- }
- if (textA.charAt(i) != textB.charAt(j)) {
- let range = document.createRange()
- range.setStart(nodesB[nodeIdxB].node, j - nodesB[nodeIdxB].start)
- let a = textA.indexOf(textB.substr(j, 3), i)
- if (a < 0) a = textA.length
- let b = textB.indexOf(textA.substr(i, 3), j)
- if (b < 0) b = textB.length
- if (3 * (a - i) <= b - j) {
- // Suppose some text was deleted.
- i = a
- range.setEnd(nodesB[nodeIdxB].node, j - nodesB[nodeIdxB].start)
- } else if (3 * (b - j) <= a - i) {
- // Suppose some text was inserted.
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end < b)
- nodeIdxB++
- range.setEnd(nodesB[nodeIdxB].node, (j = b) - nodesB[nodeIdxB].start)
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end <= b)
- nodeIdxB++
- } else {
- // Suppose some text was replaced.
- i = a
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end < b)
- nodeIdxB++
- range.setEnd(nodesB[nodeIdxB].node, (j = b) - nodesB[nodeIdxB].start)
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end <= b)
- nodeIdxB++
- }
- ranges.push(range)
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end <= j) nodeIdxB++
- continue
- }
- i++
- while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end <= j) nodeIdxB++
- j++
- }
- return ranges
- }
-
/**
* Grammar-(re)checks the host element.
*/
@@ -1267,7 +1155,7 @@ class BesPlainTextService extends BesService {
let nodes = []
let text = ''
for (
- let node = this.hostElement.childNodes[0];
+ let node = this.textElement.childNodes[0];
node;
node = node.nextSibling
) {
@@ -1285,7 +1173,8 @@ class BesPlainTextService extends BesService {
* Tests if given paragraph has already been grammar-checked.
*
* @param {Range} range Paragraph range
- * @returns {*} Result of grammar check if the element has already been grammar-checked; undefined otherwise.
+ * @returns {*} Result of grammar check if the element has already been grammar-checked;
+ * undefined otherwise.
*/
getProofing(range) {
return this.results.find(result =>
@@ -1370,7 +1259,9 @@ class BesPlainTextService extends BesService {
*
* Displays or hides grammar mistake popup.
*
- * @param {PointerEvent} event The event produced by a pointer such as the geometry of the contact point, the device type that generated the event, the amount of pressure that was applied on the contact surface, etc.
+ * @param {PointerEvent} event The event produced by a pointer such as the geometry of the
+ * contact point, the device type that generated the event, the
+ * amount of pressure that was applied on the contact surface, etc.
*/
onClick(event) {
const source = event?.detail !== 1 ? event?.detail : event
@@ -1395,16 +1286,416 @@ class BesPlainTextService extends BesService {
}
BesPopup.hide()
}
+
+ /**
+ * Simple string compare.
+ *
+ * For performance reasons, this method compares only string beginnings and endings. Maximum one
+ * difference is reported.
+ *
+ * @param {String} x First string
+ * @param {String} y Second string
+ * @returns {Array} Array of string differences
+ */
+ static diffStrings(x, y) {
+ let m = x.length,
+ n = y.length
+ for (let i = 0; ; ++i) {
+ if (i >= m && i >= n) return []
+ if (i >= m) return [{ type: '+', start: i, length: n - i }]
+ if (i >= n) return [{ type: '-', start: i, end: m }]
+ if (x.charAt(i) !== y.charAt(i)) {
+ for (;;) {
+ if (m <= i) return [{ type: '+', start: i, length: n - i }]
+ if (n <= i) return [{ type: '-', start: i, end: m }]
+ --m, --n
+ if (x.charAt(m) !== y.charAt(n))
+ return [{ type: '*', start: i, end: m + 1, length: n - i + 1 }]
+ }
+ }
+ }
+ }
}
-/*************************************************************************
+/**************************************************************************************************
+ *
+ * Plain-text grammar-checking service
+ *
+ * This class provides grammar-checking functionality to contenteditable="plaintext-only" HTML
+ * controls.
+ *
+ * Note: Chrome and Edge only, as Firefox reverts contenteditable="plaintext-only" to "false". The
+ * grammar mistakes will be highlighted nevertheless, but consider using BesDOMService on Firefox
+ * instead.
+ *
+ *************************************************************************************************/
+class BesDOMPlainTextService extends BesPlainTextService {
+ /**
+ * Constructs class.
+ *
+ * @param {Element} hostElement The element in DOM tree we are providing grammar-checking service
+ * for
+ */
+ constructor(hostElement) {
+ super(hostElement, hostElement)
+ }
+
+ /**
+ * Registers grammar checking service.
+ *
+ * @param {Element} hostElement DOM element to register grammar checking service for
+ * @returns {BesDOMPlainTextService} Grammar checking service instance
+ */
+ static register(hostElement) {
+ let service = new BesDOMPlainTextService(hostElement)
+ service.proofAll()
+ return service
+ }
+
+ /**
+ * Called to report the text is about to change
+ *
+ * Marks section of the text that is about to change as not-yet-grammar-checked.
+ *
+ * @param {InputEvent} event The event notifying the user of editable content changes
+ */
+ onBeforeInput(event) {
+ if (this.timer) clearTimeout(this.timer)
+ if (this.abortController) this.abortController.abort()
+
+ // Firefox does not support contenteditable="plaintext-only" at all, Chrome/Edge's InputEvent.
+ // getTargetRanges() return a useless empty array for contenteditable="plaintext-only". This
+ // makes tracking location of changes a pain. We need to save the text on beforeinput and
+ // compare it to the text on input event to do the this.clearProofing().
+ let { text } = this.getTextFromNodes()
+ this.textBeforeChange = text
+ // Continues in onInput...
+ }
+
+ /**
+ * Called to report the text has changed
+ *
+ * @param {InputEvent} event The event notifying the user of editable content changes
+ */
+ onInput(event) {
+ // ...Continued from onBeforeInput: Remove markup of all paragraphs that changed.
+ // Use the offsets before change, as paragraph changes have not been updated yet.
+ let paragraphRanges = new Set()
+ this.getTargetRanges().forEach(range => {
+ this.results.forEach(result => {
+ if (BesPlainTextService.isOverlappingParagraph(result.range, range))
+ paragraphRanges.add(result.range)
+ })
+ })
+ paragraphRanges.forEach(range => this.clearProofing(range))
+ delete this.textBeforeChange
+
+ // Now that the text is done changing, we can correctly calculate markup position.
+ this.repositionAllMarkup()
+
+ // Defer grammar-checking to reduce stress on grammar-checking server.
+ this.timer = setTimeout(() => {
+ this.proofAll()
+ delete this.timer
+ }, 1000)
+ }
+
+ /**
+ * Returns an array of ranges that will be affected by a change to the DOM.
+ *
+ * This method attempts to fix the Chrome/Edge shortcoming in InputEvent.getTargetRanges()
+ * failing to return meaningful range array on beforeinput event.
+ */
+ getTargetRanges() {
+ let textA = this.textBeforeChange
+ let { text, nodes } = this.getTextFromNodes()
+ let textB = text
+ let nodesB = nodes
+ let diff = BesPlainTextService.diffStrings(textA, textB)
+ let ranges = []
+ for (
+ let i = 0, j = 0, nodeIdxB = 0, diffIdx = 0;
+ diffIdx < diff.length;
+ ++diffIdx
+ ) {
+ let length = diff[diffIdx].start - i
+ i = diff[diffIdx].start
+ j += length
+ while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end < j) nodeIdxB++
+ let range = document.createRange()
+ range.setStart(nodesB[nodeIdxB].node, j - nodesB[nodeIdxB].start)
+ switch (diff[diffIdx].type) {
+ case '-': {
+ // Suppose some text was deleted.
+ i = diff[diffIdx].end
+ range.setEnd(nodesB[nodeIdxB].node, j - nodesB[nodeIdxB].start)
+ break
+ }
+ case '+': {
+ // Suppose some text was inserted.
+ let b = j + diff[diffIdx].length
+ while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end < b)
+ nodeIdxB++
+ range.setEnd(nodesB[nodeIdxB].node, (j = b) - nodesB[nodeIdxB].start)
+ break
+ }
+ case 'x': {
+ // Suppose some text was replaced.
+ i = diff[diffIdx].end
+ let b = j + diff[diffIdx].length
+ while (nodeIdxB < nodesB.length && nodesB[nodeIdxB].end < b)
+ nodeIdxB++
+ range.setEnd(nodesB[nodeIdxB].node, (j = b) - nodesB[nodeIdxB].start)
+ break
+ }
+ }
+ ranges.push(range)
+ }
+ return ranges
+ }
+}
+
+/**************************************************************************************************
+ *
+ * Plain-text grammar-checking service
+ *
+ * This class provides grammar-checking functionality to