diff --git a/spec/components/blockquote-manager-spec.coffee b/spec/components/blockquote-manager-spec.coffee new file mode 100644 index 000000000..4e7bac07e --- /dev/null +++ b/spec/components/blockquote-manager-spec.coffee @@ -0,0 +1,130 @@ +{DOMUtils} = require 'nylas-exports' +BlockquoteManager = require '../../src/components/contenteditable/blockquote-manager' + +describe "BlockquoteManager", -> + outdentCases = [""" +
|
+ """ + , + """ +
+ | +
+ """ + , + """ +

+ \n + | + """ + , + """ + +

+ + | + """ + , + """ +
+
+
|
+
+
+ """ + , + """ +
+ + | +
+ """ + , + """ + +

yo

+ + + + + |test + + """ + ] + + backspaceCases = [""" +
yo|
+ """ + , + """ +
+ yo + | +
+ """ + , + """ +

+   + | + """ + , + """ + +

+ yo + | + """ + , + """ +
+
+
yo|
+
+
+ """ + , + """ +
+ yo + | +
+ """ + , + """ + +

yo

+ + + yo + + |test + + """ + ] + + setupContext = (testCase) -> + context = document.createElement("blockquote") + context.innerHTML = testCase + {node, index} = DOMUtils.findCharacter(context, "|") + if not node then throw new Error("Couldn't find where to set Selection") + mockSelection = { + isCollapsed: true + anchorNode: node + anchorOffset: index + } + return mockSelection + + outdentCases.forEach (testCase) -> + it """outdents\n#{testCase}""", -> + mockSelection = setupContext(testCase) + editor = {currentSelection: -> mockSelection} + expect(BlockquoteManager._isInBlockquote(editor)).toBe true + expect(BlockquoteManager._isAtStartOfLine(editor)).toBe true + + backspaceCases.forEach (testCase) -> + it """backspaces (does NOT outdent)\n#{testCase}""", -> + mockSelection = setupContext(testCase) + editor = {currentSelection: -> mockSelection} + expect(BlockquoteManager._isInBlockquote(editor)).toBe true + expect(BlockquoteManager._isAtStartOfLine(editor)).toBe false diff --git a/src/components/contenteditable/blockquote-manager.coffee b/src/components/contenteditable/blockquote-manager.coffee new file mode 100644 index 000000000..91ce2144f --- /dev/null +++ b/src/components/contenteditable/blockquote-manager.coffee @@ -0,0 +1,36 @@ +{DOMUtils, ContenteditableExtension} = require 'nylas-exports' + +class BlockquoteManager extends ContenteditableExtension + @onKeyDown: ({editor, event}) -> + if event.key is "Backspace" + if @_isInBlockquote(editor) and @_isAtStartOfLine(editor) + editor.outdent() + event.preventDefault() + + @_isInBlockquote: (editor) -> + sel = editor.currentSelection() + return unless sel.isCollapsed + DOMUtils.closest(sel.anchorNode, "blockquote")? + + @_isAtStartOfLine: (editor) -> + sel = editor.currentSelection() + return false unless sel.anchorNode + return false unless sel.isCollapsed + return false unless sel.anchorOffset is 0 + + return @_ancestorRelativeLooksLikeBlock(sel.anchorNode) + + @_ancestorRelativeLooksLikeBlock: (node) -> + return true if DOMUtils.looksLikeBlockElement(node) + sibling = node + while sibling = sibling.previousSibling + if DOMUtils.looksLikeBlockElement(sibling) + return true + + if DOMUtils.looksLikeNonEmptyNode(sibling) + return false + + # never found block level element + return @_ancestorRelativeLooksLikeBlock(node.parentNode) + +module.exports = BlockquoteManager diff --git a/src/components/contenteditable/contenteditable.cjsx b/src/components/contenteditable/contenteditable.cjsx index 46746f1b7..a07a8b082 100644 --- a/src/components/contenteditable/contenteditable.cjsx +++ b/src/components/contenteditable/contenteditable.cjsx @@ -13,6 +13,7 @@ ListManager = require './list-manager' MouseService = require './mouse-service' DOMNormalizer = require './dom-normalizer' ClipboardService = require './clipboard-service' +BlockquoteManager = require './blockquote-manager' ### Public: A modern React-compatible contenteditable @@ -62,7 +63,7 @@ class Contenteditable extends React.Component coreServices: [MouseService, ClipboardService] - coreExtensions: [DOMNormalizer, ListManager, TabManager] + coreExtensions: [DOMNormalizer, ListManager, TabManager, BlockquoteManager] ######################################################################## diff --git a/src/dom-utils.coffee b/src/dom-utils.coffee index b68b6c9f7..f9b8fa642 100644 --- a/src/dom-utils.coffee +++ b/src/dom-utils.coffee @@ -344,6 +344,18 @@ DOMUtils = return nodeList + findCharacter: (context, character) -> + node = null + index = null + treeWalker = document.createTreeWalker(context, NodeFilter.SHOW_TEXT) + while currentNode = treeWalker.nextNode() + i = currentNode.data.indexOf(character) + if i >= 0 + node = currentNode + index = i + break + return {node, index} + escapeHTMLCharacters: (text) -> map = '&': '&', @@ -607,4 +619,18 @@ DOMUtils = parent = parent.parentElement false + looksLikeBlockElement: (node) -> + return node.nodeName in ["BR", "P", "BLOCKQUOTE", "DIV", "TABLE"] + + # When detecting if we're at the start of a "visible" line, we need to look + # for text nodes that have visible content in them. + looksLikeNonEmptyNode: (node) -> + textNode = DOMUtils.findFirstTextNode(node) + if textNode + if /^[\n ]*$/.test(textNode.data) + return false + else return true + else + return false + module.exports = DOMUtils