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('.online-editor').forEach(edit => { let editor = { ignoreInput: false, timer: null } besEditors[edit.id] = editor besCheckText(edit) //edit.addEventListener('beforeinput', e => besBeforeInput(edit.id, e), false) edit.addEventListener('click', e => { besHandleClick(e) }) }) } async function besCheckText(el) { switch (el.nodeType) { case Node.TEXT_NODE: return [{ text: el.textContent, el: el }] case Node.ELEMENT_NODE: let data = [] for (const el2 of el.childNodes) { data = data.concat(await besCheckText(el2)) } let defaultView = document.defaultView; switch (defaultView.getComputedStyle(el, null).getPropertyValue('display')) { case 'inline': case 'inline-block': data.splice(0, 0, { markup: '<'+el.tagName+'>', el: el }) data.splice(data.length, 0, { markup: '' }) return data; default: let regAllWhitespace = /^\s*$/ if (data.some(x => 'text' in x && !regAllWhitespace.test(x.text))) { const requestData = { format: 'plain', data: JSON.stringify({annotation: data}), language: '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. throw new Error('Backend server response was not OK') } return response.json() }) .then(responseData => { // responseData.matches.sort((a, b) => a.offset < b.offset ? -1 : a.offset > b.offset ? +1 : 0) responseData.matches.sort((a, b) => a.offset < b.offset ? +1 : a.offset > b.offset ? -1 : 0); responseData.matches.forEach(match => { let range = document.createRange() // Locate start of the grammar mistake. for (let idx = 0, startingOffset = 0; ; ) { if ('text' in data[idx]) { if (startingOffset <= match.offset && match.offset < startingOffset + data[idx].text.length) { range.setStart(data[idx].el, match.offset - startingOffset) break } startingOffset += data[idx++].text.length } else startingOffset += data[idx++].markup.length } // Locate end of the grammar mistake. let endOffset = match.offset + match.length for (let idx = 0, startingOffset = 0; ; ) { if ('text' in data[idx]) { if (startingOffset <= endOffset && endOffset <= startingOffset + data[idx].text.length) { range.setEnd(data[idx].el, endOffset - startingOffset) break } startingOffset += data[idx++].text.length } else startingOffset += data[idx++].markup.length } const span = document.createElement('span') span.classList.add('typo-mistake') // Dirty hack, copied from: https://stackoverflow.com/questions/67634286/invalidstateerror-failed-to-execute-surroundcontents-on-range-the-range-ha span.appendChild(range.extractContents()); range.insertNode(span); // This is a better way to apply span elements, // but it doesnt work when the range has partially selected a non-Text node. // range.surroundContents(span) }) }) .catch(error => { // TODO: Make parsing issues non-fatal. throw new Error('Request to backend server failed: ' + error) }) } return [{ markup: '<'+el.tagName+'/>', el: el }] } default: return [{ markup: '', el: el }] } } function besBeforeInput(editorId, event) { let editor = besEditors[editorId] if (editor.ignoreInput) return if (editor.timer) clearTimeout(editor.timer) editor.timer = setTimeout(function(){ besCheckText(editorId) }, 1000) let edit = document.getElementById(editorId) event.getTargetRanges().forEach(range => { if (range.startContainer === edit) return for (var el = range.startContainer; el ; el = el.nextSibling) { for (var el2 = el; el2 && el2 !== edit; el2 = el2.parentElement) { if (!(el2 instanceof HTMLElement) || el2.tagName !== 'DIV') continue el2.removeAttribute('data-info') } if (el === range.endContainer) break } }) } function besHandleClick(e) { switch (e.target) { case e.target.closest('span'): const clicked = e.target.closest('span') const infoText = clicked?.dataset.info const myComponent = document.querySelector('my-component') myComponent.setAttribute('my-attribute', infoText) console.log(clicked) break } }