206 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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 = {
 | |
|       timer: null
 | |
|     }
 | |
|     besEditors[edit.id] = editor
 | |
|     besProof(edit)
 | |
|     edit.addEventListener('beforeinput', e => besHandleBeforeInput(edit.id, e), false)
 | |
|     edit.addEventListener('click', e => besHandleClick(e))
 | |
|     // TODO: Handle editor resizes.
 | |
|   })
 | |
| }
 | |
| 
 | |
| // Recursively grammar-proofs one node.
 | |
| async function besProof(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(el)) {
 | |
|           return [{ text: '<'+el.tagName+'/>', el: el, markup: true }]
 | |
|         }
 | |
|         besClearAllMistakes(el)
 | |
|         let data = []
 | |
|         for (const el2 of el.childNodes) {
 | |
|           data = data.concat(await besProof(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: '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 => {
 | |
|               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
 | |
|                   }
 | |
|                 }
 | |
| 
 | |
|                 besAddMistake(range, match)
 | |
|               })
 | |
| 
 | |
|               besMarkProofed(el)
 | |
|             })
 | |
|             .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 <tagName>...</tagName>.
 | |
|         let data = [{ text: '<'+el.tagName+'>', el: el, markup: true }]
 | |
|         for (const el2 of el.childNodes) {
 | |
|           data = data.concat(await besProof(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(editorId, event)
 | |
| {
 | |
|   let editor = besEditors[editorId]
 | |
|   if (editor.timer) clearTimeout(editor.timer)
 | |
|   editor.timer = setTimeout(function(){ besProof(edit) }, 1000)
 | |
| 
 | |
|   // No need to invalidate elements after range.startContainer since they will
 | |
|   // get either deleted or replaced.
 | |
|   let edit = document.getElementById(editorId)
 | |
|   event.getTargetRanges().forEach(range => besClearProofed(besGetBlockParent(range.startContainer, edit)))
 | |
| }
 | |
| 
 | |
| // Test if given block element has already been grammar-proofed.
 | |
| function besIsProofed(el)
 | |
| {
 | |
|   return el.getAttribute('besProofed') === 'true'
 | |
| }
 | |
| 
 | |
| // Mark given block element as grammar-proofed.
 | |
| function besMarkProofed(el)
 | |
| {
 | |
|   el.setAttribute('besProofed', 'true')
 | |
| }
 | |
| 
 | |
| // Mark given block element as not grammar-proofed.
 | |
| function besClearProofed(el)
 | |
| {
 | |
|   el?.removeAttribute('besProofed')
 | |
| }
 | |
| 
 | |
| // Remove all grammar mistakes markup for given block element.
 | |
| function besClearAllMistakes(el) {
 | |
|   for (const el2 of el.childNodes) {
 | |
|     if (el2.tagName === 'SPAN' && el2.classList.contains('typo-mistake')) {
 | |
|       el2.replaceWith(...el2.childNodes)
 | |
|       el2.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("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);
 | |
| 
 | |
|     // TODO: Find a solution to handle click events on the highlights
 | |
|     // const editor = document.querySelector('.online-editor')
 | |
|     // highlight.addEventListener("click", function(e) {
 | |
|       // console.log(e);
 | |
|       // editor.focus();
 | |
|       // besHandleClick(e);
 | |
|       // return true;
 | |
|     // });
 | |
|   }
 | |
| }
 | |
| 
 | |
| // 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(el, edit)
 | |
| {
 | |
|   for (; el && el !== edit; el = el.parentNode) {
 | |
|     if (el.nodeType === Node.ELEMENT_NODE && besIsBlockElement(el)) return el
 | |
|   }
 | |
|   return el
 | |
| }
 | |
| 
 | |
| 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
 | |
|   }
 | |
| }
 |