Revise event handling, document, cleanup
This commit is contained in:
		
							
								
								
									
										184
									
								
								online-editor.js
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								online-editor.js
									
									
									
									
									
								
							| @@ -1,9 +1,9 @@ | |||||||
| const besUrl = 'http://localhost:225/api/v2/check' | const besUrl = 'http://localhost:225/api/v2/check' | ||||||
|  |  | ||||||
| let besEditors = [] // Collection of all editors on page | let besEditors = [] // Collection of all grammar checking services in the document | ||||||
|  |  | ||||||
| class BesEditor { | class BesEditor { | ||||||
|   constructor(edit) { |   constructor(edit, isCkeditor) { | ||||||
|     this.el = edit |     this.el = edit | ||||||
|     this.timer = null |     this.timer = null | ||||||
|     this.children = [] |     this.children = [] | ||||||
| @@ -11,31 +11,54 @@ class BesEditor { | |||||||
|     this.correctionPanel = correctionPanel |     this.correctionPanel = correctionPanel | ||||||
|     this.scrollPanel = scrollPanel |     this.scrollPanel = scrollPanel | ||||||
|     this.offsetTop = null |     this.offsetTop = null | ||||||
|     this.isCKeditor = false |     this.isCKeditor = !!isCkeditor | ||||||
|     this.disableSpellcheck(edit) |     edit.classList.add('bes-online-editor') | ||||||
|     this.proof(edit) |     this.originalSpellcheck = edit.spellcheck | ||||||
|     edit.addEventListener('beforeinput', e => this.handleBeforeInput(e), false) |  | ||||||
|     edit.addEventListener('click', e => this.handleClick(e)) |  | ||||||
|     edit.addEventListener('scroll', e => |  | ||||||
|       this.handleScrollEvent(edit, this.scrollPanel) |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Register editor |  | ||||||
|   static register(edit, isCkeditor) { |  | ||||||
|     let editor = new BesEditor(edit) |  | ||||||
|     besEditors.push(editor) |  | ||||||
|     if (isCkeditor) editor.isCKeditor = true |  | ||||||
|     return editor |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Set spellcheck to false |  | ||||||
|   disableSpellcheck(edit) { |  | ||||||
|     edit.spellcheck = false |     edit.spellcheck = false | ||||||
|  |     this.proof(edit) | ||||||
|  |     edit.addEventListener('beforeinput', BesEditor.handleBeforeInput, false) | ||||||
|  |     edit.addEventListener('click', BesEditor.handleClick) | ||||||
|  |     edit.addEventListener('scroll', BesEditor.handleScroll) | ||||||
|  |     besEditors.push(this) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Registers grammar checking service | ||||||
|  |    * | ||||||
|  |    * @param {Element} edit DOM element to register grammar checking service for | ||||||
|  |    * @param {Boolean} isCkeditor Enable CKEditor tweaks | ||||||
|  |    * @returns {BesEditor} Grammar checking service instance | ||||||
|  |    */ | ||||||
|  |   static register(edit, isCkeditor) { | ||||||
|  |     return new BesEditor(edit, isCkeditor) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Unregisters grammar checking service | ||||||
|  |    */ | ||||||
|  |   unregister() { | ||||||
|  |     this.el.removeEventListener('scroll', BesEditor.handleScroll) | ||||||
|  |     this.el.removeEventListener('click', BesEditor.handleClick) | ||||||
|  |     this.el.removeEventListener( | ||||||
|  |       'beforeinput', | ||||||
|  |       BesEditor.handleBeforeInput, | ||||||
|  |       false | ||||||
|  |     ) | ||||||
|  |     if (this.timer) clearTimeout(this.timer) | ||||||
|  |     besEditors = besEditors.filter(item => item !== this) | ||||||
|  |     this.el.spellcheck = this.originalSpellcheck | ||||||
|  |     this.el.classList.remove('bes-online-editor') | ||||||
|  |     this.correctionPanel.remove() | ||||||
|  |     this.scrollPanel.remove() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // TODO: add support for textarea elements |   // TODO: add support for textarea elements | ||||||
|   // Recursively grammar-proofs one node. |   /** | ||||||
|  |    * Recursively grammar-proofs a DOM tree. | ||||||
|  |    * | ||||||
|  |    * @param {Node} el DOM root node to proof | ||||||
|  |    * @returns {Array} Markup of text to proof using BesStr | ||||||
|  |    */ | ||||||
|   async proof(el) { |   async proof(el) { | ||||||
|     // If first child is not a block element, add a dummy <div>...</div> around it. |     // If first child is not a block element, add a dummy <div>...</div> around it. | ||||||
|     // This solution is still not fully tested and might need some improvements. |     // This solution is still not fully tested and might need some improvements. | ||||||
| @@ -196,27 +219,35 @@ class BesEditor { | |||||||
|     return { correctionPanel, scrollPanel } |     return { correctionPanel, scrollPanel } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Marks section of text that is about to change as not-yet-grammar-proofed. |   /** | ||||||
|   handleBeforeInput(event) { |    * beforeinput event handler | ||||||
|     if (this.timer) clearTimeout(this.timer) |    * | ||||||
|  |    * Marks section of the text that is about to change as not-yet-grammar-proofed. | ||||||
|  |    * | ||||||
|  |    * @param {InputEvent} event The event notifying the user of editable content changes | ||||||
|  |    */ | ||||||
|  |   static handleBeforeInput(event) { | ||||||
|  |     const edit = event.target | ||||||
|  |     let editor = besEditors.find(e => e.el === edit) | ||||||
|  |     if (!editor) return | ||||||
|  |     if (editor.timer) clearTimeout(editor.timer) | ||||||
|     let blockElements = new Set() |     let blockElements = new Set() | ||||||
|     event.getTargetRanges().forEach(range => { |     event.getTargetRanges().forEach(range => { | ||||||
|       BesEditor.getNodesInRange(range).forEach(el => |       BesEditor.getNodesInRange(range).forEach(el => | ||||||
|         blockElements.add(this.getBlockParent(el)) |         blockElements.add(editor.getBlockParent(el)) | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|     blockElements.forEach(block => { |     blockElements.forEach(block => { | ||||||
|       this.clearProofed(block) |       editor.clearProofed(block) | ||||||
|       this.clearMistakeMarkup(block) |       editor.clearMistakeMarkup(block) | ||||||
|       this.clearChildren(block) |       editor.clearChildren(block) | ||||||
|     }) |     }) | ||||||
|     let editor = this |     // Not a nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished. | ||||||
|     // Not the nice way to do it, but it works for now the repositionMistakes function is called before the DOM updates are finished. |  | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       editor.repositionMistakes(editor) |       editor.repositionMistakes() | ||||||
|     }, 0) |     }, 0) | ||||||
|     this.timer = setTimeout(function () { |     editor.timer = setTimeout(function () { | ||||||
|       editor.proof(editor.el) |       editor.proof(edit) | ||||||
|     }, 1000) |     }, 1000) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -230,7 +261,7 @@ class BesEditor { | |||||||
|   markProofed(el, matches) { |   markProofed(el, matches) { | ||||||
|     let newChild = { |     let newChild = { | ||||||
|       isProofed: true, |       isProofed: true, | ||||||
|       elements: el, |       elements: el, // TODO: Rename "elements" to "el" - 1. It contains only single element (plural elements is misleading), 2. BesEditor also uses "el" named field for DOM matching. | ||||||
|       matches: matches |       matches: matches | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -269,8 +300,8 @@ class BesEditor { | |||||||
|     else this.children = this.children.filter(child => child.elements !== el) |     else this.children = this.children.filter(child => child.elements !== el) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   repositionMistakes(editor) { |   repositionMistakes() { | ||||||
|     editor.children.forEach(child => { |     this.children.forEach(child => { | ||||||
|       this.clearMistakeMarkup(child.elements) |       this.clearMistakeMarkup(child.elements) | ||||||
|       child.matches.forEach(match => { |       child.matches.forEach(match => { | ||||||
|         const { clientRects, highlight } = this.addMistakeMarkup( |         const { clientRects, highlight } = this.addMistakeMarkup( | ||||||
| @@ -382,60 +413,71 @@ class BesEditor { | |||||||
|     return nodes |     return nodes | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   handleClick(e) { |   /** | ||||||
|     const targetEl = e.target |    * click event handler | ||||||
|  |    * | ||||||
|  |    * 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. | ||||||
|  |    */ | ||||||
|  |   static handleClick(event) { | ||||||
|  |     const edit = BesEditor.findParent(event.target) | ||||||
|  |     let editor = besEditors.find(e => e.el === edit) | ||||||
|  |     if (!editor) return | ||||||
|  |     const target = editor.getBlockParent(event.target) | ||||||
|     const popup = document.querySelector('bes-popup-el') |     const popup = document.querySelector('bes-popup-el') | ||||||
|     // If target has not parent with class 'bes-online-editor', find target's parent whose parent is 'bes-online-editor' |     const matches = editor.children.find( | ||||||
|     const target = BesEditor.findParent(targetEl) |       child => child.elements === target | ||||||
|       ? BesEditor.findParent(targetEl) |     )?.matches | ||||||
|       : targetEl |  | ||||||
|  |  | ||||||
|     const divIndex = this.children.findIndex(child => child.elements === target) |  | ||||||
|     const matches = this.children[divIndex]?.matches |  | ||||||
|     if (!matches) { |  | ||||||
|       popup.hide() |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     if ( |     if ( | ||||||
|       BesEditor.renderPopup(target, matches, popup, e.clientX, e.clientY, this) |       !matches || | ||||||
|  |       !editor.renderPopup(target, matches, popup, event.clientX, event.clientY) | ||||||
|     ) |     ) | ||||||
|       return |       popup.hide() | ||||||
|     else popup.hide() |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   handleScrollEvent(editor, scrollPanel) { |   /** | ||||||
|     scrollPanel.style.top = -editor.scrollTop + 'px' |    * scroll event handler | ||||||
|     this.offsetTop = editor.scrollTop |    * | ||||||
|  |    * Syncs grammar mistake positions with editor scroll offset. | ||||||
|  |    * | ||||||
|  |    * @param {Event} event The event which takes place. | ||||||
|  |    */ | ||||||
|  |   static handleScroll(event) { | ||||||
|  |     const edit = event.target | ||||||
|  |     let editor = besEditors.find(e => e.el === edit) | ||||||
|  |     if (!editor) return | ||||||
|  |     editor.scrollPanel.style.top = -edit.scrollTop + 'px' | ||||||
|  |     editor.offsetTop = edit.scrollTop | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       this.repositionMistakes(this) |       editor.repositionMistakes() | ||||||
|     }, 300) |     }, 300) | ||||||
|  |     // TODO: Move popup (if open) too. | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Finds the editor with grammar checking service the given DOM node is a child of. | ||||||
|  |    * | ||||||
|  |    * @param {Node} target DOM node | ||||||
|  |    * @returns {Element} Editor DOM element; null if DOM node is not a descendant of an editor. | ||||||
|  |    */ | ||||||
|   static findParent(target) { |   static findParent(target) { | ||||||
|     let element = target |     for (let el = target; el; el = el.parentNode) { | ||||||
|     while (element && element.parentNode) { |       if (el.classList?.contains('bes-online-editor')) { | ||||||
|       if (element.parentNode.classList?.contains('bes-online-editor')) { |         return el | ||||||
|         return element |  | ||||||
|       } |       } | ||||||
|       element = element.parentNode |  | ||||||
|     } |     } | ||||||
|     return null |     return null | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static renderPopup(el, matches, popup, clientX, clientY, editor) { |   renderPopup(el, matches, popup, clientX, clientY) { | ||||||
|     for (let m of matches) { |     for (let m of matches) { | ||||||
|       if (m.rects) { |       if (m.rects) { | ||||||
|         for (let r of m.rects) { |         for (let r of m.rects) { | ||||||
|           if (BesEditor.isPointInRect(clientX, clientY, r)) { |           if (BesEditor.isPointInRect(clientX, clientY, r)) { | ||||||
|             popup.changeText(m.match.message) |             popup.changeText(m.match.message) | ||||||
|             m.match.replacements.forEach(replacement => { |             m.match.replacements.forEach(replacement => { | ||||||
|               popup.appendReplacements( |               popup.appendReplacements(el, r, m.match, replacement.value, this) | ||||||
|                 el, |  | ||||||
|                 r, |  | ||||||
|                 m.match, |  | ||||||
|                 replacement.value, |  | ||||||
|                 editor |  | ||||||
|               ) |  | ||||||
|             }) |             }) | ||||||
|             popup.show(clientX, clientY) |             popup.show(clientX, clientY) | ||||||
|             return true |             return true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user