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:
Juan Tejada 2015-12-30 11:36:47 -05:00
parent 4628643195
commit 6315bc9d80
33 changed files with 374 additions and 151 deletions

View file

@ -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]
}
}

View file

@ -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."

View file

@ -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');

View file

@ -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

View file

@ -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

View file

@ -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>')

View file

@ -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

View file

@ -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

View file

@ -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(), {

View file

@ -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}#"

View file

@ -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

View file

@ -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})

View file

@ -100,7 +100,7 @@ TrackingBlacklist = [{
class TrackingPixelsExtension extends MessageViewExtension
@formatMessageBody: (message) ->
@formatMessageBody: ({message}) ->
return unless message.isFromMe()
regex = RegExpUtils.imageTagRegex()

View file

@ -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)

View file

@ -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)

View 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, []);
});
});
});

View file

@ -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", ->

View file

@ -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)
########################################################################

View file

@ -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)

View file

@ -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) ->

View file

@ -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()

View file

@ -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

View file

@ -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
);

View file

@ -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

View 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;
}

View file

@ -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

View file

@ -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

View 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*/);
}

View 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;
}

View file

@ -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

View file

@ -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) ->

View file

@ -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

View file

@ -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