Split BesService to isolate CKEditor specifics in BesCKService
This commit is contained in:
		@@ -15,7 +15,7 @@
 | 
				
			|||||||
    <script>
 | 
					    <script>
 | 
				
			||||||
      ClassicEditor.create(document.querySelector('#editor'))
 | 
					      ClassicEditor.create(document.querySelector('#editor'))
 | 
				
			||||||
        .then(newEditor => {
 | 
					        .then(newEditor => {
 | 
				
			||||||
          BesService.register(newEditor.ui.view.editable.element, newEditor)
 | 
					          BesCKService.register(newEditor.ui.view.editable.element, newEditor)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .catch(error => {
 | 
					        .catch(error => {
 | 
				
			||||||
          console.error(error)
 | 
					          console.error(error)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										196
									
								
								service.js
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								service.js
									
									
									
									
									
								
							@@ -4,21 +4,27 @@ let besServices = [] // Collection of all grammar checking services in the docum
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// TODO: Add support for <textarea>
 | 
					// TODO: Add support for <textarea>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Grammar checking service base class
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
class BesService {
 | 
					class BesService {
 | 
				
			||||||
  constructor(hostElement, ckEditorInstance) {
 | 
					  constructor(hostElement) {
 | 
				
			||||||
    this.hostElement = hostElement
 | 
					    this.hostElement = hostElement
 | 
				
			||||||
    this.timer = null
 | 
					    this.timer = null
 | 
				
			||||||
    this.children = []
 | 
					    this.children = []
 | 
				
			||||||
    const { correctionPanel, scrollPanel } = this.createCorrectionPanel(hostElement)
 | 
					    const { correctionPanel, scrollPanel } =
 | 
				
			||||||
 | 
					      this.createCorrectionPanel(hostElement)
 | 
				
			||||||
    this.correctionPanel = correctionPanel
 | 
					    this.correctionPanel = correctionPanel
 | 
				
			||||||
    this.scrollPanel = scrollPanel
 | 
					    this.scrollPanel = scrollPanel
 | 
				
			||||||
    this.offsetTop = null
 | 
					    this.offsetTop = null
 | 
				
			||||||
    this.ckEditorInstance = ckEditorInstance
 | 
					 | 
				
			||||||
    this.originalSpellcheck = hostElement.spellcheck
 | 
					    this.originalSpellcheck = hostElement.spellcheck
 | 
				
			||||||
    hostElement.spellcheck = false
 | 
					 | 
				
			||||||
    this.abortController = new AbortController()
 | 
					    this.abortController = new AbortController()
 | 
				
			||||||
    this.proof(hostElement)
 | 
					    hostElement.spellcheck = false
 | 
				
			||||||
    hostElement.addEventListener('beforeinput', BesService.handleBeforeInput, false)
 | 
					    hostElement.addEventListener(
 | 
				
			||||||
 | 
					      'beforeinput',
 | 
				
			||||||
 | 
					      BesService.handleBeforeInput,
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    hostElement.addEventListener('click', BesService.handleClick)
 | 
					    hostElement.addEventListener('click', BesService.handleClick)
 | 
				
			||||||
    hostElement.addEventListener('scroll', BesService.handleScroll)
 | 
					    hostElement.addEventListener('scroll', BesService.handleScroll)
 | 
				
			||||||
    besServices.push(this)
 | 
					    besServices.push(this)
 | 
				
			||||||
@@ -28,11 +34,12 @@ class BesService {
 | 
				
			|||||||
   * Registers grammar checking service
 | 
					   * Registers grammar checking service
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {Element} hostElement DOM element to register grammar checking service for
 | 
					   * @param {Element} hostElement DOM element to register grammar checking service for
 | 
				
			||||||
   * @param {CKEditorInstance} ckEditorInstance Enable CKEditor tweaks
 | 
					 | 
				
			||||||
   * @returns {BesService} Grammar checking service instance
 | 
					   * @returns {BesService} Grammar checking service instance
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static register(hostElement, ckEditorInstance) {
 | 
					  static register(hostElement) {
 | 
				
			||||||
    return new BesService(hostElement, ckEditorInstance)
 | 
					    let service = new BesService(hostElement)
 | 
				
			||||||
 | 
					    service.proof(hostElement)
 | 
				
			||||||
 | 
					    return service
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -47,6 +54,7 @@ class BesService {
 | 
				
			|||||||
      false
 | 
					      false
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    if (this.timer) clearTimeout(this.timer)
 | 
					    if (this.timer) clearTimeout(this.timer)
 | 
				
			||||||
 | 
					    service.abortController.abort()
 | 
				
			||||||
    besServices = besServices.filter(item => item !== this)
 | 
					    besServices = besServices.filter(item => item !== this)
 | 
				
			||||||
    this.hostElement.spellcheck = this.originalSpellcheck
 | 
					    this.hostElement.spellcheck = this.originalSpellcheck
 | 
				
			||||||
    this.correctionPanel.remove()
 | 
					    this.correctionPanel.remove()
 | 
				
			||||||
@@ -255,9 +263,6 @@ class BesService {
 | 
				
			|||||||
      element: el,
 | 
					      element: el,
 | 
				
			||||||
      matches: matches
 | 
					      matches: matches
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // This is a solution for displaying mistakes in CKEditor. It is not the best solution, but it works for now.
 | 
					 | 
				
			||||||
    if (this.ckEditorInstance) window.dispatchEvent(new Event('resize'))
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -307,7 +312,6 @@ class BesService {
 | 
				
			|||||||
   * @returns {Object} Client rectangles and grammar mistake highlight elements
 | 
					   * @returns {Object} Client rectangles and grammar mistake highlight elements
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  addMistakeMarkup(range) {
 | 
					  addMistakeMarkup(range) {
 | 
				
			||||||
    // In CKEditor case, the highlight element is not shown for some reason. But after resizing the window it is shown.
 | 
					 | 
				
			||||||
    const clientRects = range.getClientRects()
 | 
					    const clientRects = range.getClientRects()
 | 
				
			||||||
    const scrollPanelRect = this.scrollPanel.getBoundingClientRect()
 | 
					    const scrollPanelRect = this.scrollPanel.getBoundingClientRect()
 | 
				
			||||||
    let highlights = []
 | 
					    let highlights = []
 | 
				
			||||||
@@ -521,36 +525,8 @@ class BesService {
 | 
				
			|||||||
  replaceText(el, match, replacement) {
 | 
					  replaceText(el, match, replacement) {
 | 
				
			||||||
    if (this.timer) clearTimeout(this.timer)
 | 
					    if (this.timer) clearTimeout(this.timer)
 | 
				
			||||||
    this.abortController.abort()
 | 
					    this.abortController.abort()
 | 
				
			||||||
    // const tags = this.getTagsAndText(el)
 | 
					 | 
				
			||||||
    if (!this.ckEditorInstance) {
 | 
					 | 
				
			||||||
    match.range.deleteContents()
 | 
					    match.range.deleteContents()
 | 
				
			||||||
    match.range.insertNode(document.createTextNode(replacement))
 | 
					    match.range.insertNode(document.createTextNode(replacement))
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const { ckEditorInstance } = this
 | 
					 | 
				
			||||||
      ckEditorInstance.model.change(writer => {
 | 
					 | 
				
			||||||
        const viewElement =
 | 
					 | 
				
			||||||
          ckEditorInstance.editing.view.domConverter.mapDomToView(el)
 | 
					 | 
				
			||||||
        const modelElement =
 | 
					 | 
				
			||||||
          ckEditorInstance.editing.mapper.toModelElement(viewElement)
 | 
					 | 
				
			||||||
        if (modelElement) {
 | 
					 | 
				
			||||||
          const elementRange = writer.createRangeIn(modelElement)
 | 
					 | 
				
			||||||
          // TODO: This logic should work once the HTML tags are removed from match.match.offset and match.match.length if is possible.
 | 
					 | 
				
			||||||
          if (
 | 
					 | 
				
			||||||
            elementRange.start.offset <= match.match.offset &&
 | 
					 | 
				
			||||||
            elementRange.end.offset >= match.match.offset + match.match.length
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            const start = writer.createPositionAt(modelElement, match.match.offset)
 | 
					 | 
				
			||||||
            const end = writer.createPositionAt(
 | 
					 | 
				
			||||||
              modelElement,
 | 
					 | 
				
			||||||
              match.match.offset + match.match.length
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            const range = writer.createRange(start, end)
 | 
					 | 
				
			||||||
            writer.remove(range)
 | 
					 | 
				
			||||||
            writer.insertText(replacement, start)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.clearMistakeMarkup(el)
 | 
					    this.clearMistakeMarkup(el)
 | 
				
			||||||
    // In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change.
 | 
					    // In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change.
 | 
				
			||||||
    // It maintains reasonable performance as it only checks the block element that has been modified,
 | 
					    // It maintains reasonable performance as it only checks the block element that has been modified,
 | 
				
			||||||
@@ -588,31 +564,108 @@ class BesService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.onload = () => {
 | 
					///
 | 
				
			||||||
  document
 | 
					/// Grammar checking service for CKEditor
 | 
				
			||||||
    .querySelectorAll('.bes-service')
 | 
					///
 | 
				
			||||||
    .forEach(hostElement => BesService.register(hostElement))
 | 
					class BesCKService extends BesService {
 | 
				
			||||||
 | 
					  constructor(hostElement, ckEditorInstance) {
 | 
				
			||||||
 | 
					    super(hostElement)
 | 
				
			||||||
 | 
					    this.ckEditorInstance = ckEditorInstance
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.onresize = () => {
 | 
					  /**
 | 
				
			||||||
  besServices.forEach(service => {
 | 
					   * Registers grammar checking service
 | 
				
			||||||
    service.setCorrectionPanelSize(
 | 
					   *
 | 
				
			||||||
      service.hostElement,
 | 
					   * @param {Element} hostElement DOM element to register grammar checking service for
 | 
				
			||||||
      service.correctionPanel,
 | 
					   * @param {CKEditorInstance} ckEditorInstance Enable CKEditor tweaks
 | 
				
			||||||
      service.scrollPanel
 | 
					   * @returns {BesCKService} Grammar checking service instance
 | 
				
			||||||
    )
 | 
					   */
 | 
				
			||||||
    service.children.forEach(child => {
 | 
					  static register(hostElement, ckEditorInstance) {
 | 
				
			||||||
      service.clearMistakeMarkup(child.element)
 | 
					    let service = new BesCKService(hostElement, ckEditorInstance)
 | 
				
			||||||
 | 
					    service.proof(hostElement)
 | 
				
			||||||
 | 
					    return service
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Marks given block element as grammar-proofed.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {Element} el DOM element that was checked
 | 
				
			||||||
 | 
					   * @param {Array} matches Grammar mistakes
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  markProofed(el, matches) {
 | 
				
			||||||
 | 
					    super.markProofed(el, matches)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // This is a solution for displaying mistakes in CKEditor. It is not the best solution, but it works for now.
 | 
				
			||||||
 | 
					    if (this.ckEditorInstance) window.dispatchEvent(new Event('resize'))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Removes given block element from this.children array
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {Element} el DOM element for removal
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  removeChild(el) {
 | 
				
			||||||
 | 
					    this.children = this.children.filter(child => child.element !== el)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Updates grammar mistake markup positions.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  repositionMistakes() {
 | 
				
			||||||
 | 
					    this.children.forEach(child => {
 | 
				
			||||||
 | 
					      this.clearMistakeMarkup(child.element)
 | 
				
			||||||
      child.matches.forEach(match => {
 | 
					      child.matches.forEach(match => {
 | 
				
			||||||
        const { clientRects, highlights } = service.addMistakeMarkup(match.range)
 | 
					        const { clientRects, highlights } = this.addMistakeMarkup(match.range)
 | 
				
			||||||
        match.rects = clientRects
 | 
					        match.rects = clientRects
 | 
				
			||||||
        match.highlights = highlights
 | 
					        match.highlights = highlights
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This is popup element
 | 
					  // TODO: In rich HTML texts, match.offset has different value than in plain text.
 | 
				
			||||||
 | 
					  // This function should be able to handle both cases or find a way that works for both.
 | 
				
			||||||
 | 
					  replaceText(el, match, replacement) {
 | 
				
			||||||
 | 
					    if (this.timer) clearTimeout(this.timer)
 | 
				
			||||||
 | 
					    this.abortController.abort()
 | 
				
			||||||
 | 
					    const { ckEditorInstance } = this
 | 
				
			||||||
 | 
					    ckEditorInstance.model.change(writer => {
 | 
				
			||||||
 | 
					      const viewElement =
 | 
				
			||||||
 | 
					        ckEditorInstance.editing.view.domConverter.mapDomToView(el)
 | 
				
			||||||
 | 
					      const modelElement =
 | 
				
			||||||
 | 
					        ckEditorInstance.editing.mapper.toModelElement(viewElement)
 | 
				
			||||||
 | 
					      if (modelElement) {
 | 
				
			||||||
 | 
					        const elementRange = writer.createRangeIn(modelElement)
 | 
				
			||||||
 | 
					        // TODO: This logic should work once the HTML tags are removed from match.match.offset and match.match.length if is possible.
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          elementRange.start.offset <= match.match.offset &&
 | 
				
			||||||
 | 
					          elementRange.end.offset >= match.match.offset + match.match.length
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          const start = writer.createPositionAt(
 | 
				
			||||||
 | 
					            modelElement,
 | 
				
			||||||
 | 
					            match.match.offset
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          const end = writer.createPositionAt(
 | 
				
			||||||
 | 
					            modelElement,
 | 
				
			||||||
 | 
					            match.match.offset + match.match.length
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          const range = writer.createRange(start, end)
 | 
				
			||||||
 | 
					          writer.remove(range)
 | 
				
			||||||
 | 
					          writer.insertText(replacement, start)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.clearMistakeMarkup(el)
 | 
				
			||||||
 | 
					    // In my opinion, this approach provides the most straightforward solution for repositioning mistakes after a change.
 | 
				
			||||||
 | 
					    // It maintains reasonable performance as it only checks the block element that has been modified,
 | 
				
			||||||
 | 
					    // rather than re-evaluating the entire document or a larger set of elements.
 | 
				
			||||||
 | 
					    this.abortController = new AbortController()
 | 
				
			||||||
 | 
					    this.proof(el)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Grammar mistake popup dialog
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
class BesPopupEl extends HTMLElement {
 | 
					class BesPopupEl extends HTMLElement {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
@@ -731,12 +784,37 @@ class BesPopupEl extends HTMLElement {
 | 
				
			|||||||
    replacementBtn.textContent = replacement
 | 
					    replacementBtn.textContent = replacement
 | 
				
			||||||
    replacementBtn.classList.add('bes-replacement')
 | 
					    replacementBtn.classList.add('bes-replacement')
 | 
				
			||||||
    replacementBtn.addEventListener('click', () => {
 | 
					    replacementBtn.addEventListener('click', () => {
 | 
				
			||||||
      if (allowReplacements)
 | 
					      if (allowReplacements) service.replaceText(el, match, replacement)
 | 
				
			||||||
        service.replaceText(el, match, replacement)
 | 
					 | 
				
			||||||
      // TODO: Close popup
 | 
					      // TODO: Close popup
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    replacementDiv.appendChild(replacementBtn)
 | 
					    replacementDiv.appendChild(replacementBtn)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.onload = () => {
 | 
				
			||||||
 | 
					  document
 | 
				
			||||||
 | 
					    .querySelectorAll('.bes-service')
 | 
				
			||||||
 | 
					    .forEach(hostElement => BesService.register(hostElement))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.onresize = () => {
 | 
				
			||||||
 | 
					  besServices.forEach(service => {
 | 
				
			||||||
 | 
					    service.setCorrectionPanelSize(
 | 
				
			||||||
 | 
					      service.hostElement,
 | 
				
			||||||
 | 
					      service.correctionPanel,
 | 
				
			||||||
 | 
					      service.scrollPanel
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    service.children.forEach(child => {
 | 
				
			||||||
 | 
					      service.clearMistakeMarkup(child.element)
 | 
				
			||||||
 | 
					      child.matches.forEach(match => {
 | 
				
			||||||
 | 
					        const { clientRects, highlights } = service.addMistakeMarkup(
 | 
				
			||||||
 | 
					          match.range
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        match.rects = clientRects
 | 
				
			||||||
 | 
					        match.highlights = highlights
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
customElements.define('bes-popup-el', BesPopupEl)
 | 
					customElements.define('bes-popup-el', BesPopupEl)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user