diff --git a/online-editor.js b/online-editor.js
index a0a744f..4323265 100644
--- a/online-editor.js
+++ b/online-editor.js
@@ -1,23 +1,304 @@
const besUrl = 'http://localhost:225/api/v2/check'
-let besEditors = {} // Collection of all editors on page
+class BesEditor {
+ constructor(edit) {
+ this.el = edit
+ this.timer = null
+ this.children = []
+
+ this.proof(edit)
+ edit.addEventListener(
+ 'beforeinput',
+ e => this.handleBeforeInput(e),
+ false
+ )
+ edit.addEventListener('click', e => this.handleClick(e))
+ }
+
+ // Recursively grammar-proofs one node.
+ async proof(el) {
+ switch (el.nodeType) {
+ case Node.TEXT_NODE:
+ return [{ text: el.textContent, el: el, markup: false }]
+
+ case Node.ELEMENT_NODE:
+ if (BesEditor.isBlockElement(el)) {
+ // Block elements are grammar-proofed independently.
+ if (this.isProofed(el)) {
+ return [{ text: '<' + el.tagName + '/>', el: el, markup: true }]
+ }
+ this.clearAllMistakes(el)
+ let data = []
+ for (const el2 of el.childNodes) {
+ data = data.concat(await this.proof(el2))
+ }
+ if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) {
+ const requestData = {
+ format: 'plain',
+ data: JSON.stringify({
+ annotation: data.map(x =>
+ x.markup ? { markup: x.text } : { text: x.text }
+ )
+ }),
+ language: el.lang ? el.lang : 'sl',
+ level: 'picky'
+ }
+ const request = new Request(besUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams(requestData)
+ })
+ fetch(request)
+ .then(response => {
+ if (!response.ok) {
+ // TODO: Make connectivity and BesStr issues non-fatal. But show an error sign somewhere in the UI.
+ throw new Error('Backend server response was not OK')
+ }
+ return response.json()
+ })
+ .then(responseData => {
+ let matches = []
+ responseData.matches.forEach(match => {
+ let range = document.createRange()
+
+ // Locate start of the grammar mistake.
+ for (
+ let idx = 0, startingOffset = 0;
+ ;
+ startingOffset += data[idx++].text.length
+ ) {
+ if (
+ !data[idx].markup &&
+ /*startingOffset <= match.offset &&*/ match.offset <
+ startingOffset + data[idx].text.length
+ ) {
+ range.setStart(data[idx].el, match.offset - startingOffset)
+ break
+ }
+ }
+
+ // Locate end of the grammar mistake.
+ let endOffset = match.offset + match.length
+ for (
+ let idx = 0, startingOffset = 0;
+ ;
+ startingOffset += data[idx++].text.length
+ ) {
+ if (
+ !data[idx].markup &&
+ /*startingOffset <= endOffset &&*/ endOffset <=
+ startingOffset + data[idx].text.length
+ ) {
+ range.setEnd(data[idx].el, endOffset - startingOffset)
+ break
+ }
+ }
+
+ const clientRect = BesEditor.addMistake(range, match)
+ matches.push({
+ range: range,
+ rects: clientRect,
+ match: match
+ })
+ })
+
+ this.markProofed(el, matches)
+ })
+ .catch(error => {
+ // TODO: Make parsing issues non-fatal. But show an error sign somewhere in the UI.
+ throw new Error(
+ 'Parsing backend server response failed: ' + error
+ )
+ })
+ }
+ return [{ text: '<' + el.tagName + '/>', el: el, markup: true }]
+ } else {
+ // Surround inline element with dummy ....
+ let data = [{ text: '<' + el.tagName + '>', el: el, markup: true }]
+ for (const el2 of el.childNodes) {
+ data = data.concat(await this.proof(el2))
+ }
+ data.splice(data.length, 0, {
+ text: '' + el.tagName + '>',
+ markup: true
+ })
+ return data
+ }
+
+ default:
+ return [{ text: '' + el.nodeType + '>', el: el, markup: true }]
+ }
+ }
+
+ // Marks section of text that is about to change as not-yet-grammar-proofed.
+ handleBeforeInput(event) {
+ if (this.timer) clearTimeout(this.timer)
+ let editor = this
+ this.timer = setTimeout(function () {
+ editor.proof(editor.el)
+ }, 1000)
+
+ // No need to invalidate elements after range.startContainer since they will
+ // get either deleted or replaced.
+ event
+ .getTargetRanges()
+ .forEach(range =>
+ this.clearProofed(this.getBlockParent(range.startContainer))
+ )
+ }
+
+ // Test if given block element has already been grammar-proofed.
+ isProofed(el) {
+ let filteredChildren = this.children.filter(child => child.elements === el)
+ return filteredChildren[0]?.isProofed
+ }
+
+ // Mark given block element as grammar-proofed.
+ markProofed(el, matches) {
+ let newChild = {
+ isProofed: true,
+ elements: el,
+ matches: matches
+ }
+
+ this.children = this.children.map(child =>
+ child.elements === newChild.elements ? newChild : child
+ )
+ if (!this.children.some(child => child.elements === newChild.elements)) {
+ this.children.push(newChild)
+ }
+ }
+
+ // Mark given block element as not grammar-proofed.
+ clearProofed(el) {
+ let filteredChildren = this.children.filter(child => child.elements === el)
+ if (filteredChildren.length) filteredChildren[0].isProofed = false
+ }
+
+ // Remove all grammar mistakes markup for given block element.
+ clearAllMistakes(el) {
+ let filteredChildren = this.children.filter(child => child.elements === el)
+ if (!filteredChildren.length) return
+
+ const correctionPanel = document.getElementById('correction-panel')
+ filteredChildren[0].matches.forEach(match => {
+ for (const rect of match.rects) {
+ for (let child of correctionPanel.children) {
+ let childRect = child.getBoundingClientRect()
+ const isWithinRect =
+ childRect.left >= rect.left &&
+ childRect.right <= rect.right &&
+ childRect.top >= rect.top &&
+ childRect.bottom <= rect.bottom + 20
+ if (isWithinRect) {
+ child.remove()
+ }
+ }
+ }
+ })
+ }
+
+ // Adds grammar mistake markup
+ static addMistake(range, match) {
+ const correctionPanel = document.getElementById('correction-panel')
+ const clientRects = range.getClientRects()
+ 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}px`
+ highlight.style.top = `${rect.top}px`
+ highlight.style.width = `${rect.width}px`
+ highlight.style.height = `${rect.height}px`
+ correctionPanel.appendChild(highlight)
+ }
+ return clientRects
+ }
+
+ // Tests if given element is block element.
+ static isBlockElement(el) {
+ const defaultView = document.defaultView
+ switch (
+ defaultView
+ .getComputedStyle(el, null)
+ .getPropertyValue('display')
+ .toLowerCase()
+ ) {
+ case 'inline':
+ case 'inline-block':
+ return false
+ default:
+ return true
+ }
+ }
+
+ // Returns first block parent element
+ getBlockParent(el) {
+ for (; el && el !== this.el; el = el.parentNode) {
+ if (el.nodeType === Node.ELEMENT_NODE && BesEditor.isBlockElement(el)) return el
+ }
+ return el
+ }
+
+ handleClick(e) {
+ const targetEl = e.target
+ const popup = document.querySelector('bes-popup-el')
+ if (targetEl.tagName === 'DIV') {
+ const divIndex = this.children.findIndex(
+ child => child.elements === targetEl
+ )
+ const matches = this.children[divIndex]?.matches
+ if (!matches) {
+ popup.hide()
+ return
+ }
+ if (BesEditor.renderPopup(matches, popup, e.clientX, e.clientY)) return
+ } else {
+ popup.hide()
+ }
+ }
+
+ static renderPopup(matches, popup, clientX, clientY) {
+ for (let m of matches) {
+ if (m.rects) {
+ for (let r of m.rects) {
+ if (BesEditor.isPointInRect(clientX, clientY, r)) {
+ popup.changeText(m.match.message)
+ m.match.replacements.forEach(replacement => {
+ popup.appendReplacements(
+ replacement.value,
+ m.match.offset,
+ m.match.length
+ )
+ })
+ popup.show(clientX, clientY)
+ return true
+ }
+ }
+ } else {
+ popup.hide()
+ }
+ }
+ return false
+ }
+
+ static isPointInRect(x, y, rect) {
+ return (
+ x >= rect.x &&
+ x < rect.x + rect.width &&
+ y >= rect.y &&
+ y < rect.y + rect.height
+ )
+ }
+}
+
+let besEditors = [] // Collection of all editors on page
window.onload = () => {
// Search and prepare all our editors found in the document.
document.querySelectorAll('.bes-online-editor').forEach(edit => {
- let editor = {
- el: edit,
- timer: null,
- children: []
- }
+ let editor = new BesEditor(edit)
besEditors[edit.id] = editor
- besProof(editor, edit)
- edit.addEventListener(
- 'beforeinput',
- e => besHandleBeforeInput(editor, e),
- false
- )
- edit.addEventListener('click', e => besHandleClick(editor, e))
})
}
@@ -25,286 +306,11 @@ window.onresize = () => {
Object.keys(besEditors).forEach(key => {
let editor = besEditors[key]
editor.children.forEach(child => {
- besClearAllMistakes(editor, child?.elements)
+ editor.clearAllMistakes(child?.elements)
child.matches.forEach(match => {
- const clientRect = besAddMistake(match.range, match)
+ const clientRect = BesEditor.addMistake(match.range, match)
match.rects = clientRect
})
})
})
}
-
-// Recursively grammar-proofs one node.
-async function besProof(editor, el) {
- switch (el.nodeType) {
- case Node.TEXT_NODE:
- return [{ text: el.textContent, el: el, markup: false }]
-
- case Node.ELEMENT_NODE:
- if (besIsBlockElement(el)) {
- // Block elements are grammar-proofed independently.
- if (besIsProofed(editor, el)) {
- return [{ text: '<' + el.tagName + '/>', el: el, markup: true }]
- }
- besClearAllMistakes(editor, el)
- let data = []
- for (const el2 of el.childNodes) {
- data = data.concat(await besProof(editor, el2))
- }
- if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) {
- const requestData = {
- format: 'plain',
- data: JSON.stringify({
- annotation: data.map(x =>
- x.markup ? { markup: x.text } : { text: x.text }
- )
- }),
- language: el.lang ? el.lang : 'sl',
- level: 'picky'
- }
- const request = new Request(besUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams(requestData)
- })
- fetch(request)
- .then(response => {
- if (!response.ok) {
- // TODO: Make connectivity and BesStr issues non-fatal. But show an error sign somewhere in the UI.
- throw new Error('Backend server response was not OK')
- }
- return response.json()
- })
- .then(responseData => {
- let matches = []
- responseData.matches.forEach(match => {
- let range = document.createRange()
-
- // Locate start of the grammar mistake.
- for (
- let idx = 0, startingOffset = 0;
- ;
- startingOffset += data[idx++].text.length
- ) {
- if (
- !data[idx].markup &&
- /*startingOffset <= match.offset &&*/ match.offset <
- startingOffset + data[idx].text.length
- ) {
- range.setStart(data[idx].el, match.offset - startingOffset)
- break
- }
- }
-
- // Locate end of the grammar mistake.
- let endOffset = match.offset + match.length
- for (
- let idx = 0, startingOffset = 0;
- ;
- startingOffset += data[idx++].text.length
- ) {
- if (
- !data[idx].markup &&
- /*startingOffset <= endOffset &&*/ endOffset <=
- startingOffset + data[idx].text.length
- ) {
- range.setEnd(data[idx].el, endOffset - startingOffset)
- break
- }
- }
-
- const clientRect = besAddMistake(range, match)
- matches.push({
- range: range,
- rects: clientRect,
- match: match
- })
- })
-
- besMarkProofed(editor, el, matches)
- })
- .catch(error => {
- // TODO: Make parsing issues non-fatal. But show an error sign somewhere in the UI.
- throw new Error(
- 'Parsing backend server response failed: ' + error
- )
- })
- }
- return [{ text: '<' + el.tagName + '/>', el: el, markup: true }]
- } else {
- // Surround inline element with dummy ....
- let data = [{ text: '<' + el.tagName + '>', el: el, markup: true }]
- for (const el2 of el.childNodes) {
- data = data.concat(await besProof(editor, el2))
- }
- data.splice(data.length, 0, {
- text: '' + el.tagName + '>',
- markup: true
- })
- return data
- }
-
- default:
- return [{ text: '' + el.nodeType + '>', el: el, markup: true }]
- }
-}
-
-// Marks section of text that is about to change as not-yet-grammar-proofed.
-function besHandleBeforeInput(editor, event) {
- if (editor.timer) clearTimeout(editor.timer)
- editor.timer = setTimeout(function () {
- besProof(editor, editor.el)
- }, 1000)
-
- // No need to invalidate elements after range.startContainer since they will
- // get either deleted or replaced.
- event
- .getTargetRanges()
- .forEach(range =>
- besClearProofed(editor, besGetBlockParent(editor, range.startContainer))
- )
-}
-
-// Test if given block element has already been grammar-proofed.
-function besIsProofed(editor, el) {
- let filteredChildren = editor?.children.filter(child => child.elements === el)
- return filteredChildren[0]?.isProofed
-}
-
-// Mark given block element as grammar-proofed.
-function besMarkProofed(editor, el, matches) {
- let newChild = {
- isProofed: true,
- elements: el,
- matches: matches
- }
-
- editor.children = editor.children.map(child =>
- child.elements === newChild.elements ? newChild : child
- )
- if (!editor.children.some(child => child.elements === newChild.elements)) {
- editor.children.push(newChild)
- }
-}
-
-// Mark given block element as not grammar-proofed.
-function besClearProofed(editor, el) {
- let filteredChildren = editor.children.filter(child => child.elements === el)
- if (filteredChildren.length) filteredChildren[0].isProofed = false
-}
-
-// Remove all grammar mistakes markup for given block element.
-function besClearAllMistakes(editor, el) {
- let filteredChildren = editor?.children.filter(child => child.elements === el)
- if (!filteredChildren.length) return
-
- const correctionPanel = document.getElementById('correction-panel')
- filteredChildren[0].matches.forEach(match => {
- for (const rect of match.rects) {
- for (let child of correctionPanel.children) {
- let childRect = child.getBoundingClientRect()
- const isWithinRect =
- childRect.left >= rect.left &&
- childRect.right <= rect.right &&
- childRect.top >= rect.top &&
- childRect.bottom <= rect.bottom + 20
- if (isWithinRect) {
- child.remove()
- }
- }
- }
- })
-}
-
-// Adds grammar mistake markup
-function besAddMistake(range, match) {
- const correctionPanel = document.getElementById('correction-panel')
- const clientRects = range.getClientRects()
- 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}px`
- highlight.style.top = `${rect.top}px`
- highlight.style.width = `${rect.width}px`
- highlight.style.height = `${rect.height}px`
- correctionPanel.appendChild(highlight)
- }
- return clientRects
-}
-
-// Tests if given element is block element.
-function besIsBlockElement(el) {
- const defaultView = document.defaultView
- switch (
- defaultView
- .getComputedStyle(el, null)
- .getPropertyValue('display')
- .toLowerCase()
- ) {
- case 'inline':
- case 'inline-block':
- return false
- default:
- return true
- }
-}
-
-// Returns first block parent element
-function besGetBlockParent(editor, el) {
- for (; el && el !== editor.el; el = el.parentNode) {
- if (el.nodeType === Node.ELEMENT_NODE && besIsBlockElement(el)) return el
- }
- return el
-}
-
-function besHandleClick(editor, e) {
- const targetEl = e.target
- const popup = document.querySelector('bes-popup-el')
- if (targetEl.tagName === 'DIV') {
- const divIndex = editor.children.findIndex(
- child => child.elements === targetEl
- )
- const matches = editor.children[divIndex]?.matches
- if (!matches) {
- popup.hide()
- return
- }
- if (besRenderPopup(matches, popup, e.clientX, e.clientY)) return
- } else {
- popup.hide()
- }
-}
-
-function besRenderPopup(matches, popup, clientX, clientY) {
- for (let m of matches) {
- if (m.rects) {
- for (let r of m.rects) {
- if (besIsPointInRect(clientX, clientY, r)) {
- popup.changeText(m.match.message)
- m.match.replacements.forEach(replacement => {
- popup.appendReplacements(
- replacement.value,
- m.match.offset,
- m.match.length
- )
- })
- popup.show(clientX, clientY)
- return true
- }
- }
- } else {
- popup.hide()
- }
- }
- return false
-}
-
-function besIsPointInRect(x, y, rect) {
- return (
- x >= rect.x &&
- x < rect.x + rect.width &&
- y >= rect.y &&
- y < rect.y + rect.height
- )
-}