mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-14 00:24:33 +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"],
|
"eqeqeq": [2, "smart"],
|
||||||
"id-length": [0],
|
"id-length": [0],
|
||||||
"no-loop-func": [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
|
class ProductsExtension extends ComposerExtension
|
||||||
|
|
||||||
@warningsForSending: (draft) ->
|
@warningsForSending: ({draft}) ->
|
||||||
words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']
|
words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']
|
||||||
body = draft.body.toLowercase()
|
body = draft.body.toLowercase()
|
||||||
for word in words
|
for word in words
|
||||||
|
@ -35,9 +35,9 @@ class ProductsExtension extends ComposerExtension
|
||||||
return ["with the word '#{word}'?"]
|
return ["with the word '#{word}'?"]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@finalizeSessionBeforeSending: (session) ->
|
@finalizeSessionBeforeSending: ({session}) ->
|
||||||
draft = session.draft()
|
draft = session.draft()
|
||||||
if @warningsForSending(draft)
|
if @warningsForSending({draft})
|
||||||
bodyWithWarning = draft.body += "<br>This email \
|
bodyWithWarning = draft.body += "<br>This email \
|
||||||
contains competitor's product names \
|
contains competitor's product names \
|
||||||
or trademarks used in context."
|
or trademarks used in context."
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {DOMUtils, ComposerExtension} from 'nylas-exports';
|
||||||
|
|
||||||
class TemplatesComposerExtension extends ComposerExtension {
|
class TemplatesComposerExtension extends ComposerExtension {
|
||||||
|
|
||||||
static warningsForSending(draft) {
|
static warningsForSending({draft}) {
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
if (draft.body.search(/<code[^>]*empty[^>]*>/i) > 0) {
|
if (draft.body.search(/<code[^>]*empty[^>]*>/i) > 0) {
|
||||||
warnings.push('with an empty template area');
|
warnings.push('with an empty template area');
|
||||||
|
@ -10,7 +10,7 @@ class TemplatesComposerExtension extends ComposerExtension {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
static finalizeSessionBeforeSending(session) {
|
static finalizeSessionBeforeSending({session}) {
|
||||||
const body = session.draft().body;
|
const body = session.draft().body;
|
||||||
const clean = body.replace(/<\/?code[^>]*>/g, '');
|
const clean = body.replace(/<\/?code[^>]*>/g, '');
|
||||||
if (body !== clean) {
|
if (body !== clean) {
|
||||||
|
@ -18,32 +18,33 @@ class TemplatesComposerExtension extends ComposerExtension {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static onClick(editor, event) {
|
static onClick({editor, event}) {
|
||||||
var node = event.target;
|
const node = event.target;
|
||||||
if(node.nodeName === "CODE" && node.classList.contains("var") && node.classList.contains("empty")) {
|
if (node.nodeName === 'CODE' && node.classList.contains('var') && node.classList.contains('empty')) {
|
||||||
editor.selectAllChildren(node)
|
editor.selectAllChildren(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static onKeyDown(editor, event) {
|
static onKeyDown({editor, event}) {
|
||||||
const editableNode = editor.rootNode;
|
const editableNode = editor.rootNode;
|
||||||
if (event.key === 'Tab') {
|
if (event.key === 'Tab') {
|
||||||
const nodes = editableNode.querySelectorAll('code.var');
|
const nodes = editableNode.querySelectorAll('code.var');
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
let sel = editor.currentSelection();
|
const sel = editor.currentSelection();
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
// First, try to find a <code> that the selection is within. If found,
|
// 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
|
// select the next/prev node if the selection ends at the end of the
|
||||||
// <code>'s text, otherwise select the <code>'s contents.
|
// <code>'s text, otherwise select the <code>'s contents.
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
let node = nodes[i];
|
const node = nodes[i];
|
||||||
if (DOMUtils.selectionIsWithin(node)) {
|
if (DOMUtils.selectionIsWithin(node)) {
|
||||||
let selIndex = editor.getSelectionTextIndex(node);
|
const selIndex = editor.getSelectionTextIndex(node);
|
||||||
let length = DOMUtils.getIndexedTextContent(node).slice(-1)[0].end;
|
const length = DOMUtils.getIndexedTextContent(node).slice(-1)[0].end;
|
||||||
let nextIndex = i;
|
let nextIndex = i;
|
||||||
if(selIndex.endIndex === length)
|
if (selIndex.endIndex === length) {
|
||||||
nextIndex = event.shiftKey ? i - 1 : i + 1;
|
nextIndex = event.shiftKey ? i - 1 : i + 1;
|
||||||
|
}
|
||||||
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
||||||
sel.selectAllChildren(nodes[nextIndex]);
|
sel.selectAllChildren(nodes[nextIndex]);
|
||||||
found = true;
|
found = true;
|
||||||
|
@ -54,13 +55,13 @@ class TemplatesComposerExtension extends ComposerExtension {
|
||||||
// If we failed to find a <code> that the selection is within, select the
|
// If we failed to find a <code> that the selection is within, select the
|
||||||
// nearest <code> before/after the selection (depending on shift).
|
// nearest <code> before/after the selection (depending on shift).
|
||||||
if (!found) {
|
if (!found) {
|
||||||
let treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);
|
const treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);
|
||||||
let curIndex = 0, nextIndex = null;
|
let curIndex = 0;
|
||||||
|
let nextIndex = null;
|
||||||
|
let node;
|
||||||
while (node = treeWalker.nextNode()) {
|
while (node = treeWalker.nextNode()) {
|
||||||
if(sel.anchorNode === node || sel.focusNode === node)
|
if (sel.anchorNode === node || sel.focusNode === node) break;
|
||||||
break;
|
if (node.nodeName === 'CODE' && node.classList.contains('var')) curIndex++;
|
||||||
if(node.nodeName === "CODE" && node.classList.contains("var"))
|
|
||||||
curIndex++
|
|
||||||
}
|
}
|
||||||
nextIndex = event.shiftKey ? curIndex - 1 : curIndex;
|
nextIndex = event.shiftKey ? curIndex - 1 : curIndex;
|
||||||
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions
|
||||||
|
@ -70,8 +71,7 @@ class TemplatesComposerExtension extends ComposerExtension {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
} else if (event.key === 'Enter') {
|
||||||
else if(event.key === 'Enter') {
|
|
||||||
const nodes = editableNode.querySelectorAll('code.var');
|
const nodes = editableNode.querySelectorAll('code.var');
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
if (DOMUtils.selectionStartsOrEndsIn(nodes[i])) {
|
if (DOMUtils.selectionStartsOrEndsIn(nodes[i])) {
|
||||||
|
@ -83,9 +83,9 @@ class TemplatesComposerExtension extends ComposerExtension {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static onContentChanged(editor) {
|
static onContentChanged({editor}) {
|
||||||
editableNode = editor.rootNode;
|
const editableNode = editor.rootNode;
|
||||||
selection = editor.currentSelection().rawSelection;
|
const selection = editor.currentSelection().rawSelection;
|
||||||
const isWithinNode = (node)=> {
|
const isWithinNode = (node)=> {
|
||||||
let test = selection.baseNode;
|
let test = selection.baseNode;
|
||||||
while (test !== editableNode) {
|
while (test !== editableNode) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ class AvailabilityComposerExtension extends ComposerExtension
|
||||||
# When subclassing the ComposerExtension, you can add your own custom logic
|
# When subclassing the ComposerExtension, you can add your own custom logic
|
||||||
# to execute before a draft is sent in the @finalizeSessionBeforeSending
|
# to execute before a draft is sent in the @finalizeSessionBeforeSending
|
||||||
# method. Here, we're registering the events before we send the draft.
|
# method. Here, we're registering the events before we send the draft.
|
||||||
@finalizeSessionBeforeSending: (session) ->
|
@finalizeSessionBeforeSending: ({session}) ->
|
||||||
body = session.draft().body
|
body = session.draft().body
|
||||||
participants = session.draft().participants()
|
participants = session.draft().participants()
|
||||||
sender = session.draft().from
|
sender = session.draft().from
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ComposerExtension, AccountStore} = require 'nylas-exports'
|
{ComposerExtension, AccountStore} = require 'nylas-exports'
|
||||||
|
|
||||||
class SignatureComposerExtension extends ComposerExtension
|
class SignatureComposerExtension extends ComposerExtension
|
||||||
@prepareNewDraft: (draft) ->
|
@prepareNewDraft: ({draft}) ->
|
||||||
accountId = AccountStore.current().id
|
accountId = AccountStore.current().id
|
||||||
signature = NylasEnv.config.get("nylas.account-#{accountId}.signature")
|
signature = NylasEnv.config.get("nylas.account-#{accountId}.signature")
|
||||||
return unless signature
|
return unless signature
|
||||||
|
|
|
@ -18,9 +18,9 @@ describe "SignatureComposerExtension", ->
|
||||||
draft: true
|
draft: true
|
||||||
body: 'This is a another test.'
|
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>')
|
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>')
|
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", ->
|
describe "when a signature is not defined", ->
|
||||||
|
@ -32,5 +32,5 @@ describe "SignatureComposerExtension", ->
|
||||||
a = new Message
|
a = new Message
|
||||||
draft: true
|
draft: true
|
||||||
body: 'This is a test! <blockquote>Hello world</blockquote>'
|
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>')
|
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] ?= spellchecker.isMisspelled(word)
|
||||||
SpellcheckCache[word]
|
SpellcheckCache[word]
|
||||||
|
|
||||||
@onInput: (editableNode) =>
|
@onContentChanged: ({editor}) =>
|
||||||
@walkTree(editableNode)
|
@walkTree(editor.rootNode)
|
||||||
|
|
||||||
@onShowContextMenu: (editor, event, menu) =>
|
@onShowContextMenu: ({editor, event, menu}) =>
|
||||||
selection = editor.currentSelection()
|
selection = editor.currentSelection()
|
||||||
editableNode = editor.rootNode
|
editableNode = editor.rootNode
|
||||||
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
|
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
|
||||||
|
@ -119,7 +119,7 @@ class SpellcheckComposerExtension extends ComposerExtension
|
||||||
if selectionImpacted
|
if selectionImpacted
|
||||||
selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset)
|
selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset)
|
||||||
|
|
||||||
@finalizeSessionBeforeSending: (session) ->
|
@finalizeSessionBeforeSending: ({session}) ->
|
||||||
body = session.draft().body
|
body = session.draft().body
|
||||||
clean = body.replace(/<\/?spelling[^>]*>/g, '')
|
clean = body.replace(/<\/?spelling[^>]*>/g, '')
|
||||||
if body != clean
|
if body != clean
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe "SpellcheckComposerExtension", ->
|
||||||
changes:
|
changes:
|
||||||
add: jasmine.createSpy('add')
|
add: jasmine.createSpy('add')
|
||||||
|
|
||||||
SpellcheckComposerExtension.finalizeSessionBeforeSending(session)
|
SpellcheckComposerExtension.finalizeSessionBeforeSending({session})
|
||||||
expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML)
|
expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML)
|
||||||
|
|
||||||
module.exports = SpellcheckComposerExtension
|
module.exports = SpellcheckComposerExtension
|
||||||
|
|
|
@ -739,7 +739,7 @@ class ComposerView extends React.Component
|
||||||
# Check third party warnings added via Composer extensions
|
# Check third party warnings added via Composer extensions
|
||||||
for extension in ExtensionRegistry.Composer.extensions()
|
for extension in ExtensionRegistry.Composer.extensions()
|
||||||
continue unless extension.warningsForSending
|
continue unless extension.warningsForSending
|
||||||
warnings = warnings.concat(extension.warningsForSending(draft))
|
warnings = warnings.concat(extension.warningsForSending({draft}))
|
||||||
|
|
||||||
if warnings.length > 0 and not options.force
|
if warnings.length > 0 and not options.force
|
||||||
response = dialog.showMessageBox(remote.getCurrentWindow(), {
|
response = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
|
|
@ -3,7 +3,7 @@ AutoloadImagesStore = require './autoload-images-store'
|
||||||
|
|
||||||
class AutoloadImagesExtension extends MessageViewExtension
|
class AutoloadImagesExtension extends MessageViewExtension
|
||||||
|
|
||||||
@formatMessageBody: (message) ->
|
@formatMessageBody: ({message}) ->
|
||||||
if AutoloadImagesStore.shouldBlockImagesIn(message)
|
if AutoloadImagesStore.shouldBlockImagesIn(message)
|
||||||
message.body = message.body.replace AutoloadImagesStore.ImagesRegexp, (match, prefix, imageUrl) ->
|
message.body = message.body.replace AutoloadImagesStore.ImagesRegexp, (match, prefix, imageUrl) ->
|
||||||
"#{prefix}#"
|
"#{prefix}#"
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe "AutoloadImagesExtension", ->
|
||||||
spyOn(AutoloadImagesStore, 'shouldBlockImagesIn').andReturn(true)
|
spyOn(AutoloadImagesStore, 'shouldBlockImagesIn').andReturn(true)
|
||||||
message =
|
message =
|
||||||
body: scenario.in
|
body: scenario.in
|
||||||
AutoloadImagesExtension.formatMessageBody(message)
|
AutoloadImagesExtension.formatMessageBody({message})
|
||||||
expect(message.body == scenario.out).toBe(true)
|
expect(message.body == scenario.out).toBe(true)
|
||||||
|
|
||||||
module.exports = AutoloadImagesExtension
|
module.exports = AutoloadImagesExtension
|
||||||
|
|
|
@ -3,7 +3,7 @@ Autolinker = require 'autolinker'
|
||||||
|
|
||||||
class AutolinkerExtension extends MessageViewExtension
|
class AutolinkerExtension extends MessageViewExtension
|
||||||
|
|
||||||
@formatMessageBody: (message) ->
|
@formatMessageBody: ({message}) ->
|
||||||
# Apply the autolinker pass to make emails and links clickable
|
# Apply the autolinker pass to make emails and links clickable
|
||||||
message.body = Autolinker.link(message.body, {twitter: false})
|
message.body = Autolinker.link(message.body, {twitter: false})
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ TrackingBlacklist = [{
|
||||||
|
|
||||||
class TrackingPixelsExtension extends MessageViewExtension
|
class TrackingPixelsExtension extends MessageViewExtension
|
||||||
|
|
||||||
@formatMessageBody: (message) ->
|
@formatMessageBody: ({message}) ->
|
||||||
return unless message.isFromMe()
|
return unless message.isFromMe()
|
||||||
|
|
||||||
regex = RegExpUtils.imageTagRegex()
|
regex = RegExpUtils.imageTagRegex()
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe "AutolinkerExtension", ->
|
||||||
spyOn(Autolinker, 'link').andCallFake (txt) => txt
|
spyOn(Autolinker, 'link').andCallFake (txt) => txt
|
||||||
|
|
||||||
it "should call through to Autolinker", ->
|
it "should call through to Autolinker", ->
|
||||||
AutolinkerExtension.formatMessageBody({body:'body'})
|
AutolinkerExtension.formatMessageBody(message: {body:'body'})
|
||||||
expect(Autolinker.link).toHaveBeenCalledWith('body', {twitter: false})
|
expect(Autolinker.link).toHaveBeenCalledWith('body', {twitter: false})
|
||||||
|
|
||||||
it "should add a title to everything with an href", ->
|
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 ='http://apple.com' title='http://apple.com' >hello world!</a>
|
||||||
<a href ='mailto://' title='mailto://' >hello world!</a>
|
<a href ='mailto://' title='mailto://' >hello world!</a>
|
||||||
"""
|
"""
|
||||||
AutolinkerExtension.formatMessageBody(message)
|
AutolinkerExtension.formatMessageBody({message})
|
||||||
expect(message.body).toEqual(expected.body)
|
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", ->
|
it "should splice tracking pixels and only run on messages by the current user", ->
|
||||||
message = new Message(body: testBody)
|
message = new Message(body: testBody)
|
||||||
spyOn(message, 'isFromMe').andCallFake -> false
|
spyOn(message, 'isFromMe').andCallFake -> false
|
||||||
TrackingPixelsExtension.formatMessageBody(message)
|
TrackingPixelsExtension.formatMessageBody({message})
|
||||||
expect(message.body).toEqual(testBody)
|
expect(message.body).toEqual(testBody)
|
||||||
|
|
||||||
message = new Message(body: testBody)
|
message = new Message(body: testBody)
|
||||||
spyOn(message, 'isFromMe').andCallFake -> true
|
spyOn(message, 'isFromMe').andCallFake -> true
|
||||||
TrackingPixelsExtension.formatMessageBody(message)
|
TrackingPixelsExtension.formatMessageBody({message})
|
||||||
expect(message.body).toEqual(testBodyProcessed)
|
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
|
msgWithReplyToDuplicates = null
|
||||||
|
|
||||||
class TestExtension extends ComposerExtension
|
class TestExtension extends ComposerExtension
|
||||||
@prepareNewDraft: (draft) ->
|
@prepareNewDraft: ({draft}) ->
|
||||||
draft.body = "Edited by TestExtension!" + draft.body
|
draft.body = "Edited by TestExtension!" + draft.body
|
||||||
|
|
||||||
describe "DraftStore", ->
|
describe "DraftStore", ->
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Contenteditable extends React.Component
|
||||||
|
|
||||||
Edits made within the editing function will eventually fire _onDOMMutated
|
Edits made within the editing function will eventually fire _onDOMMutated
|
||||||
###
|
###
|
||||||
atomicEdit: (editingFunction, extraArgs...) =>
|
atomicEdit: (editingFunction, extraArgsObj={}) =>
|
||||||
@_teardownListeners()
|
@_teardownListeners()
|
||||||
|
|
||||||
editor = new EditorAPI(@_editableNode())
|
editor = new EditorAPI(@_editableNode())
|
||||||
|
@ -89,14 +89,14 @@ class Contenteditable extends React.Component
|
||||||
if not editor.currentSelection().isInScope()
|
if not editor.currentSelection().isInScope()
|
||||||
editor.importSelection(@innerState.exportedSelection)
|
editor.importSelection(@innerState.exportedSelection)
|
||||||
|
|
||||||
args = [editor, extraArgs...]
|
argsObj = _.extend(extraArgsObj, {editor})
|
||||||
editingFunction.apply(null, args)
|
editingFunction(argsObj)
|
||||||
|
|
||||||
@_setupListeners()
|
@_setupListeners()
|
||||||
|
|
||||||
focus: => @_editableNode().focus()
|
focus: => @_editableNode().focus()
|
||||||
|
|
||||||
selectEnd: => @atomicEdit (editor) -> editor.selectEnd()
|
selectEnd: => @atomicEdit ({editor}) -> editor.selectEnd()
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
|
@ -202,7 +202,7 @@ class Contenteditable extends React.Component
|
||||||
_keymapHandlers: ->
|
_keymapHandlers: ->
|
||||||
atomicEditWrap = (command) =>
|
atomicEditWrap = (command) =>
|
||||||
(event) =>
|
(event) =>
|
||||||
@atomicEdit(((editor) -> editor[command]()), event)
|
@atomicEdit((({editor}) -> editor[command]()), event)
|
||||||
|
|
||||||
keymapHandlers = {
|
keymapHandlers = {
|
||||||
'contenteditable:bold': atomicEditWrap("bold")
|
'contenteditable:bold': atomicEditWrap("bold")
|
||||||
|
@ -253,7 +253,7 @@ class Contenteditable extends React.Component
|
||||||
@setInnerState dragging: false if @innerState.dragging
|
@setInnerState dragging: false if @innerState.dragging
|
||||||
@setInnerState doubleDown: false if @innerState.doubleDown
|
@setInnerState doubleDown: false if @innerState.doubleDown
|
||||||
|
|
||||||
@_runCallbackOnExtensions("onContentChanged", mutations)
|
@_runCallbackOnExtensions("onContentChanged", {mutations})
|
||||||
|
|
||||||
@_saveSelectionState()
|
@_saveSelectionState()
|
||||||
|
|
||||||
|
@ -306,7 +306,7 @@ class Contenteditable extends React.Component
|
||||||
|
|
||||||
menu = new Menu()
|
menu = new Menu()
|
||||||
|
|
||||||
@dispatchEventToExtensions("onShowContextMenu", event, menu)
|
@dispatchEventToExtensions("onShowContextMenu", event, {menu})
|
||||||
menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
|
menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
|
||||||
menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
|
menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
|
||||||
menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
|
menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
|
||||||
|
@ -320,9 +320,9 @@ class Contenteditable extends React.Component
|
||||||
############################# Extensions ###############################
|
############################# Extensions ###############################
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
_runCallbackOnExtensions: (method, args...) =>
|
_runCallbackOnExtensions: (method, argsObj={}) =>
|
||||||
for extension in @props.extensions.concat(@coreExtensions)
|
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
|
# Will execute the event handlers on each of the registerd and core
|
||||||
# extensions In this context, event.preventDefault and
|
# 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
|
# basically means preventing the core extension handlers from being
|
||||||
# called. If any of the extensions calls event.stopPropagation(), it
|
# called. If any of the extensions calls event.stopPropagation(), it
|
||||||
# will prevent any other extension handlers from being called.
|
# 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
|
for extension in @props.extensions
|
||||||
break if event?.isPropagationStopped()
|
break if event?.isPropagationStopped()
|
||||||
@_runExtensionMethod(extension, method, event, args...)
|
@_runExtensionMethod(extension, method, argsObj)
|
||||||
|
|
||||||
return if event?.defaultPrevented or event?.isPropagationStopped()
|
return if event?.defaultPrevented or event?.isPropagationStopped()
|
||||||
for extension in @coreExtensions
|
for extension in @coreExtensions
|
||||||
break if event?.isPropagationStopped()
|
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]?
|
return if not extension[method]?
|
||||||
editingFunction = extension[method].bind(extension)
|
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
|
# 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
|
# variety of edge cases that we need to correct for and prepare both the
|
||||||
# HTML and the selection to be serialized without error.
|
# HTML and the selection to be serialized without error.
|
||||||
@onContentChanged: (editor, mutations) ->
|
@onContentChanged: ({editor, mutations}) ->
|
||||||
@_cleanHTML(editor)
|
@_cleanHTML(editor)
|
||||||
@_cleanSelection(editor)
|
@_cleanSelection(editor)
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ class FloatingToolbarContainer extends React.Component
|
||||||
onDoneWithLink={@_onDoneWithLink} />
|
onDoneWithLink={@_onDoneWithLink} />
|
||||||
|
|
||||||
_onSaveUrl: (url, linkToModify) =>
|
_onSaveUrl: (url, linkToModify) =>
|
||||||
@props.atomicEdit (editor) ->
|
@props.atomicEdit ({editor}) ->
|
||||||
if linkToModify?
|
if linkToModify?
|
||||||
equivalentNode = DOMUtils.findSimilarNodes(editor.rootNode, linkToModify)?[0]
|
equivalentNode = DOMUtils.findSimilarNodes(editor.rootNode, linkToModify)?[0]
|
||||||
return unless equivalentNode?
|
return unless equivalentNode?
|
||||||
|
@ -118,8 +118,9 @@ class FloatingToolbarContainer extends React.Component
|
||||||
# core actions and user-defined plugins. The FloatingToolbar simply
|
# core actions and user-defined plugins. The FloatingToolbar simply
|
||||||
# renders them.
|
# renders them.
|
||||||
_toolbarButtonConfigs: ->
|
_toolbarButtonConfigs: ->
|
||||||
atomicEditWrap = (command) => (event) =>
|
atomicEditWrap = (command) =>
|
||||||
@props.atomicEdit(((editor) -> editor[command]()), event)
|
(event) =>
|
||||||
|
@props.atomicEdit((({editor}) -> editor[command]()), event)
|
||||||
|
|
||||||
extensionButtonConfigs = []
|
extensionButtonConfigs = []
|
||||||
ExtensionRegistry.Composer.extensions().forEach (ext) ->
|
ExtensionRegistry.Composer.extensions().forEach (ext) ->
|
||||||
|
|
|
@ -2,13 +2,13 @@ _str = require 'underscore.string'
|
||||||
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
||||||
|
|
||||||
class ListManager extends ContenteditableExtension
|
class ListManager extends ContenteditableExtension
|
||||||
@onContentChanged: (editor, mutations) ->
|
@onContentChanged: ({editor, mutations}) ->
|
||||||
if @_spaceEntered and @hasListStartSignature(editor.currentSelection())
|
if @_spaceEntered and @hasListStartSignature(editor.currentSelection())
|
||||||
@createList(editor)
|
@createList(editor)
|
||||||
|
|
||||||
@_collapseAdjacentLists(editor)
|
@_collapseAdjacentLists(editor)
|
||||||
|
|
||||||
@onKeyDown: (editor, event) ->
|
@onKeyDown: ({editor, event}) ->
|
||||||
@_spaceEntered = event.key is " "
|
@_spaceEntered = event.key is " "
|
||||||
if DOMUtils.isInList()
|
if DOMUtils.isInList()
|
||||||
if event.key is "Backspace" and DOMUtils.atStartOfList()
|
if event.key is "Backspace" and DOMUtils.atStartOfList()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
||||||
|
|
||||||
class TabManager extends ContenteditableExtension
|
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
|
# 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
|
# the keymap manager if the extension prevented the default behavior
|
||||||
if event.defaultPrevented
|
if event.defaultPrevented
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import {Listener, Publisher} from './flux/modules/reflux-coffee';
|
import {Listener, Publisher} from './flux/modules/reflux-coffee';
|
||||||
import {includeModule} from './flux/coffee-helpers';
|
import {includeModule} from './flux/coffee-helpers';
|
||||||
|
import composerExtAdapter from './extensions/composer-extension-adapter';
|
||||||
|
import messageViewExtAdapter from './extensions/message-view-extension-adapter';
|
||||||
|
|
||||||
export class Registry {
|
export class Registry {
|
||||||
|
|
||||||
|
@ -55,9 +56,10 @@ Registry.include(Listener);
|
||||||
|
|
||||||
export const Composer = new Registry(
|
export const Composer = new Registry(
|
||||||
'Composer',
|
'Composer',
|
||||||
require('./extensions/composer-extension-adapter')
|
composerExtAdapter
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MessageView = new Registry(
|
export const MessageView = new Registry(
|
||||||
'MessageView',
|
'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
|
Returns a list of warning strings, or an empty array if no warnings need
|
||||||
to be displayed.
|
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
|
valuable way, you should set `draft.pristine = false` so the draft
|
||||||
saves, even if no further changes are made.
|
saves, even if no further changes are made.
|
||||||
###
|
###
|
||||||
@prepareNewDraft: (draft) ->
|
@prepareNewDraft: ({draft}) ->
|
||||||
return
|
return
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -103,14 +103,14 @@ class ComposerExtension extends ContenteditableExtension
|
||||||
|
|
||||||
```coffee
|
```coffee
|
||||||
# Remove any <code> tags found in the draft body
|
# Remove any <code> tags found in the draft body
|
||||||
finalizeSessionBeforeSending: (session) ->
|
finalizeSessionBeforeSending: ({session}) ->
|
||||||
body = session.draft().body
|
body = session.draft().body
|
||||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||||
if body != clean
|
if body != clean
|
||||||
session.changes.add(body: clean)
|
session.changes.add(body: clean)
|
||||||
```
|
```
|
||||||
###
|
###
|
||||||
@finalizeSessionBeforeSending: (session) ->
|
@finalizeSessionBeforeSending: ({session}) ->
|
||||||
return
|
return
|
||||||
|
|
||||||
module.exports = ComposerExtension
|
module.exports = ComposerExtension
|
||||||
|
|
|
@ -64,7 +64,7 @@ class ContenteditableExtension
|
||||||
reflect that it is no longer empty.
|
reflect that it is no longer empty.
|
||||||
|
|
||||||
```coffee
|
```coffee
|
||||||
onContentChanged: (editor, mutations) ->
|
onContentChanged: ({editor, mutations}) ->
|
||||||
isWithinNode = (node) ->
|
isWithinNode = (node) ->
|
||||||
test = selection.baseNode
|
test = selection.baseNode
|
||||||
while test isnt editableNode
|
while test isnt editableNode
|
||||||
|
@ -78,9 +78,9 @@ class ContenteditableExtension
|
||||||
codeTag.classList.remove('empty')
|
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
|
Public: Override onBlur to mutate the contenteditable DOM node whenever the
|
||||||
|
@ -91,7 +91,7 @@ class ContenteditableExtension
|
||||||
methods for manipulating the selection and DOM
|
methods for manipulating the selection and DOM
|
||||||
- event: DOM event fired on the contenteditable
|
- event: DOM event fired on the contenteditable
|
||||||
###
|
###
|
||||||
@onBlur: (editor, event) ->
|
@onBlur: ({editor, event}) ->
|
||||||
|
|
||||||
###
|
###
|
||||||
Public: Override onFocus to mutate the contenteditable DOM node whenever the
|
Public: Override onFocus to mutate the contenteditable DOM node whenever the
|
||||||
|
@ -102,7 +102,7 @@ class ContenteditableExtension
|
||||||
methods for manipulating the selection and DOM
|
methods for manipulating the selection and DOM
|
||||||
- event: DOM event fired on the contenteditable
|
- event: DOM event fired on the contenteditable
|
||||||
###
|
###
|
||||||
@onFocus: (editor, event) ->
|
@onFocus: ({editor, event}) ->
|
||||||
|
|
||||||
###
|
###
|
||||||
Public: Override onClick to mutate the contenteditable DOM node whenever the
|
Public: Override onClick to mutate the contenteditable DOM node whenever the
|
||||||
|
@ -113,7 +113,7 @@ class ContenteditableExtension
|
||||||
methods for manipulating the selection and DOM
|
methods for manipulating the selection and DOM
|
||||||
- event: DOM event fired on the contenteditable
|
- event: DOM event fired on the contenteditable
|
||||||
###
|
###
|
||||||
@onClick: (editor, event) ->
|
@onClick: ({editor, event}) ->
|
||||||
|
|
||||||
###
|
###
|
||||||
Public: Override onKeyDown to mutate the contenteditable DOM node whenever the
|
Public: Override onKeyDown to mutate the contenteditable DOM node whenever the
|
||||||
|
@ -133,12 +133,11 @@ class ContenteditableExtension
|
||||||
methods for manipulating the selection and DOM
|
methods for manipulating the selection and DOM
|
||||||
- event: DOM event fired on the contenteditable
|
- event: DOM event fired on the contenteditable
|
||||||
###
|
###
|
||||||
@onKeyDown: (editor, event) ->
|
@onKeyDown: ({editor, event}) ->
|
||||||
|
|
||||||
###
|
###
|
||||||
Public: Override onInput to mutate the contenteditable DOM node whenever the
|
Public: Override onShowContextMenu to add new menu items to the right click menu
|
||||||
onInput event is fired on it.You may mutate the contenteditable in place, we
|
inside the contenteditable.
|
||||||
not expect any return value from this method.
|
|
||||||
|
|
||||||
- editor: The {Editor} controller that provides a host of convenience
|
- editor: The {Editor} controller that provides a host of convenience
|
||||||
methods for manipulating the selection and DOM
|
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)
|
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.
|
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
|
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
|
Public: Transform the message body HTML provided in `body` and return HTML
|
||||||
that should be displayed for the message.
|
that should be displayed for the message.
|
||||||
###
|
###
|
||||||
@formatMessageBody: (body) ->
|
@formatMessageBody: ({message}) ->
|
||||||
return body
|
return body
|
||||||
|
|
||||||
module.exports = MessageViewExtension
|
module.exports = MessageViewExtension
|
||||||
|
|
|
@ -238,7 +238,7 @@ class DraftStore
|
||||||
# Give extensions an opportunity to perform additional setup to the draft
|
# Give extensions an opportunity to perform additional setup to the draft
|
||||||
for extension in @extensions()
|
for extension in @extensions()
|
||||||
continue unless extension.prepareNewDraft
|
continue unless extension.prepareNewDraft
|
||||||
extension.prepareNewDraft(draft)
|
extension.prepareNewDraft({draft})
|
||||||
|
|
||||||
# Optimistically create a draft session and hand it the draft so that it
|
# 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.
|
# 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) ->
|
_runExtensionsBeforeSend: (session) ->
|
||||||
for extension in @extensions()
|
for extension in @extensions()
|
||||||
continue unless extension.finalizeSessionBeforeSending
|
continue unless extension.finalizeSessionBeforeSending
|
||||||
extension.finalizeSessionBeforeSending(session)
|
extension.finalizeSessionBeforeSending({session})
|
||||||
|
|
||||||
_onRemoveFile: ({file, messageClientId}) =>
|
_onRemoveFile: ({file, messageClientId}) =>
|
||||||
@sessionForClientId(messageClientId).then (session) ->
|
@sessionForClientId(messageClientId).then (session) ->
|
||||||
|
|
|
@ -51,7 +51,7 @@ class MessageBodyProcessor
|
||||||
continue unless extension.formatMessageBody
|
continue unless extension.formatMessageBody
|
||||||
virtual = message.clone()
|
virtual = message.clone()
|
||||||
virtual.body = body
|
virtual.body = body
|
||||||
extension.formatMessageBody(virtual)
|
extension.formatMessageBody({message: virtual})
|
||||||
body = virtual.body
|
body = virtual.body
|
||||||
|
|
||||||
# Find inline images and give them a calculated CSS height based on
|
# Find inline images and give them a calculated CSS height based on
|
||||||
|
|
|
@ -29,4 +29,8 @@ RegExpUtils =
|
||||||
|
|
||||||
looseStyleTag: -> /<style/gim
|
looseStyleTag: -> /<style/gim
|
||||||
|
|
||||||
|
# Regular expression matching javasript function arguments:
|
||||||
|
# https://regex101.com/r/pZ6zF0/1
|
||||||
|
functionArgs: -> /\(\s*([^)]+?)\s*\)/
|
||||||
|
|
||||||
module.exports = RegExpUtils
|
module.exports = RegExpUtils
|
||||||
|
|
Loading…
Add table
Reference in a new issue