mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-25 09:46:07 +08:00
f3d58aaede
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
129 lines
4.7 KiB
CoffeeScript
129 lines
4.7 KiB
CoffeeScript
{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
|