Mailspring/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.coffee
Evan Morikawa 0ab5886423 refactor(contenteditable): use DOM mutation observers
Summary: This uses DOM mutation observers instead of `onInput`

Test Plan: manual and new integration tests

Reviewers: bengotow, juan

Differential Revision: https://phab.nylas.com/D2291

feat(contenteditable): add bold, underline, etc keymaps

Moving button extensions out of toolbar

Extracted floating toolbar buttons

Convert ContenteditableExtension to new spec

Update packages to use new callback signature

Fix specs
2015-12-01 15:31:03 -08:00

129 lines
4.7 KiB
CoffeeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ComposerExtension, AccountStore, DOMUtils} = require 'nylas-exports'
_ = require 'underscore'
spellchecker = require('spellchecker')
remote = require('remote')
MenuItem = remote.require('menu-item')
SpellcheckCache = {}
class SpellcheckComposerExtension extends ComposerExtension
@isMisspelled: (word) ->
SpellcheckCache[word] ?= spellchecker.isMisspelled(word)
SpellcheckCache[word]
@onInput: (editableNode) =>
@walkTree(editableNode)
@onShowContextMenu: (editableNode, selection, event, menu) =>
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
word = range.toString()
if @isMisspelled(word)
corrections = spellchecker.getCorrectionsForMisspelling(word)
if corrections.length > 0
corrections.forEach (correction) =>
menu.append(new MenuItem({
label: correction,
click: @applyCorrection.bind(@, editableNode, range, selection, correction)
}))
else
menu.append(new MenuItem({ label: 'No Guesses Found', enabled: false}))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({
label: 'Learn Spelling',
click: @learnSpelling.bind(@, editableNode, word)
}))
menu.append(new MenuItem({ type: 'separator' }))
@applyCorrection: (editableNode, range, selection, correction) =>
DOMUtils.Mutating.applyTextInRange(range, selection, correction)
@walkTree(editableNode)
@learnSpelling: (editableNode, word) =>
spellchecker.add(word)
delete SpellcheckCache[word]
@walkTree(editableNode)
@walkTree: (editableNode) =>
treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_TEXT)
nodeList = []
selection = document.getSelection()
selectionSnapshot =
anchorNode: selection.anchorNode
anchorOffset: selection.anchorOffset
focusNode: selection.focusNode
focusOffset: selection.focusOffset
selectionImpacted = false
while (treeWalker.nextNode())
nodeList.push(treeWalker.currentNode)
while (node = nodeList.pop())
str = node.textContent
# https://regex101.com/r/bG5yC4/1
wordRegexp = /(\w[\w'-]*\w|\w)/g
while ((match = wordRegexp.exec(str)) isnt null)
spellingSpan = null
if node.parentNode and node.parentNode.nodeName is 'SPELLING'
if match[0] is str
spellingSpan = node.parentNode
else
node.parentNode.classList.remove('misspelled')
misspelled = @isMisspelled(match[0])
markedAsMisspelled = spellingSpan?.classList.contains('misspelled')
if misspelled and not markedAsMisspelled
# The insertion point is currently at the end of this misspelled word.
# Do not mark it until the user types a space or leaves.
if selectionSnapshot.focusNode is node and selectionSnapshot.focusOffset is match.index + match[0].length
continue
if spellingSpan
spellingSpan.classList.add('misspelled')
else
if match.index is 0
matchNode = node
else
matchNode = node.splitText(match.index)
afterMatchNode = matchNode.splitText(match[0].length)
spellingSpan = document.createElement('spelling')
spellingSpan.classList.add('misspelled')
spellingSpan.innerText = match[0]
matchNode.parentNode.replaceChild(spellingSpan, matchNode)
for prop in ['anchor', 'focus']
if selectionSnapshot["#{prop}Node"] is node
if selectionSnapshot["#{prop}Offset"] > match.index + match[0].length
selectionImpacted = true
selectionSnapshot["#{prop}Node"] = afterMatchNode
selectionSnapshot["#{prop}Offset"] -= match.index + match[0].length
else if selectionSnapshot["#{prop}Offset"] > match.index
selectionImpacted = true
selectionSnapshot["#{prop}Node"] = spellingSpan.childNodes[0]
selectionSnapshot["#{prop}Offset"] -= match.index
nodeList.unshift(afterMatchNode)
break
else if not misspelled and markedAsMisspelled
spellingSpan.classList.remove('misspelled')
if selectionImpacted
selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset)
@finalizeSessionBeforeSending: (session) ->
body = session.draft().body
clean = body.replace(/<\/?spelling[^>]*>/g, '')
if body != clean
session.changes.add(body: clean)
SpellcheckComposerExtension.SpellcheckCache = SpellcheckCache
module.exports = SpellcheckComposerExtension