mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-04 19:54:32 +08:00
fix(extension-adapter): Update adapter to support all versions of extension api we've used
Summary: - Rewrites composer extension adpater to support all versions of the ComposerExtension API we've ever declared. This will allow old plugins (or plugins that haven't been reinstalled after update) to keep functioning without breaking N1 - Adds specs Test Plan: - Unit tests Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2399
This commit is contained in:
parent
4628643195
commit
6315bc9d80
33 changed files with 374 additions and 151 deletions
|
@ -14,6 +14,7 @@
|
|||
"eqeqeq": [2, "smart"],
|
||||
"id-length": [0],
|
||||
"no-loop-func": [0],
|
||||
"new-cap": [2, {"capIsNew": false}]
|
||||
"new-cap": [2, {"capIsNew": false}],
|
||||
"no-shadow": [1]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ This extension displays a warning before sending a draft that contains the names
|
|||
|
||||
class ProductsExtension extends ComposerExtension
|
||||
|
||||
@warningsForSending: (draft) ->
|
||||
@warningsForSending: ({draft}) ->
|
||||
words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']
|
||||
body = draft.body.toLowercase()
|
||||
for word in words
|
||||
|
@ -35,9 +35,9 @@ class ProductsExtension extends ComposerExtension
|
|||
return ["with the word '#{word}'?"]
|
||||
return []
|
||||
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
@finalizeSessionBeforeSending: ({session}) ->
|
||||
draft = session.draft()
|
||||
if @warningsForSending(draft)
|
||||
if @warningsForSending({draft})
|
||||
bodyWithWarning = draft.body += "<br>This email \
|
||||
contains competitor's product names \
|
||||
or trademarks used in context."
|
||||
|
|
|
@ -2,7 +2,7 @@ import {DOMUtils, ComposerExtension} from 'nylas-exports';
|
|||
|
||||
class TemplatesComposerExtension extends ComposerExtension {
|
||||
|
||||
static warningsForSending(draft) {
|
||||
static warningsForSending({draft}) {
|
||||
const warnings = [];
|
||||
if (draft.body.search(/<code[^>]*empty[^>]*>/i) > 0) {
|
||||
warnings.push('with an empty template area');
|
||||
|
@ -10,7 +10,7 @@ class TemplatesComposerExtension extends ComposerExtension {
|
|||
return warnings;
|
||||
}
|
||||
|
||||
static finalizeSessionBeforeSending(session) {
|
||||
static finalizeSessionBeforeSending({session}) {
|
||||
const body = session.draft().body;
|
||||
const clean = body.replace(/<\/?code[^>]*>/g, '');
|
||||
if (body !== clean) {
|
||||
|
@ -18,33 +18,34 @@ class TemplatesComposerExtension extends ComposerExtension {
|
|||
}
|
||||
}
|
||||
|
||||
static onClick(editor, event) {
|
||||
var node = event.target;
|
||||
if(node.nodeName === "CODE" && node.classList.contains("var") && node.classList.contains("empty")) {
|
||||
editor.selectAllChildren(node)
|
||||
static onClick({editor, event}) {
|
||||
const node = event.target;
|
||||
if (node.nodeName === 'CODE' && node.classList.contains('var') && node.classList.contains('empty')) {
|
||||
editor.selectAllChildren(node);
|
||||
}
|
||||
}
|
||||
|
||||
static onKeyDown(editor, event) {
|
||||
static onKeyDown({editor, event}) {
|
||||
const editableNode = editor.rootNode;
|
||||
if (event.key === 'Tab') {
|
||||
const nodes = editableNode.querySelectorAll('code.var');
|
||||
if(nodes.length>0) {
|
||||
let sel = editor.currentSelection();
|
||||
if (nodes.length > 0) {
|
||||
const sel = editor.currentSelection();
|
||||
let found = false;
|
||||
|
||||
// First, try to find a <code> that the selection is within. If found,
|
||||
// select the next/prev node if the selection ends at the end of the
|
||||
// <code>'s text, otherwise select the <code>'s contents.
|
||||
for (let i=0; i<nodes.length; i++) {
|
||||
let node = nodes[i];
|
||||
if(DOMUtils.selectionIsWithin(node)) {
|
||||
let selIndex = editor.getSelectionTextIndex(node);
|
||||
let length = DOMUtils.getIndexedTextContent(node).slice(-1)[0].end;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (DOMUtils.selectionIsWithin(node)) {
|
||||
const selIndex = editor.getSelectionTextIndex(node);
|
||||
const length = DOMUtils.getIndexedTextContent(node).slice(-1)[0].end;
|
||||
let nextIndex = i;
|
||||
if(selIndex.endIndex === length)
|
||||
nextIndex = event.shiftKey ? i-1 : i+1;
|
||||
nextIndex = (nextIndex+nodes.length) % nodes.length; //allow wraparound in both directions
|
||||
if (selIndex.endIndex === length) {
|
||||
nextIndex = event.shiftKey ? i - 1 : i + 1;
|
||||
}
|
||||
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
||||
sel.selectAllChildren(nodes[nextIndex]);
|
||||
found = true;
|
||||
break;
|
||||
|
@ -53,28 +54,27 @@ class TemplatesComposerExtension extends ComposerExtension {
|
|||
|
||||
// If we failed to find a <code> that the selection is within, select the
|
||||
// nearest <code> before/after the selection (depending on shift).
|
||||
if(!found) {
|
||||
let treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);
|
||||
let curIndex = 0, nextIndex = null;
|
||||
if (!found) {
|
||||
const treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);
|
||||
let curIndex = 0;
|
||||
let nextIndex = null;
|
||||
let node;
|
||||
while (node = treeWalker.nextNode()) {
|
||||
if(sel.anchorNode === node || sel.focusNode === node)
|
||||
break;
|
||||
if(node.nodeName === "CODE" && node.classList.contains("var"))
|
||||
curIndex++
|
||||
if (sel.anchorNode === node || sel.focusNode === node) break;
|
||||
if (node.nodeName === 'CODE' && node.classList.contains('var')) curIndex++;
|
||||
}
|
||||
nextIndex = event.shiftKey ? curIndex-1 : curIndex;
|
||||
nextIndex = (nextIndex+nodes.length) % nodes.length; //allow wraparound in both directions
|
||||
nextIndex = event.shiftKey ? curIndex - 1 : curIndex;
|
||||
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
||||
sel.selectAllChildren(nodes[nextIndex]);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
else if(event.key === 'Enter') {
|
||||
} else if (event.key === 'Enter') {
|
||||
const nodes = editableNode.querySelectorAll('code.var');
|
||||
for (let i=0; i<nodes.length; i++) {
|
||||
if(DOMUtils.selectionStartsOrEndsIn(nodes[i])) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (DOMUtils.selectionStartsOrEndsIn(nodes[i])) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
|
@ -83,9 +83,9 @@ class TemplatesComposerExtension extends ComposerExtension {
|
|||
}
|
||||
}
|
||||
|
||||
static onContentChanged(editor) {
|
||||
editableNode = editor.rootNode;
|
||||
selection = editor.currentSelection().rawSelection;
|
||||
static onContentChanged({editor}) {
|
||||
const editableNode = editor.rootNode;
|
||||
const selection = editor.currentSelection().rawSelection;
|
||||
const isWithinNode = (node)=> {
|
||||
let test = selection.baseNode;
|
||||
while (test !== editableNode) {
|
||||
|
@ -100,7 +100,7 @@ class TemplatesComposerExtension extends ComposerExtension {
|
|||
const result = [];
|
||||
for (let i = 0, codeTag; i < codeTags.length; i++) {
|
||||
codeTag = codeTags[i];
|
||||
codeTag.textContent = codeTag.textContent; //sets node contents to just its textContent, strips HTML
|
||||
codeTag.textContent = codeTag.textContent; // sets node contents to just its textContent, strips HTML
|
||||
result.push((() => {
|
||||
if (selection.containsNode(codeTag) || isWithinNode(codeTag)) {
|
||||
return codeTag.classList.remove('empty');
|
||||
|
|
|
@ -6,7 +6,7 @@ class AvailabilityComposerExtension extends ComposerExtension
|
|||
# When subclassing the ComposerExtension, you can add your own custom logic
|
||||
# to execute before a draft is sent in the @finalizeSessionBeforeSending
|
||||
# method. Here, we're registering the events before we send the draft.
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
@finalizeSessionBeforeSending: ({session}) ->
|
||||
body = session.draft().body
|
||||
participants = session.draft().participants()
|
||||
sender = session.draft().from
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ComposerExtension, AccountStore} = require 'nylas-exports'
|
||||
|
||||
class SignatureComposerExtension extends ComposerExtension
|
||||
@prepareNewDraft: (draft) ->
|
||||
@prepareNewDraft: ({draft}) ->
|
||||
accountId = AccountStore.current().id
|
||||
signature = NylasEnv.config.get("nylas.account-#{accountId}.signature")
|
||||
return unless signature
|
||||
|
|
|
@ -18,9 +18,9 @@ describe "SignatureComposerExtension", ->
|
|||
draft: true
|
||||
body: 'This is a another test.'
|
||||
|
||||
SignatureComposerExtension.prepareNewDraft(a)
|
||||
SignatureComposerExtension.prepareNewDraft(draft: a)
|
||||
expect(a.body).toEqual('This is a test! <br/><div class="nylas-n1-signature"><div id="signature">This is my signature.</div></div><blockquote>Hello world</blockquote>')
|
||||
SignatureComposerExtension.prepareNewDraft(b)
|
||||
SignatureComposerExtension.prepareNewDraft(draft: b)
|
||||
expect(b.body).toEqual('This is a another test.<br/><div class="nylas-n1-signature"><div id="signature">This is my signature.</div></div>')
|
||||
|
||||
describe "when a signature is not defined", ->
|
||||
|
@ -32,5 +32,5 @@ describe "SignatureComposerExtension", ->
|
|||
a = new Message
|
||||
draft: true
|
||||
body: 'This is a test! <blockquote>Hello world</blockquote>'
|
||||
SignatureComposerExtension.prepareNewDraft(a)
|
||||
SignatureComposerExtension.prepareNewDraft(draft: a)
|
||||
expect(a.body).toEqual('This is a test! <blockquote>Hello world</blockquote>')
|
||||
|
|
|
@ -12,10 +12,10 @@ class SpellcheckComposerExtension extends ComposerExtension
|
|||
SpellcheckCache[word] ?= spellchecker.isMisspelled(word)
|
||||
SpellcheckCache[word]
|
||||
|
||||
@onInput: (editableNode) =>
|
||||
@walkTree(editableNode)
|
||||
@onContentChanged: ({editor}) =>
|
||||
@walkTree(editor.rootNode)
|
||||
|
||||
@onShowContextMenu: (editor, event, menu) =>
|
||||
@onShowContextMenu: ({editor, event, menu}) =>
|
||||
selection = editor.currentSelection()
|
||||
editableNode = editor.rootNode
|
||||
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
|
||||
|
@ -119,7 +119,7 @@ class SpellcheckComposerExtension extends ComposerExtension
|
|||
if selectionImpacted
|
||||
selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset)
|
||||
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
@finalizeSessionBeforeSending: ({session}) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?spelling[^>]*>/g, '')
|
||||
if body != clean
|
||||
|
|
|
@ -27,7 +27,7 @@ describe "SpellcheckComposerExtension", ->
|
|||
changes:
|
||||
add: jasmine.createSpy('add')
|
||||
|
||||
SpellcheckComposerExtension.finalizeSessionBeforeSending(session)
|
||||
SpellcheckComposerExtension.finalizeSessionBeforeSending({session})
|
||||
expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML)
|
||||
|
||||
module.exports = SpellcheckComposerExtension
|
||||
|
|
|
@ -739,7 +739,7 @@ class ComposerView extends React.Component
|
|||
# Check third party warnings added via Composer extensions
|
||||
for extension in ExtensionRegistry.Composer.extensions()
|
||||
continue unless extension.warningsForSending
|
||||
warnings = warnings.concat(extension.warningsForSending(draft))
|
||||
warnings = warnings.concat(extension.warningsForSending({draft}))
|
||||
|
||||
if warnings.length > 0 and not options.force
|
||||
response = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
|
|
|
@ -3,7 +3,7 @@ AutoloadImagesStore = require './autoload-images-store'
|
|||
|
||||
class AutoloadImagesExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
@formatMessageBody: ({message}) ->
|
||||
if AutoloadImagesStore.shouldBlockImagesIn(message)
|
||||
message.body = message.body.replace AutoloadImagesStore.ImagesRegexp, (match, prefix, imageUrl) ->
|
||||
"#{prefix}#"
|
||||
|
|
|
@ -18,7 +18,7 @@ describe "AutoloadImagesExtension", ->
|
|||
spyOn(AutoloadImagesStore, 'shouldBlockImagesIn').andReturn(true)
|
||||
message =
|
||||
body: scenario.in
|
||||
AutoloadImagesExtension.formatMessageBody(message)
|
||||
AutoloadImagesExtension.formatMessageBody({message})
|
||||
expect(message.body == scenario.out).toBe(true)
|
||||
|
||||
module.exports = AutoloadImagesExtension
|
||||
|
|
|
@ -3,7 +3,7 @@ Autolinker = require 'autolinker'
|
|||
|
||||
class AutolinkerExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
@formatMessageBody: ({message}) ->
|
||||
# Apply the autolinker pass to make emails and links clickable
|
||||
message.body = Autolinker.link(message.body, {twitter: false})
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ TrackingBlacklist = [{
|
|||
|
||||
class TrackingPixelsExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
@formatMessageBody: ({message}) ->
|
||||
return unless message.isFromMe()
|
||||
|
||||
regex = RegExpUtils.imageTagRegex()
|
||||
|
|
|
@ -6,7 +6,7 @@ describe "AutolinkerExtension", ->
|
|||
spyOn(Autolinker, 'link').andCallFake (txt) => txt
|
||||
|
||||
it "should call through to Autolinker", ->
|
||||
AutolinkerExtension.formatMessageBody({body:'body'})
|
||||
AutolinkerExtension.formatMessageBody(message: {body:'body'})
|
||||
expect(Autolinker.link).toHaveBeenCalledWith('body', {twitter: false})
|
||||
|
||||
it "should add a title to everything with an href", ->
|
||||
|
@ -24,5 +24,5 @@ describe "AutolinkerExtension", ->
|
|||
<a href ='http://apple.com' title='http://apple.com' >hello world!</a>
|
||||
<a href ='mailto://' title='mailto://' >hello world!</a>
|
||||
"""
|
||||
AutolinkerExtension.formatMessageBody(message)
|
||||
AutolinkerExtension.formatMessageBody({message})
|
||||
expect(message.body).toEqual(expected.body)
|
||||
|
|
|
@ -22,10 +22,10 @@ describe "TrackingPixelsExtension", ->
|
|||
it "should splice tracking pixels and only run on messages by the current user", ->
|
||||
message = new Message(body: testBody)
|
||||
spyOn(message, 'isFromMe').andCallFake -> false
|
||||
TrackingPixelsExtension.formatMessageBody(message)
|
||||
TrackingPixelsExtension.formatMessageBody({message})
|
||||
expect(message.body).toEqual(testBody)
|
||||
|
||||
message = new Message(body: testBody)
|
||||
spyOn(message, 'isFromMe').andCallFake -> true
|
||||
TrackingPixelsExtension.formatMessageBody(message)
|
||||
TrackingPixelsExtension.formatMessageBody({message})
|
||||
expect(message.body).toEqual(testBodyProcessed)
|
||||
|
|
124
spec/extensions/composer-extension-adapter-spec.es6
Normal file
124
spec/extensions/composer-extension-adapter-spec.es6
Normal file
|
@ -0,0 +1,124 @@
|
|||
import * as adapter from '../../src/extensions/composer-extension-adapter';
|
||||
import {DOMUtils} from 'nylas-exports';
|
||||
|
||||
const selection = 'selection';
|
||||
const node = 'node';
|
||||
const event = 'event';
|
||||
const extra = 'extra';
|
||||
const editor = {
|
||||
rootNode: node,
|
||||
currentSelection() {
|
||||
return selection;
|
||||
},
|
||||
};
|
||||
|
||||
describe('ComposerExtensionAdapter', ()=> {
|
||||
describe('adaptOnInput', ()=> {
|
||||
it('adapts correctly if onContentChanged already defined', ()=> {
|
||||
const onInputSpy = jasmine.createSpy('onInput');
|
||||
const extension = {
|
||||
onContentChanged() {},
|
||||
onInput(ev, editableNode, sel) {
|
||||
onInputSpy(ev, editableNode, sel);
|
||||
},
|
||||
};
|
||||
adapter.adaptOnInput(extension);
|
||||
extension.onContentChanged({editor, mutations: []});
|
||||
expect(onInputSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adapts correctly when signature is (event, ...)', ()=> {
|
||||
const onInputSpy = jasmine.createSpy('onInput');
|
||||
const extension = {
|
||||
onInput(ev, editableNode, sel) {
|
||||
onInputSpy(ev, editableNode, sel);
|
||||
},
|
||||
};
|
||||
adapter.adaptOnInput(extension);
|
||||
expect(extension.onContentChanged).toBeDefined();
|
||||
extension.onContentChanged({editor, mutations: []});
|
||||
expect(onInputSpy).toHaveBeenCalledWith([], node, selection);
|
||||
});
|
||||
|
||||
it('adapts correctly when signature is (editableNode, selection, ...)', ()=> {
|
||||
const onInputSpy = jasmine.createSpy('onInput');
|
||||
const extension = {
|
||||
onInput(editableNode, sel, ev) {
|
||||
onInputSpy(editableNode, sel, ev);
|
||||
},
|
||||
};
|
||||
adapter.adaptOnInput(extension);
|
||||
expect(extension.onContentChanged).toBeDefined();
|
||||
extension.onContentChanged({editor, mutations: []});
|
||||
expect(onInputSpy).toHaveBeenCalledWith(node, selection, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adaptOnTabDown', ()=> {
|
||||
it('adapts onTabDown correctly', ()=> {
|
||||
const onTabDownSpy = jasmine.createSpy('onTabDownSpy');
|
||||
const mockEvent = {key: 'Tab'};
|
||||
const range = 'range';
|
||||
spyOn(DOMUtils, 'getRangeInScope').andReturn(range);
|
||||
const extension = {
|
||||
onTabDown(editableNode, rn, ev) {
|
||||
onTabDownSpy(editableNode, rn, ev);
|
||||
},
|
||||
};
|
||||
adapter.adaptOnTabDown(extension, 'method');
|
||||
expect(extension.onKeyDown).toBeDefined();
|
||||
extension.onKeyDown({editor, event: mockEvent});
|
||||
expect(onTabDownSpy).toHaveBeenCalledWith(node, range, mockEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adaptMethod', ()=> {
|
||||
it('adapts correctly when signature is (editor, ...)', ()=> {
|
||||
const methodSpy = jasmine.createSpy('methodSpy');
|
||||
const extension = {
|
||||
method(editor, ev, other) {
|
||||
methodSpy(editor, ev, other);
|
||||
},
|
||||
};
|
||||
adapter.adaptMethod(extension, 'method');
|
||||
extension.method({editor, event, extra});
|
||||
expect(methodSpy).toHaveBeenCalledWith(editor, event, extra);
|
||||
});
|
||||
|
||||
it('adapts correctly when signature is (event, ...)', ()=> {
|
||||
const methodSpy = jasmine.createSpy('methodSpy');
|
||||
const extension = {
|
||||
method(ev, editableNode, sel, other) {
|
||||
methodSpy(ev, editableNode, sel, other);
|
||||
},
|
||||
};
|
||||
adapter.adaptMethod(extension, 'method');
|
||||
extension.method({editor, event, extra});
|
||||
expect(methodSpy).toHaveBeenCalledWith(event, node, selection, extra);
|
||||
});
|
||||
|
||||
it('adapts correctly when signature is (editableNode, selection, ...)', ()=> {
|
||||
const methodSpy = jasmine.createSpy('methodSpy');
|
||||
const extension = {
|
||||
method(editableNode, sel, ev, other) {
|
||||
methodSpy(editableNode, sel, ev, other);
|
||||
},
|
||||
};
|
||||
adapter.adaptMethod(extension, 'method');
|
||||
extension.method({editor, event, extra});
|
||||
expect(methodSpy).toHaveBeenCalledWith(node, selection, event, extra);
|
||||
});
|
||||
|
||||
it('adapts correctly when using mutations instead of an event', ()=> {
|
||||
const methodSpy = jasmine.createSpy('methodSpy');
|
||||
const extension = {
|
||||
method(editor, mutations) {
|
||||
methodSpy(editor, mutations);
|
||||
},
|
||||
};
|
||||
adapter.adaptMethod(extension, 'method');
|
||||
extension.method({editor, mutations: []});
|
||||
expect(methodSpy).toHaveBeenCalledWith(editor, []);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -32,7 +32,7 @@ fakeMessageWithFiles = null
|
|||
msgWithReplyToDuplicates = null
|
||||
|
||||
class TestExtension extends ComposerExtension
|
||||
@prepareNewDraft: (draft) ->
|
||||
@prepareNewDraft: ({draft}) ->
|
||||
draft.body = "Edited by TestExtension!" + draft.body
|
||||
|
||||
describe "DraftStore", ->
|
||||
|
|
|
@ -81,7 +81,7 @@ class Contenteditable extends React.Component
|
|||
|
||||
Edits made within the editing function will eventually fire _onDOMMutated
|
||||
###
|
||||
atomicEdit: (editingFunction, extraArgs...) =>
|
||||
atomicEdit: (editingFunction, extraArgsObj={}) =>
|
||||
@_teardownListeners()
|
||||
|
||||
editor = new EditorAPI(@_editableNode())
|
||||
|
@ -89,14 +89,14 @@ class Contenteditable extends React.Component
|
|||
if not editor.currentSelection().isInScope()
|
||||
editor.importSelection(@innerState.exportedSelection)
|
||||
|
||||
args = [editor, extraArgs...]
|
||||
editingFunction.apply(null, args)
|
||||
argsObj = _.extend(extraArgsObj, {editor})
|
||||
editingFunction(argsObj)
|
||||
|
||||
@_setupListeners()
|
||||
|
||||
focus: => @_editableNode().focus()
|
||||
|
||||
selectEnd: => @atomicEdit (editor) -> editor.selectEnd()
|
||||
selectEnd: => @atomicEdit ({editor}) -> editor.selectEnd()
|
||||
|
||||
|
||||
########################################################################
|
||||
|
@ -202,7 +202,7 @@ class Contenteditable extends React.Component
|
|||
_keymapHandlers: ->
|
||||
atomicEditWrap = (command) =>
|
||||
(event) =>
|
||||
@atomicEdit(((editor) -> editor[command]()), event)
|
||||
@atomicEdit((({editor}) -> editor[command]()), event)
|
||||
|
||||
keymapHandlers = {
|
||||
'contenteditable:bold': atomicEditWrap("bold")
|
||||
|
@ -253,7 +253,7 @@ class Contenteditable extends React.Component
|
|||
@setInnerState dragging: false if @innerState.dragging
|
||||
@setInnerState doubleDown: false if @innerState.doubleDown
|
||||
|
||||
@_runCallbackOnExtensions("onContentChanged", mutations)
|
||||
@_runCallbackOnExtensions("onContentChanged", {mutations})
|
||||
|
||||
@_saveSelectionState()
|
||||
|
||||
|
@ -306,7 +306,7 @@ class Contenteditable extends React.Component
|
|||
|
||||
menu = new Menu()
|
||||
|
||||
@dispatchEventToExtensions("onShowContextMenu", event, menu)
|
||||
@dispatchEventToExtensions("onShowContextMenu", event, {menu})
|
||||
menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
|
||||
menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
|
||||
menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
|
||||
|
@ -320,9 +320,9 @@ class Contenteditable extends React.Component
|
|||
############################# Extensions ###############################
|
||||
########################################################################
|
||||
|
||||
_runCallbackOnExtensions: (method, args...) =>
|
||||
_runCallbackOnExtensions: (method, argsObj={}) =>
|
||||
for extension in @props.extensions.concat(@coreExtensions)
|
||||
@_runExtensionMethod(extension, method, args...)
|
||||
@_runExtensionMethod(extension, method, argsObj)
|
||||
|
||||
# Will execute the event handlers on each of the registerd and core
|
||||
# extensions In this context, event.preventDefault and
|
||||
|
@ -334,20 +334,21 @@ class Contenteditable extends React.Component
|
|||
# basically means preventing the core extension handlers from being
|
||||
# called. If any of the extensions calls event.stopPropagation(), it
|
||||
# will prevent any other extension handlers from being called.
|
||||
dispatchEventToExtensions: (method, event, args...) =>
|
||||
dispatchEventToExtensions: (method, event, args={}) =>
|
||||
argsObj = _.extend(args, {event})
|
||||
for extension in @props.extensions
|
||||
break if event?.isPropagationStopped()
|
||||
@_runExtensionMethod(extension, method, event, args...)
|
||||
@_runExtensionMethod(extension, method, argsObj)
|
||||
|
||||
return if event?.defaultPrevented or event?.isPropagationStopped()
|
||||
for extension in @coreExtensions
|
||||
break if event?.isPropagationStopped()
|
||||
@_runExtensionMethod(extension, method, event, args...)
|
||||
@_runExtensionMethod(extension, method, argsObj)
|
||||
|
||||
_runExtensionMethod: (extension, method, args...) =>
|
||||
_runExtensionMethod: (extension, method, argsObj={}) =>
|
||||
return if not extension[method]?
|
||||
editingFunction = extension[method].bind(extension)
|
||||
@atomicEdit(editingFunction, args...)
|
||||
@atomicEdit(editingFunction, argsObj)
|
||||
|
||||
|
||||
########################################################################
|
||||
|
|
|
@ -9,7 +9,7 @@ class DOMNormalizer extends ContenteditableExtension
|
|||
# structures, a simple replacement of the DOM is not easy. There are a
|
||||
# variety of edge cases that we need to correct for and prepare both the
|
||||
# HTML and the selection to be serialized without error.
|
||||
@onContentChanged: (editor, mutations) ->
|
||||
@onContentChanged: ({editor, mutations}) ->
|
||||
@_cleanHTML(editor)
|
||||
@_cleanSelection(editor)
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ class FloatingToolbarContainer extends React.Component
|
|||
onDoneWithLink={@_onDoneWithLink} />
|
||||
|
||||
_onSaveUrl: (url, linkToModify) =>
|
||||
@props.atomicEdit (editor) ->
|
||||
@props.atomicEdit ({editor}) ->
|
||||
if linkToModify?
|
||||
equivalentNode = DOMUtils.findSimilarNodes(editor.rootNode, linkToModify)?[0]
|
||||
return unless equivalentNode?
|
||||
|
@ -118,8 +118,9 @@ class FloatingToolbarContainer extends React.Component
|
|||
# core actions and user-defined plugins. The FloatingToolbar simply
|
||||
# renders them.
|
||||
_toolbarButtonConfigs: ->
|
||||
atomicEditWrap = (command) => (event) =>
|
||||
@props.atomicEdit(((editor) -> editor[command]()), event)
|
||||
atomicEditWrap = (command) =>
|
||||
(event) =>
|
||||
@props.atomicEdit((({editor}) -> editor[command]()), event)
|
||||
|
||||
extensionButtonConfigs = []
|
||||
ExtensionRegistry.Composer.extensions().forEach (ext) ->
|
||||
|
|
|
@ -2,13 +2,13 @@ _str = require 'underscore.string'
|
|||
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
||||
|
||||
class ListManager extends ContenteditableExtension
|
||||
@onContentChanged: (editor, mutations) ->
|
||||
@onContentChanged: ({editor, mutations}) ->
|
||||
if @_spaceEntered and @hasListStartSignature(editor.currentSelection())
|
||||
@createList(editor)
|
||||
|
||||
@_collapseAdjacentLists(editor)
|
||||
|
||||
@onKeyDown: (editor, event) ->
|
||||
@onKeyDown: ({editor, event}) ->
|
||||
@_spaceEntered = event.key is " "
|
||||
if DOMUtils.isInList()
|
||||
if event.key is "Backspace" and DOMUtils.atStartOfList()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
||||
|
||||
class TabManager extends ContenteditableExtension
|
||||
@onKeyDown: (editor, event) ->
|
||||
@onKeyDown: ({editor, event}) ->
|
||||
# This is a special case where we don't want to bubble up the event to
|
||||
# the keymap manager if the extension prevented the default behavior
|
||||
if event.defaultPrevented
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
import _ from 'underscore';
|
||||
import {Listener, Publisher} from './flux/modules/reflux-coffee';
|
||||
import {includeModule} from './flux/coffee-helpers';
|
||||
import composerExtAdapter from './extensions/composer-extension-adapter';
|
||||
import messageViewExtAdapter from './extensions/message-view-extension-adapter';
|
||||
|
||||
export class Registry {
|
||||
|
||||
|
@ -55,9 +56,10 @@ Registry.include(Listener);
|
|||
|
||||
export const Composer = new Registry(
|
||||
'Composer',
|
||||
require('./extensions/composer-extension-adapter')
|
||||
composerExtAdapter
|
||||
);
|
||||
|
||||
export const MessageView = new Registry(
|
||||
'MessageView',
|
||||
messageViewExtAdapter
|
||||
);
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
{deprecate} = require '../deprecate-utils'
|
||||
DOMUtils = require '../dom-utils'
|
||||
|
||||
ComposerExtensionAdapter = (extension) ->
|
||||
|
||||
if extension.onInput?
|
||||
origInput = extension.onInput
|
||||
extension.onContentChanged = (editor, mutations) ->
|
||||
origInput(editor.rootNode)
|
||||
|
||||
extension.onInput = deprecate(
|
||||
"DraftStoreExtension.onInput",
|
||||
"ComposerExtension.onContentChanged",
|
||||
extension,
|
||||
extension.onContentChanged
|
||||
)
|
||||
|
||||
if extension.onTabDown?
|
||||
origKeyDown = extension.onKeyDown
|
||||
extension.onKeyDown = (editor, event) ->
|
||||
if event.key is "Tab"
|
||||
range = DOMUtils.getRangeInScope(editor.rootNode)
|
||||
extension.onTabDown(editor.rootNode, range, event)
|
||||
else
|
||||
origKeyDown?(event, editor.rootNode, editor.currentSelection())
|
||||
|
||||
extension.onKeyDown = deprecate(
|
||||
"DraftStoreExtension.onTabDown",
|
||||
"ComposerExtension.onKeyDown",
|
||||
extension,
|
||||
extension.onKeyDown
|
||||
)
|
||||
|
||||
if extension.onMouseUp?
|
||||
origOnClick = extension.onClick
|
||||
extension.onClick = (editor, event) ->
|
||||
range = DOMUtils.getRangeInScope(editor.rootNode)
|
||||
extension.onMouseUp(editor.rootNode, range, event)
|
||||
origOnClick?(event, editor.rootNode, editor.currentSelection())
|
||||
|
||||
extension.onClick = deprecate(
|
||||
"DraftStoreExtension.onMouseUp",
|
||||
"ComposerExtension.onClick",
|
||||
extension,
|
||||
extension.onClick
|
||||
)
|
||||
|
||||
return extension
|
||||
|
||||
module.exports = ComposerExtensionAdapter
|
114
src/extensions/composer-extension-adapter.es6
Normal file
114
src/extensions/composer-extension-adapter.es6
Normal file
|
@ -0,0 +1,114 @@
|
|||
import _ from 'underscore';
|
||||
import DOMUtils from '../dom-utils';
|
||||
import {deprecate} from '../deprecate-utils';
|
||||
import {getFunctionArgs} from './extension-utils';
|
||||
|
||||
export function isUsingOutdatedAPI(func) {
|
||||
// Might not always be true, but it is our best guess
|
||||
const firstArg = getFunctionArgs(func)[0];
|
||||
if (func.length > 1) return true; // Not using a named arguments hash
|
||||
return (
|
||||
firstArg.includes('ev') ||
|
||||
firstArg.includes('node') ||
|
||||
firstArg.includes('Node')
|
||||
);
|
||||
}
|
||||
|
||||
export function adaptMethod(extension, method, original = extension[method]) {
|
||||
// Check if it is using old API
|
||||
if (!original || !isUsingOutdatedAPI(original)) return;
|
||||
|
||||
let deprecatedArgs = '';
|
||||
extension[method] = (argsObj)=> {
|
||||
const {editor, event, mutations} = argsObj;
|
||||
const eventOrMutations = event || mutations || {};
|
||||
const extraArgs = _.keys(_.omit(argsObj, ['editor', 'event', 'mutations'])).map(
|
||||
key => argsObj[key]
|
||||
);
|
||||
|
||||
// This is our best guess at the function signature that is being used
|
||||
const firstArg = getFunctionArgs(original)[0];
|
||||
if (firstArg.includes('editor')) {
|
||||
deprecatedArgs = '(editor, ...)';
|
||||
original(editor, eventOrMutations, ...extraArgs);
|
||||
} else if (firstArg.includes('ev')) {
|
||||
deprecatedArgs = '(event, editableNode, selection, ...)';
|
||||
original(eventOrMutations, editor.rootNode, editor.currentSelection(), ...extraArgs);
|
||||
} else {
|
||||
deprecatedArgs = '(editableNode, selection, ...)';
|
||||
original(editor.rootNode, editor.currentSelection(), eventOrMutations, ...extraArgs);
|
||||
}
|
||||
};
|
||||
|
||||
extension[method] = deprecate(
|
||||
`ComposerExtension.${method}${deprecatedArgs}`,
|
||||
`ComposerExtension.${method}(args = {editor, ...})`,
|
||||
extension,
|
||||
extension[method]
|
||||
);
|
||||
}
|
||||
|
||||
export function adaptOnInput(extension) {
|
||||
if (extension.onContentChanged != null) return;
|
||||
adaptMethod(extension, 'onContentChanged', extension.onInput);
|
||||
}
|
||||
|
||||
export function adaptOnTabDown(extension) {
|
||||
if (!extension.onTabDown) return;
|
||||
const origOnKeyDown = extension.onKeyDown;
|
||||
extension.onKeyDown = ({editor, event})=> {
|
||||
if (event.key === 'Tab') {
|
||||
const range = DOMUtils.getRangeInScope(editor.rootNode);
|
||||
extension.onTabDown(editor.rootNode, range, event);
|
||||
} else {
|
||||
// At this point, onKeyDown should have already been adapted
|
||||
if (origOnKeyDown != null) origOnKeyDown(editor, event);
|
||||
}
|
||||
};
|
||||
|
||||
extension.onKeyDown = deprecate(
|
||||
'DraftStoreExtension.onTabDown',
|
||||
'ComposerExtension.onKeyDown',
|
||||
extension,
|
||||
extension.onKeyDown
|
||||
);
|
||||
}
|
||||
|
||||
export function adaptOnMouseUp(extension) {
|
||||
if (!extension.onMouseUp) return;
|
||||
const origOnClick = extension.onClick;
|
||||
extension.onClick = ({editor, event})=> {
|
||||
const range = DOMUtils.getRangeInScope(editor.rootNode);
|
||||
extension.onMouseUp(editor.rootNode, range, event);
|
||||
// At this point, onClick should have already been adapted
|
||||
if (origOnClick != null) origOnClick(editor, event);
|
||||
};
|
||||
|
||||
extension.onClick = deprecate(
|
||||
'DraftStoreExtension.onMouseUp',
|
||||
'ComposerExtension.onClick',
|
||||
extension,
|
||||
extension.onClick
|
||||
);
|
||||
}
|
||||
|
||||
export default function adaptExtension(extension) {
|
||||
const standardMethods = [
|
||||
'onContentChanged',
|
||||
'onBlur',
|
||||
'onFocus',
|
||||
'onClick',
|
||||
'onKeyDown',
|
||||
'onShowContextMenu',
|
||||
];
|
||||
standardMethods.forEach(
|
||||
method => adaptMethod(extension, method)
|
||||
);
|
||||
|
||||
// Special cases
|
||||
adaptOnInput(extension);
|
||||
adaptOnTabDown(extension);
|
||||
adaptOnMouseUp(extension);
|
||||
|
||||
return extension;
|
||||
}
|
|
@ -47,7 +47,7 @@ class ComposerExtension extends ContenteditableExtension
|
|||
Returns a list of warning strings, or an empty array if no warnings need
|
||||
to be displayed.
|
||||
###
|
||||
@warningsForSending: (draft) ->
|
||||
@warningsForSending: ({draft}) ->
|
||||
[]
|
||||
|
||||
###
|
||||
|
@ -87,7 +87,7 @@ class ComposerExtension extends ContenteditableExtension
|
|||
valuable way, you should set `draft.pristine = false` so the draft
|
||||
saves, even if no further changes are made.
|
||||
###
|
||||
@prepareNewDraft: (draft) ->
|
||||
@prepareNewDraft: ({draft}) ->
|
||||
return
|
||||
|
||||
###
|
||||
|
@ -103,14 +103,14 @@ class ComposerExtension extends ContenteditableExtension
|
|||
|
||||
```coffee
|
||||
# Remove any <code> tags found in the draft body
|
||||
finalizeSessionBeforeSending: (session) ->
|
||||
finalizeSessionBeforeSending: ({session}) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
```
|
||||
###
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
@finalizeSessionBeforeSending: ({session}) ->
|
||||
return
|
||||
|
||||
module.exports = ComposerExtension
|
||||
|
|
|
@ -64,7 +64,7 @@ class ContenteditableExtension
|
|||
reflect that it is no longer empty.
|
||||
|
||||
```coffee
|
||||
onContentChanged: (editor, mutations) ->
|
||||
onContentChanged: ({editor, mutations}) ->
|
||||
isWithinNode = (node) ->
|
||||
test = selection.baseNode
|
||||
while test isnt editableNode
|
||||
|
@ -78,9 +78,9 @@ class ContenteditableExtension
|
|||
codeTag.classList.remove('empty')
|
||||
```
|
||||
###
|
||||
@onContentChanged: (editor, mutations) ->
|
||||
@onContentChanged: ({editor, mutations}) ->
|
||||
|
||||
@onContentStoppedChanging: (editor, mutations) ->
|
||||
@onContentStoppedChanging: ({editor, mutations}) ->
|
||||
|
||||
###
|
||||
Public: Override onBlur to mutate the contenteditable DOM node whenever the
|
||||
|
@ -91,7 +91,7 @@ class ContenteditableExtension
|
|||
methods for manipulating the selection and DOM
|
||||
- event: DOM event fired on the contenteditable
|
||||
###
|
||||
@onBlur: (editor, event) ->
|
||||
@onBlur: ({editor, event}) ->
|
||||
|
||||
###
|
||||
Public: Override onFocus to mutate the contenteditable DOM node whenever the
|
||||
|
@ -102,7 +102,7 @@ class ContenteditableExtension
|
|||
methods for manipulating the selection and DOM
|
||||
- event: DOM event fired on the contenteditable
|
||||
###
|
||||
@onFocus: (editor, event) ->
|
||||
@onFocus: ({editor, event}) ->
|
||||
|
||||
###
|
||||
Public: Override onClick to mutate the contenteditable DOM node whenever the
|
||||
|
@ -113,7 +113,7 @@ class ContenteditableExtension
|
|||
methods for manipulating the selection and DOM
|
||||
- event: DOM event fired on the contenteditable
|
||||
###
|
||||
@onClick: (editor, event) ->
|
||||
@onClick: ({editor, event}) ->
|
||||
|
||||
###
|
||||
Public: Override onKeyDown to mutate the contenteditable DOM node whenever the
|
||||
|
@ -133,12 +133,11 @@ class ContenteditableExtension
|
|||
methods for manipulating the selection and DOM
|
||||
- event: DOM event fired on the contenteditable
|
||||
###
|
||||
@onKeyDown: (editor, event) ->
|
||||
@onKeyDown: ({editor, event}) ->
|
||||
|
||||
###
|
||||
Public: Override onInput to mutate the contenteditable DOM node whenever the
|
||||
onInput event is fired on it.You may mutate the contenteditable in place, we
|
||||
not expect any return value from this method.
|
||||
Public: Override onShowContextMenu to add new menu items to the right click menu
|
||||
inside the contenteditable.
|
||||
|
||||
- editor: The {Editor} controller that provides a host of convenience
|
||||
methods for manipulating the selection and DOM
|
||||
|
@ -147,6 +146,6 @@ class ContenteditableExtension
|
|||
object you can mutate in order to add new [MenuItems](https://github.com/atom/electron/blob/master/docs/api/menu-item.md)
|
||||
to the context menu that will be displayed when you right click the contenteditable.
|
||||
###
|
||||
@onShowContextMenu: (editor, event, menu) ->
|
||||
@onShowContextMenu: ({editor, event, menu}) ->
|
||||
|
||||
module.exports = ContenteditableExtension
|
||||
|
|
7
src/extensions/extension-utils.es6
Normal file
7
src/extensions/extension-utils.es6
Normal file
|
@ -0,0 +1,7 @@
|
|||
import RegExpUtils from '../regexp-utils';
|
||||
|
||||
export function getFunctionArgs(func) {
|
||||
const match = func.toString().match(RegExpUtils.functionArgs());
|
||||
if (!match) return null;
|
||||
return match[1].split(/\s*,\s*/);
|
||||
}
|
21
src/extensions/message-view-extension-adapter.es6
Normal file
21
src/extensions/message-view-extension-adapter.es6
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {getFunctionArgs} from './extension-utils';
|
||||
|
||||
export function isUsingOutdatedAPI(func) {
|
||||
// Might not always be true, but it is our best guess
|
||||
const firstArg = getFunctionArgs(func)[0];
|
||||
return (
|
||||
firstArg.includes('mes') ||
|
||||
firstArg.includes('msg') ||
|
||||
firstArg.includes('body') ||
|
||||
firstArg.includes('draft')
|
||||
);
|
||||
}
|
||||
|
||||
export default function adaptExtension(extension) {
|
||||
const original = extension.formatMessageBody;
|
||||
if (!original || !isUsingOutdatedAPI(original)) return extension;
|
||||
extension.formatMessageBody = ({message})=> {
|
||||
original(message);
|
||||
};
|
||||
return extension;
|
||||
}
|
|
@ -28,7 +28,7 @@ class MessageViewExtension
|
|||
Public: Transform the message body HTML provided in `body` and return HTML
|
||||
that should be displayed for the message.
|
||||
###
|
||||
@formatMessageBody: (body) ->
|
||||
@formatMessageBody: ({message}) ->
|
||||
return body
|
||||
|
||||
module.exports = MessageViewExtension
|
||||
|
|
|
@ -238,7 +238,7 @@ class DraftStore
|
|||
# Give extensions an opportunity to perform additional setup to the draft
|
||||
for extension in @extensions()
|
||||
continue unless extension.prepareNewDraft
|
||||
extension.prepareNewDraft(draft)
|
||||
extension.prepareNewDraft({draft})
|
||||
|
||||
# Optimistically create a draft session and hand it the draft so that it
|
||||
# doesn't need to do a query for it a second from now when the composer wants it.
|
||||
|
@ -535,7 +535,7 @@ class DraftStore
|
|||
_runExtensionsBeforeSend: (session) ->
|
||||
for extension in @extensions()
|
||||
continue unless extension.finalizeSessionBeforeSending
|
||||
extension.finalizeSessionBeforeSending(session)
|
||||
extension.finalizeSessionBeforeSending({session})
|
||||
|
||||
_onRemoveFile: ({file, messageClientId}) =>
|
||||
@sessionForClientId(messageClientId).then (session) ->
|
||||
|
|
|
@ -51,7 +51,7 @@ class MessageBodyProcessor
|
|||
continue unless extension.formatMessageBody
|
||||
virtual = message.clone()
|
||||
virtual.body = body
|
||||
extension.formatMessageBody(virtual)
|
||||
extension.formatMessageBody({message: virtual})
|
||||
body = virtual.body
|
||||
|
||||
# Find inline images and give them a calculated CSS height based on
|
||||
|
|
|
@ -29,4 +29,8 @@ RegExpUtils =
|
|||
|
||||
looseStyleTag: -> /<style/gim
|
||||
|
||||
# Regular expression matching javasript function arguments:
|
||||
# https://regex101.com/r/pZ6zF0/1
|
||||
functionArgs: -> /\(\s*([^)]+?)\s*\)/
|
||||
|
||||
module.exports = RegExpUtils
|
||||
|
|
Loading…
Add table
Reference in a new issue