const besUrl = 'http://localhost:225/api/v2/check' 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: [] } besEditors[edit.id] = editor besProof(editor, edit) edit.addEventListener( 'beforeinput', e => besHandleBeforeInput(editor, e), false ) //edit.addEventListener('click', e => besHandleClick(editor, e)) }) } window.onresize = () => { Object.keys(besEditors).forEach(key => { let editor = besEditors[key] editor.children.forEach(child => { besClearAllMistakes(editor, child?.elements) child.matches.forEach(match => { const clientRect = besAddMistake(match.range, match.message) 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, message: match.message }) }) 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: '', markup: true }) return data } default: return [{ text: '', 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.dataset.info = match.message 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.message) 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 ) }