mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
update(extensions): Rename DraftStoreExtension and MessageStoreExtension
Summary: - Rename DraftStoreExtension to ComposerExtension - Rename MessageStoreExtension to MessageViewExtension - Rename ContenteditablePlugin to ContenteditableExtension - Update Contenteditable to use new naming convention - Adds support for extension handlers as props - Add ExtensionRegistry to register extensions: - ContenteditableExtensions will not be registered through the ExtensionRegistry. They are meant for internal use, or if anyone wants to use our Contenteditable component directly in their plugins. - Adds specs - Refactors internal_packages and src to use new names and new ExtensionRegistry api - Adds deprecation util function and deprecation notices for old api methods: - DraftStore.{registerExtension, unregisterExtension} - MessageStore.{registerExtension, unregisterExtension} - DraftStoreExtension.{onMouseUp, onTabDown} - MessageStoreExtension - Adds and updates docs Test Plan: - Unit tests Reviewers: bengotow, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2293
This commit is contained in:
parent
02633ffd57
commit
cfdc401c54
34 changed files with 682 additions and 369 deletions
|
@ -14,18 +14,18 @@ This API allows your package to:
|
|||
|
||||
- Transform the draft and make additional changes before it is sent.
|
||||
|
||||
To create your own composer extensions, subclass {DraftStoreExtensions} and override the methods your extension needs.
|
||||
To create your own composer extensions, subclass {ComposerExtension} and override the methods your extension needs.
|
||||
|
||||
In the sample packages repository, [templates]() is an example of a package which uses a DraftStoreExtension to enhance the composer experience.
|
||||
In the sample packages repository, [templates]() is an example of a package which uses a ComposerExtension to enhance the composer experience.
|
||||
|
||||
### Example
|
||||
|
||||
This extension displays a warning before sending a draft that contains the names of competitors' products. If the user proceeds to send the draft containing the words, it appends a disclaimer.
|
||||
|
||||
```coffee
|
||||
{DraftStoreExtension} = require 'nylas-exports'
|
||||
{ComposerExtension} = require 'nylas-exports'
|
||||
|
||||
class ProductsExtension extends DraftStoreExtension
|
||||
class ProductsExtension extends ComposerExtension
|
||||
|
||||
@warningsForSending: (draft) ->
|
||||
words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']
|
|
@ -43,7 +43,6 @@ class TemplatesDraftStoreExtension extends DraftStoreExtension {
|
|||
}
|
||||
|
||||
static onTabDown(editableNode, range, event) {
|
||||
console.log('tabbed');
|
||||
if (event.shiftKey) {
|
||||
return this.onTabSelectNextVar(editableNode, range, event, -1);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{PreferencesUIStore, DraftStore} = require 'nylas-exports'
|
||||
SignatureDraftExtension = require './signature-draft-extension'
|
||||
{PreferencesUIStore, ExtensionRegistry} = require 'nylas-exports'
|
||||
SignatureComposerExtension = require './signature-composer-extension'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
|
@ -8,11 +8,11 @@ module.exports =
|
|||
displayName: "Signatures"
|
||||
component: require "./preferences-signatures"
|
||||
|
||||
DraftStore.registerExtension(SignatureDraftExtension)
|
||||
ExtensionRegistry.Composer.register(SignatureComposerExtension)
|
||||
PreferencesUIStore.registerPreferencesTab(@preferencesTab)
|
||||
|
||||
deactivate: ->
|
||||
DraftStore.unregisterExtension(SignatureDraftExtension)
|
||||
ExtensionRegistry.Composer.unregister(SignatureComposerExtension)
|
||||
PreferencesUIStore.unregisterPreferencesTab(@preferencesTab.sectionId)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{DraftStoreExtension, AccountStore} = require 'nylas-exports'
|
||||
{ComposerExtension, AccountStore} = require 'nylas-exports'
|
||||
|
||||
class SignatureDraftStoreExtension extends DraftStoreExtension
|
||||
class SignatureComposerExtension extends ComposerExtension
|
||||
@prepareNewDraft: (draft) ->
|
||||
accountId = AccountStore.current().id
|
||||
signature = NylasEnv.config.get("nylas.account-#{accountId}.signature")
|
||||
|
@ -11,4 +11,4 @@ class SignatureDraftStoreExtension extends DraftStoreExtension
|
|||
insertionPoint = draft.body.length
|
||||
draft.body = draft.body.substr(0, insertionPoint-1) + "<br/>" + signature + draft.body.substr(insertionPoint)
|
||||
|
||||
module.exports = SignatureDraftStoreExtension
|
||||
module.exports = SignatureComposerExtension
|
|
@ -1,8 +1,8 @@
|
|||
{Message} = require 'nylas-exports'
|
||||
|
||||
SignatureDraftStoreExtension = require '../lib/signature-draft-extension'
|
||||
SignatureComposerExtension = require '../lib/signature-composer-extension'
|
||||
|
||||
describe "SignatureDraftStoreExtension", ->
|
||||
describe "SignatureComposerExtension", ->
|
||||
describe "prepareNewDraft", ->
|
||||
describe "when a signature is defined", ->
|
||||
beforeEach ->
|
||||
|
@ -18,9 +18,9 @@ describe "SignatureDraftStoreExtension", ->
|
|||
draft: true
|
||||
body: 'This is a another test.'
|
||||
|
||||
SignatureDraftStoreExtension.prepareNewDraft(a)
|
||||
SignatureComposerExtension.prepareNewDraft(a)
|
||||
expect(a.body).toEqual("This is a test!<br/><div id='signature'>This is my signature.</div><blockquote>Hello world</blockquote>")
|
||||
SignatureDraftStoreExtension.prepareNewDraft(b)
|
||||
SignatureComposerExtension.prepareNewDraft(b)
|
||||
expect(b.body).toEqual("This is a another test<br/><div id='signature'>This is my signature.</div>")
|
||||
|
||||
describe "when a signature is not defined", ->
|
||||
|
@ -32,5 +32,5 @@ describe "SignatureDraftStoreExtension", ->
|
|||
a = new Message
|
||||
draft: true
|
||||
body: 'This is a test! <blockquote>Hello world</blockquote>'
|
||||
SignatureDraftStoreExtension.prepareNewDraft(a)
|
||||
SignatureComposerExtension.prepareNewDraft(a)
|
||||
expect(a.body).toEqual('This is a test! <blockquote>Hello world</blockquote>')
|
|
@ -1,11 +1,11 @@
|
|||
{ComponentRegistry, DraftStore} = require 'nylas-exports'
|
||||
Extension = require './draft-extension'
|
||||
{ExtensionRegistry} = require 'nylas-exports'
|
||||
SpellcheckComposerExtension = require './spellcheck-composer-extension'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
DraftStore.registerExtension(Extension)
|
||||
ExtensionRegistry.Composer.register(SpellcheckComposerExtension)
|
||||
|
||||
deactivate: ->
|
||||
DraftStore.unregisterExtension(Extension)
|
||||
ExtensionRegistry.Composer.unregister(SpellcheckComposerExtension)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{DraftStoreExtension, AccountStore, DOMUtils} = require 'nylas-exports'
|
||||
{ComposerExtension, AccountStore, DOMUtils} = require 'nylas-exports'
|
||||
_ = require 'underscore'
|
||||
spellchecker = require('spellchecker')
|
||||
remote = require('remote')
|
||||
|
@ -6,16 +6,16 @@ MenuItem = remote.require('menu-item')
|
|||
|
||||
SpellcheckCache = {}
|
||||
|
||||
class SpellcheckDraftStoreExtension extends DraftStoreExtension
|
||||
class SpellcheckComposerExtension extends ComposerExtension
|
||||
|
||||
@isMisspelled: (word) ->
|
||||
SpellcheckCache[word] ?= spellchecker.isMisspelled(word)
|
||||
SpellcheckCache[word]
|
||||
|
||||
@onInput: (editableNode) ->
|
||||
@onInput: (editableNode) =>
|
||||
@walkTree(editableNode)
|
||||
|
||||
@onShowContextMenu: (event, editableNode, selection, innerStateProxy, menu) =>
|
||||
@onShowContextMenu: (event, editableNode, selection, menu) =>
|
||||
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
|
||||
word = range.toString()
|
||||
if @isMisspelled(word)
|
||||
|
@ -123,6 +123,6 @@ class SpellcheckDraftStoreExtension extends DraftStoreExtension
|
|||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
|
||||
SpellcheckDraftStoreExtension.SpellcheckCache = SpellcheckCache
|
||||
SpellcheckComposerExtension.SpellcheckCache = SpellcheckCache
|
||||
|
||||
module.exports = SpellcheckDraftStoreExtension
|
||||
module.exports = SpellcheckComposerExtension
|
|
@ -1,22 +1,22 @@
|
|||
SpellcheckDraftStoreExtension = require '../lib/draft-extension'
|
||||
SpellcheckComposerExtension = require '../lib/spellcheck-composer-extension'
|
||||
fs = require 'fs'
|
||||
_ = require 'underscore'
|
||||
|
||||
initialHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-before.html').toString()
|
||||
expectedHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-after.html').toString()
|
||||
|
||||
describe "SpellcheckDraftStoreExtension", ->
|
||||
describe "SpellcheckComposerExtension", ->
|
||||
beforeEach ->
|
||||
# Avoid differences between node-spellcheck on different platforms
|
||||
spellings = JSON.parse(fs.readFileSync(__dirname + '/fixtures/california-spelling-lookup.json'))
|
||||
spyOn(SpellcheckDraftStoreExtension, 'isMisspelled').andCallFake (word) ->
|
||||
spyOn(SpellcheckComposerExtension, 'isMisspelled').andCallFake (word) ->
|
||||
spellings[word]
|
||||
|
||||
describe "walkTree", ->
|
||||
it "correctly walks a DOM tree and surrounds mispelled words", ->
|
||||
dom = document.createElement('div')
|
||||
dom.innerHTML = initialHTML
|
||||
SpellcheckDraftStoreExtension.walkTree(dom)
|
||||
SpellcheckComposerExtension.walkTree(dom)
|
||||
expect(dom.innerHTML).toEqual(expectedHTML)
|
||||
|
||||
describe "finalizeSessionBeforeSending", ->
|
||||
|
@ -27,7 +27,7 @@ describe "SpellcheckDraftStoreExtension", ->
|
|||
changes:
|
||||
add: jasmine.createSpy('add')
|
||||
|
||||
SpellcheckDraftStoreExtension.finalizeSessionBeforeSending(session)
|
||||
SpellcheckComposerExtension.finalizeSessionBeforeSending(session)
|
||||
expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML)
|
||||
|
||||
module.exports = SpellcheckDraftStoreExtension
|
||||
module.exports = SpellcheckComposerExtension
|
|
@ -1,27 +0,0 @@
|
|||
{DraftStore, DOMUtils, ContenteditablePlugin} = require 'nylas-exports'
|
||||
|
||||
class ComposerExtensionsPlugin extends ContenteditablePlugin
|
||||
@onInput: (event, editableNode, selection, innerStateProxy) ->
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onInput?(editableNode, event)
|
||||
|
||||
@onKeyDown: (event, editableNode, selection, innerStateProxy) ->
|
||||
if event.key is "Tab"
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onTabDown?(editableNode, range, event)
|
||||
|
||||
@onShowContextMenu: (args...) ->
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onShowContextMenu?(args...)
|
||||
|
||||
@onClick: (event, editableNode, selection, innerStateProxy) ->
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
return unless range
|
||||
try
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onMouseUp?(editableNode, range, event)
|
||||
catch e
|
||||
console.error('DraftStore extension raised an error: '+e.toString())
|
||||
|
||||
module.exports = ComposerExtensionsPlugin
|
|
@ -10,7 +10,8 @@ React = require 'react'
|
|||
AccountStore,
|
||||
FileUploadStore,
|
||||
QuotedHTMLParser,
|
||||
FileDownloadStore} = require 'nylas-exports'
|
||||
FileDownloadStore,
|
||||
ExtensionRegistry} = require 'nylas-exports'
|
||||
|
||||
{DropZone,
|
||||
RetinaImg,
|
||||
|
@ -29,8 +30,6 @@ CollapsedParticipants = require './collapsed-participants'
|
|||
|
||||
Fields = require './fields'
|
||||
|
||||
ComposerExtensionsPlugin = require './composer-extensions-plugin'
|
||||
|
||||
# The ComposerView is a unique React component because it (currently) is a
|
||||
# singleton. Normally, the React way to do things would be to re-render the
|
||||
# Composer with new props.
|
||||
|
@ -68,6 +67,7 @@ class ComposerView extends React.Component
|
|||
enabledFields: [] # Gets updated in @_initiallyEnabledFields
|
||||
showQuotedText: false
|
||||
uploads: FileUploadStore.uploadsForMessage(@props.draftClientId) ? []
|
||||
extensions: ExtensionRegistry.Composer.extensions()
|
||||
|
||||
componentWillMount: =>
|
||||
@_prepareForDraft(@props.draftClientId)
|
||||
|
@ -80,6 +80,7 @@ class ComposerView extends React.Component
|
|||
@_usubs = []
|
||||
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
|
||||
@_usubs.push AccountStore.listen @_onAccountStoreChanged
|
||||
@_usubs.push ExtensionRegistry.Composer.listen @_onExtensionsChanged
|
||||
@_applyFieldFocus()
|
||||
|
||||
componentWillUnmount: =>
|
||||
|
@ -300,7 +301,7 @@ class ComposerView extends React.Component
|
|||
onScrollTo={@props.onRequestScrollTo}
|
||||
onFilePaste={@_onFilePaste}
|
||||
onScrollToBottom={@_onScrollToBottom()}
|
||||
plugins={[ComposerExtensionsPlugin]}
|
||||
extensions={@state.extensions}
|
||||
getComposerBoundingRect={@_getComposerBoundingRect}
|
||||
initialSelectionSnapshot={@_recoveredSelection} />
|
||||
|
||||
|
@ -529,6 +530,9 @@ class ComposerView extends React.Component
|
|||
enabledFields.push Fields.Body
|
||||
return enabledFields
|
||||
|
||||
_onExtensionsChanged: =>
|
||||
@setState extensions: ExtensionRegistry.Composer.extensions()
|
||||
|
||||
# When the account store changes, the From field may or may not still
|
||||
# be in scope. We need to make sure to update our enabled fields.
|
||||
_onAccountStoreChanged: =>
|
||||
|
@ -685,7 +689,7 @@ class ComposerView extends React.Component
|
|||
warnings.push('without a body')
|
||||
|
||||
# Check third party warnings added via DraftStore extensions
|
||||
for extension in DraftStore.extensions()
|
||||
for extension in @state.extensions
|
||||
continue unless extension.warningsForSending
|
||||
warnings = warnings.concat(extension.warningsForSending(draft))
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
AutoloadImagesStore = require './autoload-images-store'
|
||||
{MessageStoreExtension} = require 'nylas-exports'
|
||||
{MessageViewExtension} = require 'nylas-exports'
|
||||
|
||||
class AutoloadImagesExtension extends MessageStoreExtension
|
||||
class AutoloadImagesExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
if AutoloadImagesStore.shouldBlockImagesIn(message)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{ComponentRegistry,
|
||||
MessageStore,
|
||||
ExtensionRegistry,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
|
||||
AutoloadImagesExtension = require './autoload-images-extension'
|
||||
|
@ -10,12 +10,12 @@ module.exports =
|
|||
|
||||
activate: (@state={}) ->
|
||||
# Register Message List Actions we provide globally
|
||||
MessageStore.registerExtension(AutoloadImagesExtension)
|
||||
ExtensionRegistry.MessageView.register AutoloadImagesExtension
|
||||
ComponentRegistry.register AutoloadImagesHeader,
|
||||
role: 'message:BodyHeader'
|
||||
|
||||
deactivate: ->
|
||||
MessageStore.unregisterExtension(AutoloadImagesExtension)
|
||||
ExtensionRegistry.MessageView.unregister AutoloadImagesExtension
|
||||
ComponentRegistry.unregister(AutoloadImagesHeader)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
MessageList = require "./message-list"
|
||||
MessageToolbarItems = require "./message-toolbar-items"
|
||||
{ComponentRegistry,
|
||||
MessageStore,
|
||||
ExtensionRegistry,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
|
||||
{SidebarContactCard,
|
||||
|
@ -46,8 +46,8 @@ module.exports =
|
|||
ComponentRegistry.register ThreadToggleUnreadButton,
|
||||
role: 'message:Toolbar'
|
||||
|
||||
MessageStore.registerExtension(AutolinkerExtension)
|
||||
MessageStore.registerExtension(TrackingPixelsExtension)
|
||||
ExtensionRegistry.MessageView.register AutolinkerExtension
|
||||
ExtensionRegistry.MessageView.register TrackingPixelsExtension
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister MessageList
|
||||
|
@ -59,7 +59,7 @@ module.exports =
|
|||
ComponentRegistry.unregister SidebarContactCard
|
||||
ComponentRegistry.unregister SidebarSpacer
|
||||
ComponentRegistry.unregister SidebarContactList
|
||||
MessageStore.unregisterExtension(AutolinkerExtension)
|
||||
MessageStore.unregisterExtension(TrackingPixelsExtension)
|
||||
ExtensionRegistry.MessageView.unregister AutolinkerExtension
|
||||
ExtensionRegistry.MessageView.unregister TrackingPixelsExtension
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Autolinker = require 'autolinker'
|
||||
{MessageStoreExtension} = require 'nylas-exports'
|
||||
{MessageViewExtension} = require 'nylas-exports'
|
||||
|
||||
class AutolinkerExtension extends MessageStoreExtension
|
||||
class AutolinkerExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
# Apply the autolinker pass to make emails and links clickable
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{MessageStoreExtension, RegExpUtils} = require 'nylas-exports'
|
||||
{MessageViewExtension, RegExpUtils} = require 'nylas-exports'
|
||||
|
||||
TrackingBlacklist = [{
|
||||
name: 'Sidekick',
|
||||
|
@ -98,7 +98,7 @@ TrackingBlacklist = [{
|
|||
homepage: 'http://salesloft.com'
|
||||
}]
|
||||
|
||||
class TrackingPixelsExtension extends MessageStoreExtension
|
||||
class TrackingPixelsExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
return unless message.isFromMe()
|
||||
|
|
63
spec/extension-registry-spec.coffee
Normal file
63
spec/extension-registry-spec.coffee
Normal file
|
@ -0,0 +1,63 @@
|
|||
ExtensionRegistry = require '../src/extension-registry'
|
||||
|
||||
class TestExtension
|
||||
@name: 'TestExtension'
|
||||
|
||||
describe 'ExtensionRegistry', ->
|
||||
beforeEach ->
|
||||
@originalAdapters = ExtensionRegistry._deprecationAdapters
|
||||
@registry = new ExtensionRegistry.Registry('Test')
|
||||
spyOn @registry, 'triggerDebounced'
|
||||
|
||||
describe 'Registry', ->
|
||||
it 'has trigger and listen to defined', ->
|
||||
expect(@registry.trigger).toBeDefined()
|
||||
expect(@registry.listen).toBeDefined()
|
||||
expect(@registry.listenTo).toBeDefined()
|
||||
|
||||
describe 'register', ->
|
||||
it 'throws an exception if extension not passed', ->
|
||||
expect(=> @registry.register(null)).toThrow()
|
||||
|
||||
it 'throws an exception if extension does not have a name', ->
|
||||
expect(=> @registry.register({})).toThrow()
|
||||
|
||||
it 'throws an exception if extension is array', ->
|
||||
expect(=> @registry.register([])).toThrow()
|
||||
|
||||
it 'throws an exception if extension is string', ->
|
||||
expect(=> @registry.register('')).toThrow()
|
||||
|
||||
it 'returns itself', ->
|
||||
expect(@registry.register(TestExtension)).toBe(@registry)
|
||||
|
||||
it 'registers extension and triggers', ->
|
||||
@registry.register(TestExtension)
|
||||
expect(@registry.extensions().length).toEqual 1
|
||||
expect(@registry.triggerDebounced).toHaveBeenCalled()
|
||||
|
||||
it 'does not add extensions with the same name', ->
|
||||
expect(@registry.extensions().length).toEqual 0
|
||||
@registry.register(TestExtension)
|
||||
expect(@registry.extensions().length).toEqual 1
|
||||
@registry.register({name: 'TestExtension'})
|
||||
expect(@registry.extensions().length).toEqual 1
|
||||
|
||||
it 'calls deprecationAdapters if present for a role', ->
|
||||
adapterSpy = jasmine.createSpy('adapterSpy').andCallFake (ext) -> ext
|
||||
@registry = new ExtensionRegistry.Registry('Test', adapterSpy)
|
||||
spyOn @registry, 'triggerDebounced'
|
||||
@registry.register(TestExtension)
|
||||
expect(adapterSpy.calls.length).toEqual 1
|
||||
|
||||
describe 'unregister', ->
|
||||
it 'unregisters the extension if it exists', ->
|
||||
@registry.register(TestExtension)
|
||||
@registry.unregister(TestExtension)
|
||||
expect(@registry.extensions().length).toEqual 0
|
||||
|
||||
it 'throws if invalid extension passed', ->
|
||||
expect( => @registry.unregister('Test')).toThrow()
|
||||
expect( => @registry.unregister(null)).toThrow()
|
||||
expect( => @registry.unregister([])).toThrow()
|
||||
expect( => @registry.unregister({})).toThrow()
|
|
@ -6,7 +6,7 @@ ModelQuery = require '../../src/flux/models/query'
|
|||
AccountStore = require '../../src/flux/stores/account-store'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
DraftStore = require '../../src/flux/stores/draft-store'
|
||||
DraftStoreExtension = require '../../src/flux/stores/draft-store-extension'
|
||||
ComposerExtension = require '../../src/flux/extensions/composer-extension'
|
||||
SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
|
||||
SoundRegistry = require '../../src/sound-registry'
|
||||
|
@ -26,7 +26,7 @@ messageWithStyleTags = null
|
|||
fakeMessages = null
|
||||
fakeMessageWithFiles = null
|
||||
|
||||
class TestExtension extends DraftStoreExtension
|
||||
class TestExtension extends ComposerExtension
|
||||
@prepareNewDraft: (draft) ->
|
||||
draft.body = "Edited by TestExtension!" + draft.body
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
_ = require 'underscore'
|
||||
Actions = require './flux/actions'
|
||||
|
||||
{Listener, Publisher} = require './flux/modules/reflux-coffee'
|
||||
CoffeeHelpers = require './flux/coffee-helpers'
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
###
|
||||
ContenteditablePlugin is an abstract base class. Implementations of this
|
||||
are used to make additional changes to a <Contenteditable /> component
|
||||
beyond a user's input intents.
|
||||
|
||||
While some ContenteditablePlugins are included with the core
|
||||
<Contenteditable /> component, others may be added via the `plugins`
|
||||
prop.
|
||||
###
|
||||
class ContenteditablePlugin
|
||||
|
||||
# The onInput event can be triggered by a variety of events, some of
|
||||
# which could have been already been looked at by a callback.
|
||||
# Pretty much any DOM mutation will fire this.
|
||||
# Sometimes those mutations are the cause of callbacks.
|
||||
@onInput: (event, editableNode, selection, innerStateProxy) ->
|
||||
|
||||
@onBlur: (event, editableNode, selection, innerStateProxy) ->
|
||||
|
||||
@onFocus: (event, editableNode, selection, innerStateProxy) ->
|
||||
|
||||
@onClick: (event, editableNode, selection, innerStateProxy) ->
|
||||
|
||||
@onKeyDown: (event, editableNode, selection, innerStateProxy) ->
|
||||
|
||||
@onShowContextMenu: (event, editableNode, selection, innerStateProxy, menu) ->
|
|
@ -36,25 +36,34 @@ class Contenteditable extends React.Component
|
|||
|
||||
initialSelectionSnapshot: React.PropTypes.object
|
||||
|
||||
# Passes an absolute top coordinate to scroll to.
|
||||
# Handlers
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
onFilePaste: React.PropTypes.func
|
||||
# Passes an absolute top coordinate to scroll to.
|
||||
onScrollTo: React.PropTypes.func
|
||||
onScrollToBottom: React.PropTypes.func
|
||||
|
||||
# A list of objects that extend {ContenteditablePlugin}
|
||||
plugins: React.PropTypes.array
|
||||
# Extension DOM Mutating handlers. See {ContenteditableExtension}
|
||||
onInput: React.PropTypes.func
|
||||
onBlur: React.PropTypes.func
|
||||
onFocus: React.PropTypes.func
|
||||
onClick: React.PropTypes.func
|
||||
onKeyDown: React.PropTypes.func
|
||||
onShowContextMenu: React.PropTypes.func
|
||||
|
||||
# A list of objects that extend {ContenteditableExtension}
|
||||
extensions: React.PropTypes.array
|
||||
|
||||
spellcheck: React.PropTypes.bool
|
||||
|
||||
floatingToolbar: React.PropTypes.bool
|
||||
|
||||
@defaultProps:
|
||||
plugins: []
|
||||
extensions: []
|
||||
spellcheck: true
|
||||
floatingToolbar: true
|
||||
|
||||
corePlugins: [ListManager]
|
||||
coreExtensions: [ListManager]
|
||||
|
||||
# We allow extensions to read, and mutate the:
|
||||
#
|
||||
|
@ -70,7 +79,7 @@ class Contenteditable extends React.Component
|
|||
innerStateProxy =
|
||||
get: => return @innerState
|
||||
set: (newInnerState) => @setInnerState(newInnerState)
|
||||
args = [event, @_editableNode(), document.getSelection(), innerStateProxy, extraArgs...]
|
||||
args = [event, @_editableNode(), document.getSelection(), extraArgs..., innerStateProxy]
|
||||
editingFunction.apply(null, args)
|
||||
@_setupSelectionListeners()
|
||||
|
||||
|
@ -162,7 +171,7 @@ class Contenteditable extends React.Component
|
|||
selection.addRange(range)
|
||||
|
||||
# When some other component (like the `FloatingToolbar` or some
|
||||
# `DraftStoreExtension`) wants to mutate the DOM, it declares a
|
||||
# `ComposerExtension`) wants to mutate the DOM, it declares a
|
||||
# `mutator` function. That mutator expects to be passed the latest DOM
|
||||
# object (the `_editableNode()`) and will do mutations to it. Once those
|
||||
# mutations are done, we need to be sure to notify that changes
|
||||
|
@ -201,38 +210,42 @@ class Contenteditable extends React.Component
|
|||
@_setupSelectionListeners()
|
||||
@_onInput()
|
||||
|
||||
# Will execute the event handlers on each of the registerd and core plugins
|
||||
# Will execute the event handlers on each of the registerd and core extensions
|
||||
# In this context, event.preventDefault and event.stopPropagation don't refer
|
||||
# to stopping default DOM behavior or prevent event bubbling through the DOM,
|
||||
# but rather prevent our own Contenteditable default behavior, and preventing
|
||||
# other plugins from being called.
|
||||
# If any of the plugins calls event.preventDefault() it will prevent the
|
||||
# other extensions from being called.
|
||||
# If any of the extensions calls event.preventDefault() it will prevent the
|
||||
# default behavior for the Contenteditable, which basically means preventing
|
||||
# the core plugin handlers from being called.
|
||||
# If any of the plugins calls event.stopPropagation(), it will prevent any
|
||||
# other plugin handlers from being called.
|
||||
# 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.
|
||||
#
|
||||
# NOTE: It's possible for there to be no `event` passed in.
|
||||
_runPluginHandlersForEvent: (method, event, args...) =>
|
||||
executeCallback = (plugin) =>
|
||||
return if not plugin[method]?
|
||||
callback = plugin[method].bind(plugin)
|
||||
_runExtensionHandlersForEvent: (method, event, args...) =>
|
||||
executeCallback = (extension) =>
|
||||
return if not extension[method]?
|
||||
callback = extension[method].bind(extension)
|
||||
@atomicEdit(callback, event, args...)
|
||||
|
||||
for plugin in @props.plugins
|
||||
# Check if any of the extension handlers where passed as a prop and call
|
||||
# that first
|
||||
executeCallback(@props)
|
||||
|
||||
for extension in @props.extensions
|
||||
break if event?.isPropagationStopped()
|
||||
executeCallback(plugin)
|
||||
executeCallback(extension)
|
||||
|
||||
return if event?.defaultPrevented or event?.isPropagationStopped()
|
||||
for plugin in @corePlugins
|
||||
for extension in @coreExtensions
|
||||
break if event?.isPropagationStopped()
|
||||
executeCallback(plugin)
|
||||
executeCallback(extension)
|
||||
|
||||
_onKeyDown: (event) =>
|
||||
@_runPluginHandlersForEvent("onKeyDown", event)
|
||||
@_runExtensionHandlersForEvent("onKeyDown", event)
|
||||
|
||||
# This is a special case where we don't want to bubble up the event to the
|
||||
# keymap manager if the plugin prevented the default behavior
|
||||
# keymap manager if the extension prevented the default behavior
|
||||
if event.defaultPrevented
|
||||
event.stopPropagation()
|
||||
return
|
||||
|
@ -259,7 +272,7 @@ class Contenteditable extends React.Component
|
|||
@_ignoreInputChanges = true
|
||||
@_resetInnerStateOnInput()
|
||||
|
||||
@_runPluginHandlersForEvent("onInput", event)
|
||||
@_runExtensionHandlersForEvent("onInput", event)
|
||||
|
||||
@_normalize()
|
||||
|
||||
|
@ -413,11 +426,12 @@ class Contenteditable extends React.Component
|
|||
_onBlur: (event) =>
|
||||
@setInnerState dragging: false
|
||||
return if @_editableNode().parentElement.contains event.relatedTarget
|
||||
@_runExtensionHandlersForEvent("onBlur", event)
|
||||
@setInnerState editableFocused: false
|
||||
|
||||
_onFocus: (event) =>
|
||||
@setInnerState editableFocused: true
|
||||
@props.onFocus?(event)
|
||||
@_runExtensionHandlersForEvent("onFocus", event)
|
||||
|
||||
_editableNode: =>
|
||||
React.findDOMNode(@refs.contenteditable)
|
||||
|
@ -616,7 +630,8 @@ class Contenteditable extends React.Component
|
|||
MenuItem = remote.require('menu-item')
|
||||
|
||||
menu = new Menu()
|
||||
@_runPluginHandlersForEvent("onShowContextMenu", event, menu)
|
||||
|
||||
@_runExtensionHandlersForEvent("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'}))
|
||||
|
@ -660,7 +675,7 @@ class Contenteditable extends React.Component
|
|||
selection = document.getSelection()
|
||||
return event unless DOMUtils.selectionInScope(selection, editableNode)
|
||||
|
||||
@_runPluginHandlersForEvent("onClick", event)
|
||||
@_runExtensionHandlersForEvent("onClick", event)
|
||||
return event
|
||||
|
||||
_onDragStart: (event) =>
|
||||
|
|
|
@ -3,7 +3,7 @@ React = require 'react/addons'
|
|||
classNames = require 'classnames'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{DraftStore} = require 'nylas-exports'
|
||||
{ExtensionRegistry} = require 'nylas-exports'
|
||||
|
||||
class FloatingToolbar extends React.Component
|
||||
@displayName = "FloatingToolbar"
|
||||
|
@ -29,9 +29,11 @@ class FloatingToolbar extends React.Component
|
|||
@state =
|
||||
urlInputValue: @_initialUrl() ? ""
|
||||
componentWidth: 0
|
||||
extensions: ExtensionRegistry.Composer.extensions()
|
||||
|
||||
componentDidMount: =>
|
||||
@subscriptions = new CompositeDisposable()
|
||||
@usubExtensions = ExtensionRegistry.Composer.listen @_onExtensionsChanged
|
||||
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
@setState
|
||||
|
@ -39,6 +41,7 @@ class FloatingToolbar extends React.Component
|
|||
|
||||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
@usubExtensions()
|
||||
|
||||
componentDidUpdate: =>
|
||||
if @props.mode is "edit-link" and not @props.linkToModify
|
||||
|
@ -88,12 +91,12 @@ class FloatingToolbar extends React.Component
|
|||
<button className="btn btn-link toolbar-btn"
|
||||
onClick={@props.onClickLinkEditBtn}
|
||||
data-command-name="link"></button>
|
||||
{@_toolbarExtensions()}
|
||||
{@_toolbarExtensions(@state.extensions)}
|
||||
</div>
|
||||
|
||||
_toolbarExtensions: ->
|
||||
_toolbarExtensions: (extensions) ->
|
||||
buttons = []
|
||||
for extension in DraftStore.extensions()
|
||||
for extension in extensions
|
||||
toolbarItem = extension.composerToolbar?()
|
||||
if toolbarItem
|
||||
buttons.push(
|
||||
|
@ -102,6 +105,9 @@ class FloatingToolbar extends React.Component
|
|||
title="#{toolbarItem.tooltip}"><RetinaImg mode={RetinaImg.Mode.ContentIsMask} url="#{toolbarItem.iconUrl}" /></button>)
|
||||
return buttons
|
||||
|
||||
_onExtensionsChanged: =>
|
||||
@setState extensions: ExtensionRegistry.Composer.extensions()
|
||||
|
||||
_extensionMutateDom: (mutator) =>
|
||||
@props.onDomMutator(mutator)
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
_str = require 'underscore.string'
|
||||
{DOMUtils} = require 'nylas-exports'
|
||||
ContenteditablePlugin = require './contenteditable-plugin'
|
||||
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
|
||||
|
||||
class ListManager extends ContenteditablePlugin
|
||||
class ListManager extends ContenteditableExtension
|
||||
@onInput: (event, editableNode, selection) ->
|
||||
if @_spaceEntered and @hasListStartSignature(selection)
|
||||
@createList(event, selection)
|
||||
|
|
21
src/deprecate-utils.coffee
Normal file
21
src/deprecate-utils.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
class DeprecateUtils
|
||||
@warn: (condition, message) ->
|
||||
console.warn message if condition
|
||||
|
||||
@deprecate: (fnName, newName, ctx, fn) ->
|
||||
if NylasEnv.inDevMode
|
||||
warn = true
|
||||
newFn = =>
|
||||
DeprecateUtils.warn(
|
||||
warn,
|
||||
"Deprecation warning! #{fnName} is deprecated and will be removed soon.
|
||||
Use #{newName} instead."
|
||||
)
|
||||
warn = false
|
||||
return fn.apply(ctx, arguments)
|
||||
return _.extend(newFn, fn)
|
||||
return fn
|
||||
|
||||
module.exports = DeprecateUtils
|
63
src/extension-registry.es6
Normal file
63
src/extension-registry.es6
Normal file
|
@ -0,0 +1,63 @@
|
|||
|
||||
import _ from 'underscore';
|
||||
import {Listener, Publisher} from './flux/modules/reflux-coffee';
|
||||
import {includeModule} from './flux/coffee-helpers';
|
||||
|
||||
export class Registry {
|
||||
|
||||
static include = includeModule;
|
||||
|
||||
constructor(name, deprecationAdapter = (ext)=> ext) {
|
||||
this.name = name;
|
||||
this._deprecationAdapter = deprecationAdapter;
|
||||
this._registry = new Map();
|
||||
}
|
||||
|
||||
register(extension) {
|
||||
this.validateExtension(extension, 'register');
|
||||
this._registry.set(extension.name, this._deprecationAdapter(extension));
|
||||
this.triggerDebounced();
|
||||
return this;
|
||||
}
|
||||
|
||||
unregister(extension) {
|
||||
this.validateExtension(extension, 'unregister');
|
||||
this._registry.delete(extension.name);
|
||||
this.triggerDebounced();
|
||||
}
|
||||
|
||||
extensions() {
|
||||
return Array.from(this._registry.values());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._registry = new Map();
|
||||
}
|
||||
|
||||
triggerDebounced() {
|
||||
_.debounce(()=> this.trigger(), 1);
|
||||
}
|
||||
|
||||
validateExtension(extension, method) {
|
||||
if (!extension || Array.isArray(extension) || !_.isObject(extension)) {
|
||||
throw new Error(`ExtensionRegistry.${this.name}.${method} requires a valid \\
|
||||
extension object that implements one of the functions defined by ${this.name}Extension`);
|
||||
}
|
||||
if (!extension.name) {
|
||||
throw new Error(`ExtensionRegistry.${this.name}.${method} requires a \\
|
||||
\`name\` property defined on the extension object`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Registry.include(Publisher);
|
||||
Registry.include(Listener);
|
||||
|
||||
export const Composer = new Registry(
|
||||
'Composer',
|
||||
require('./flux/extensions/composer-extension-adapter')
|
||||
);
|
||||
|
||||
export const MessageView = new Registry(
|
||||
'MessageView',
|
||||
);
|
|
@ -296,7 +296,7 @@ class Actions
|
|||
|
||||
###
|
||||
Public: Send the draft with the given ID. This Action is handled by the {DraftStore},
|
||||
which finalizes the {DraftChangeSet} and allows {DraftStoreExtension}s to display
|
||||
which finalizes the {DraftChangeSet} and allows {ComposerExtension}s to display
|
||||
warnings and do post-processing. To change send behavior, you should consider using
|
||||
one of these objects rather than listening for the {sendDraft} action.
|
||||
|
||||
|
|
51
src/flux/extensions/composer-extension-adapter.coffee
Normal file
51
src/flux/extensions/composer-extension-adapter.coffee
Normal file
|
@ -0,0 +1,51 @@
|
|||
_ = require 'underscore'
|
||||
{deprecate} = require '../../deprecate-utils'
|
||||
DOMUtils = require '../../dom-utils'
|
||||
|
||||
ComposerExtensionAdapter = (extension) ->
|
||||
|
||||
if extension.onInput?.length <= 2
|
||||
origInput = extension.onInput
|
||||
extension.onInput = (event, editableNode, selection) ->
|
||||
origInput(editableNode, event)
|
||||
|
||||
extension.onInput = deprecate(
|
||||
"DraftStoreExtension.onInput",
|
||||
"ComposerExtension.onInput",
|
||||
extension,
|
||||
extension.onInput
|
||||
)
|
||||
|
||||
if extension.onTabDown?
|
||||
origKeyDown = extension.onKeyDown
|
||||
extension.onKeyDown = (event, editableNode, selection) ->
|
||||
if event.key is "Tab"
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
extension.onTabDown(editableNode, range, event)
|
||||
else
|
||||
origKeyDown?(event, editableNode, selection)
|
||||
|
||||
extension.onKeyDown = deprecate(
|
||||
"DraftStoreExtension.onTabDown",
|
||||
"ComposerExtension.onKeyDown",
|
||||
extension,
|
||||
extension.onKeyDown
|
||||
)
|
||||
|
||||
if extension.onMouseUp?
|
||||
origOnClick = extension.onClick
|
||||
extension.onClick = (event, editableNode, selection) ->
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
extension.onMouseUp(editableNode, range, event)
|
||||
origOnClick?(event, editableNode, selection)
|
||||
|
||||
extension.onClick = deprecate(
|
||||
"DraftStoreExtension.onMouseUp",
|
||||
"ComposerExtension.onClick",
|
||||
extension,
|
||||
extension.onClick
|
||||
)
|
||||
|
||||
return extension
|
||||
|
||||
module.exports = ComposerExtensionAdapter
|
109
src/flux/extensions/composer-extension.coffee
Normal file
109
src/flux/extensions/composer-extension.coffee
Normal file
|
@ -0,0 +1,109 @@
|
|||
###
|
||||
Public: To create ComposerExtensions that enhance the composer experience, you
|
||||
should create objects that implement the interface defined at {ComposerExtension}.
|
||||
|
||||
{ComposerExtension} extends {ContenteditableExtension}, so you can also
|
||||
implement the methods defined there to further enhance the composer
|
||||
experience.
|
||||
|
||||
To register your extension with the ExtensionRegistry, call {ExtensionRegistry::Composer::register}.
|
||||
When your package is being unloaded, you *must* call the corresponding
|
||||
{ExtensionRegistry::Composer::unregister} to unhook your extension.
|
||||
|
||||
```coffee
|
||||
activate: ->
|
||||
ExtensionRegistry.Composer.register(MyExtension)
|
||||
|
||||
...
|
||||
|
||||
deactivate: ->
|
||||
ExtensionRegistry.Composer.unregister(MyExtension)
|
||||
```
|
||||
|
||||
Your ComposerExtension should be stateless. The user may have multiple drafts
|
||||
open at any time, and the methods of your ComposerExtension may be called for different
|
||||
drafts at any time. You should not expect that the session you receive in
|
||||
{::finalizeSessionBeforeSending} is for the same draft you previously received in
|
||||
{::warningsForSending}, etc.
|
||||
|
||||
The ComposerExtension API does not currently expose any asynchronous or {Promise}-based APIs.
|
||||
This will likely change in the future. If you have a use-case for a ComposerExtension that
|
||||
is not possible with the current API, please let us know.
|
||||
|
||||
Section: Extensions
|
||||
###
|
||||
class ComposerExtension
|
||||
|
||||
###
|
||||
Public: Inspect the draft, and return any warnings that need to be displayed before
|
||||
the draft is sent. Warnings should be string phrases, such as "without an attachment"
|
||||
that fit into a message of the form: "Send #{phase1} and #{phase2}?"
|
||||
|
||||
- `draft`: A fully populated {Message} object that is about to be sent.
|
||||
|
||||
Returns a list of warning strings, or an empty array if no warnings need to be displayed.
|
||||
###
|
||||
@warningsForSending: (draft) ->
|
||||
[]
|
||||
|
||||
###
|
||||
Public: declare an icon to be displayed in the composer's toolbar (where
|
||||
bold, italic, underline, etc are).
|
||||
|
||||
You must return an object that contains the following properties:
|
||||
|
||||
- `mutator`: A function that's called when your toolbar button is
|
||||
clicked. This mutator function will be passed as its only argument the
|
||||
`dom`. The `dom` is the full {DOM} object of the current composer. You
|
||||
may mutate this in place. We don't care about the mutator's return
|
||||
value.
|
||||
|
||||
- `tooltip`: A one or two word description of what your icon does
|
||||
|
||||
- `iconUrl`: The url of your icon. It should be in the `nylas://` scheme.
|
||||
|
||||
For example: `nylas://your-package-name/assets/my-icon@2x.png`. Note, we
|
||||
will downsample your image by 2x (for Retina screens), so make sure it's
|
||||
twice the resolution. The icon should be black and white. We will
|
||||
directly pass the `url` prop of a {RetinaImg}
|
||||
###
|
||||
@composerToolbar: ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override prepareNewDraft to modify a brand new draft before it is displayed
|
||||
in a composer. This is one of the only places in the application where it's safe
|
||||
to modify the draft object you're given directly to add participants to the draft,
|
||||
add a signature, etc.
|
||||
|
||||
By default, new drafts are considered `pristine`. If the user leaves the composer
|
||||
without making any changes, the draft is discarded. If your extension populates
|
||||
the draft in a way that makes it "populated" in a valuable way, you should set
|
||||
`draft.pristine = false` so the draft saves, even if no further changes are made.
|
||||
###
|
||||
@prepareNewDraft: (draft) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override finalizeSessionBeforeSending in your ComposerExtension subclass to
|
||||
transform the {DraftStoreProxy} editing session just before the draft is sent. This method
|
||||
gives you an opportunity to make any final substitutions or changes after any
|
||||
{::warningsForSending} have been displayed.
|
||||
|
||||
- `session`: A {DraftStoreProxy} for the draft.
|
||||
|
||||
Example:
|
||||
|
||||
```coffee
|
||||
# Remove any <code> tags found in the draft body
|
||||
finalizeSessionBeforeSending: (session) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
```
|
||||
###
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
return
|
||||
|
||||
module.exports = ComposerExtension
|
146
src/flux/extensions/contenteditable-extension.coffee
Normal file
146
src/flux/extensions/contenteditable-extension.coffee
Normal file
|
@ -0,0 +1,146 @@
|
|||
###
|
||||
Public: ContenteditableExtension is an abstract base class. Implementations of this
|
||||
are used to make additional changes to a <Contenteditable /> component
|
||||
beyond a user's input intents. The hooks in this class provide the contenteditable
|
||||
DOM Node itself, allowing you to adjust selection ranges and change content
|
||||
as necessary.
|
||||
|
||||
While some ContenteditableExtension are included with the core
|
||||
<{Contenteditable} /> component, others may be added via the `plugins`
|
||||
prop when you use it inside your own components.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
<Contenteditable extensions={[MyAwesomeExtension]}>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If you specifically want to enhance the Composer experience you should register
|
||||
a {ComposerExtension}
|
||||
|
||||
Section: Extensions
|
||||
###
|
||||
class ContenteditableExtension
|
||||
|
||||
###
|
||||
Public: Override onInput in your Contenteditable subclass to implement custom
|
||||
behavior as the user types in the contenteditable's body field. You may mutate
|
||||
the contenteditable in place, we do not expect any return value from this method.
|
||||
|
||||
The onInput event can be triggered by a variety of events, some of which could
|
||||
have been already been looked at by a callback. Almost any DOM mutation will
|
||||
fire this event. Sometimes those mutations are the cause of other callbacks.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
|
||||
Example:
|
||||
|
||||
The Nylas `templates` package uses this method to see if the user has populated a
|
||||
`<code>` tag placed in the body and change it's CSS class to reflect that it is no
|
||||
longer empty.
|
||||
|
||||
```coffee
|
||||
onInput: (event, editableNode, selection) ->
|
||||
isWithinNode = (node) ->
|
||||
test = selection.baseNode
|
||||
while test isnt editableNode
|
||||
return true if test is node
|
||||
test = test.parentNode
|
||||
return false
|
||||
|
||||
codeTags = editableNode.querySelectorAll('code.var.empty')
|
||||
for codeTag in codeTags
|
||||
if selection.containsNode(codeTag) or isWithinNode(codeTag)
|
||||
codeTag.classList.remove('empty')
|
||||
```
|
||||
###
|
||||
@onInput: (event, editableNode, selection) ->
|
||||
|
||||
###
|
||||
Public: Override onBlur to mutate the contenteditable DOM node whenever the
|
||||
onBlur event is fired on it. You may mutate the contenteditable in place, we
|
||||
not expect any return value from this method.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
###
|
||||
@onBlur: (event, editableNode, selection) ->
|
||||
|
||||
###
|
||||
Public: Override onFocus to mutate the contenteditable DOM node whenever the
|
||||
onFocus event is fired on it. You may mutate the contenteditable in place, we
|
||||
not expect any return value from this method.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
###
|
||||
@onFocus: (event, editableNode, selection) ->
|
||||
|
||||
###
|
||||
Public: Override onClick to mutate the contenteditable DOM node whenever the
|
||||
onClick event is fired on it. You may mutate the contenteditable in place, we
|
||||
not expect any return value from this method.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
###
|
||||
@onClick: (event, editableNode, selection) ->
|
||||
|
||||
###
|
||||
Public: Override onKeyDown to mutate the contenteditable DOM node whenever the
|
||||
onKeyDown event is fired on it.
|
||||
Public: Called when the user presses a key while focused on the contenteditable's body field.
|
||||
Override onKeyDown in your ContenteditableExtension to adjust the selection or
|
||||
perform other actions.
|
||||
|
||||
If your package implements key down behavior for a particular scenario, you
|
||||
should prevent the default behavior of the key via `event.preventDefault()`.
|
||||
You may mutate the contenteditable in place, we not expect any return value
|
||||
from this method.
|
||||
|
||||
Important: You should prevent the default key down behavior with great care.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
###
|
||||
@onKeyDown: (event, editableNode, selection) ->
|
||||
|
||||
###
|
||||
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.
|
||||
|
||||
- event: DOM event fired on the contenteditable
|
||||
- editableNode: DOM node that represents the current contenteditable.This object
|
||||
can be mutated in place to modify the Contenteditable's content
|
||||
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
object that represents the current selection on the contenteditable
|
||||
- menu: [Menu](https://github.com/atom/electron/blob/master/docs/api/menu.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.
|
||||
###
|
||||
@onShowContextMenu: (event, editableNode, selection, menu) ->
|
||||
|
||||
module.exports = ContenteditableExtension
|
34
src/flux/extensions/message-view-extension.coffee
Normal file
34
src/flux/extensions/message-view-extension.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
###
|
||||
Public: To create MessageViewExtension that customize message viewing, you
|
||||
should create objects that implement the interface defined at {MessageViewExtension}.
|
||||
|
||||
To register your extension with the ExtensionRegistry, call {ExtensionRegistry::MessageView::registerExtension}.
|
||||
When your package is being unloaded, you *must* call the corresponding
|
||||
{ExtensionRegistry::MessageView::unregisterExtension} to unhook your extension.
|
||||
|
||||
```coffee
|
||||
activate: ->
|
||||
ExtensionRegistry.MessageView.register(MyExtension)
|
||||
|
||||
...
|
||||
|
||||
deactivate: ->
|
||||
ExtensionRegistry.MessageView.unregister(MyExtension)
|
||||
```
|
||||
|
||||
The MessageViewExtension API does not currently expose any asynchronous or {Promise}-based APIs.
|
||||
This will likely change in the future. If you have a use-case for a Message Store extension that
|
||||
is not possible with the current API, please let us know.
|
||||
|
||||
Section: Extensions
|
||||
###
|
||||
class MessageViewExtension
|
||||
|
||||
###
|
||||
Public: Transform the message body HTML provided in `body` and return HTML
|
||||
that should be displayed for the message.
|
||||
###
|
||||
@formatMessageBody: (body) ->
|
||||
return body
|
||||
|
||||
module.exports = MessageViewExtension
|
|
@ -1,174 +1,7 @@
|
|||
###
|
||||
Public: DraftStoreExtension is an abstract base class. To create DraftStoreExtensions
|
||||
that enhance the composer experience, you should subclass {DraftStoreExtension} and
|
||||
implement the class methods your plugin needs.
|
||||
|
||||
To register your extension with the DraftStore, call {DraftStore::registerExtension}.
|
||||
When your package is being unloaded, you *must* call the corresponding
|
||||
{DraftStore::unregisterExtension} to unhook your extension.
|
||||
|
||||
```coffee
|
||||
activate: ->
|
||||
DraftStore.registerExtension(MyExtension)
|
||||
|
||||
...
|
||||
|
||||
deactivate: ->
|
||||
DraftStore.unregisterExtension(MyExtension)
|
||||
```
|
||||
|
||||
Your DraftStoreExtension subclass should be stateless. The user may have multiple drafts
|
||||
open at any time, and the methods of your DraftStoreExtension may be called for different
|
||||
drafts at any time. You should not expect that the session you receive in
|
||||
{::finalizeSessionBeforeSending} is for the same draft you previously received in
|
||||
{::warningsForSending}, etc.
|
||||
|
||||
The DraftStoreExtension API does not currently expose any asynchronous or {Promise}-based APIs.
|
||||
This will likely change in the future. If you have a use-case for a Draft Store extension that
|
||||
is not possible with the current API, please let us know.
|
||||
|
||||
Section: Drafts
|
||||
Public: DraftStoreExtension is deprecated. Use {ComposerExtension} instead.
|
||||
Section: Extensions
|
||||
###
|
||||
class DraftStoreExtension
|
||||
|
||||
###
|
||||
Public: Inspect the draft, and return any warnings that need to be displayed before
|
||||
the draft is sent. Warnings should be string phrases, such as "without an attachment"
|
||||
that fit into a message of the form: "Send #{phase1} and #{phase2}?"
|
||||
|
||||
- `draft`: A fully populated {Message} object that is about to be sent.
|
||||
|
||||
Returns a list of warning strings, or an empty array if no warnings need to be displayed.
|
||||
###
|
||||
@warningsForSending: (draft) ->
|
||||
[]
|
||||
|
||||
###
|
||||
Public: declare an icon to be displayed in the composer's toolbar (where
|
||||
bold, italic, underline, etc are).
|
||||
|
||||
You must declare the following properties:
|
||||
|
||||
- `mutator`: A function that's called when your toolbar button is
|
||||
clicked. This mutator function will be passed as its only argument the
|
||||
`dom`. The `dom` is the full {DOM} object of the current composer. You
|
||||
may mutate this in place. We don't care about the mutator's return
|
||||
value.
|
||||
|
||||
- `tooltip`: A one or two word description of what your icon does
|
||||
|
||||
- `iconUrl`: The url of your icon. It should be in the `nylas://` scheme.
|
||||
For example: `nylas://your-package-name/assets/my-icon@2x.png`. Note, we
|
||||
will downsample your image by 2x (for Retina screens), so make sure it's
|
||||
twice the resolution. The icon should be black and white. We will
|
||||
directly pass the `url` prop of a {RetinaImg}
|
||||
###
|
||||
@composerToolbar: ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override prepareNewDraft to modify a brand new draft before it is displayed
|
||||
in a composer. This is one of the only places in the application where it's safe
|
||||
to modify the draft object you're given directly to add participants to the draft,
|
||||
add a signature, etc.
|
||||
|
||||
By default, new drafts are considered `pristine`. If the user leaves the composer
|
||||
without making any changes, the draft is discarded. If your extension populates
|
||||
the draft in a way that makes it "populated" in a valuable way, you should set
|
||||
`draft.pristine = false` so the draft saves, even if no further changes are made.
|
||||
###
|
||||
@prepareNewDraft: (draft) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override finalizeSessionBeforeSending in your DraftStoreExtension subclass to transform
|
||||
the {DraftStoreProxy} editing session just before the draft is sent. This method
|
||||
gives you an opportunity to make any final substitutions or changes after any
|
||||
{::warningsForSending} have been displayed.
|
||||
|
||||
- `session`: A {DraftStoreProxy} for the draft.
|
||||
|
||||
Example:
|
||||
|
||||
```coffee
|
||||
# Remove any <code> tags found in the draft body
|
||||
finalizeSessionBeforeSending: (session) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
```
|
||||
###
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override onMouseUp in your DraftStoreExtension subclass to
|
||||
listen for mouse up events sent to the composer's body text area. This
|
||||
hook provides the contenteditable DOM Node itself, allowing you to
|
||||
adjust selection ranges and change content as necessary.
|
||||
|
||||
- `editableNode` The composer's contenteditable {Node}
|
||||
that received the event.
|
||||
|
||||
- `range`: The currently selected {Range} in the `editableNode`
|
||||
|
||||
- `event`: The mouse up event.
|
||||
###
|
||||
@onMouseUp: (editableNode, range, event) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Called when the user presses `Tab` while focused on the composer's body field.
|
||||
Override onTabDown in your DraftStoreExtension to adjust the selection or perform
|
||||
other actions. If your package implements Tab behavior in a particular scenario, you
|
||||
should prevent the default behavior of Tab via `event.preventDefault()`.
|
||||
|
||||
Important: You should prevent the default tab behavior with great care.
|
||||
|
||||
- `editableNode` The composer's contenteditable {Node} that received the event.
|
||||
|
||||
- `range`: The currently selected {Range} in the `editableNode`
|
||||
|
||||
- `event`: The mouse up event.
|
||||
|
||||
###
|
||||
@onTabDown: (editableNode, range, event) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: Override onInput in your DraftStoreExtension subclass to
|
||||
implement custom behavior as the user types in the composer's
|
||||
contenteditable body field.
|
||||
|
||||
As the first argument you are passed the entire DOM object of the
|
||||
composer. You may mutate this object and edit it in place.
|
||||
|
||||
Example:
|
||||
|
||||
The Nylas `templates` package uses this method to see if the user has populated a
|
||||
`<code>` tag placed in the body and change it's CSS class to reflect that it is no
|
||||
longer empty.
|
||||
|
||||
```coffee
|
||||
onInput: (editableNode, event) ->
|
||||
selection = document.getSelection()
|
||||
|
||||
isWithinNode = (node) ->
|
||||
test = selection.baseNode
|
||||
while test isnt editableNode
|
||||
return true if test is node
|
||||
test = test.parentNode
|
||||
return false
|
||||
|
||||
codeTags = editableNode.querySelectorAll('code.var.empty')
|
||||
for codeTag in codeTags
|
||||
if selection.containsNode(codeTag) or isWithinNode(codeTag)
|
||||
codeTag.classList.remove('empty')
|
||||
```
|
||||
|
||||
###
|
||||
@onInput: (editableNode, event) ->
|
||||
return
|
||||
|
||||
module.exports = DraftStoreExtension
|
||||
|
|
|
@ -29,6 +29,9 @@ CoffeeHelpers = require '../coffee-helpers'
|
|||
DOMUtils = require '../../dom-utils'
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
|
||||
ExtensionRegistry = require '../../extension-registry'
|
||||
{deprecate} = require '../../deprecate-utils'
|
||||
|
||||
###
|
||||
Public: DraftStore responds to Actions that interact with Drafts and exposes
|
||||
public getter methods to return Draft objects and sessions.
|
||||
|
@ -72,7 +75,6 @@ class DraftStore
|
|||
NylasEnv.onBeforeUnload @_onBeforeUnload
|
||||
|
||||
@_draftSessions = {}
|
||||
@_extensions = []
|
||||
|
||||
@_inlineStylePromises = {}
|
||||
@_inlineStyleResolvers = {}
|
||||
|
@ -131,23 +133,25 @@ class DraftStore
|
|||
|
||||
# Public: Returns the extensions registered with the DraftStore.
|
||||
extensions: =>
|
||||
@_extensions
|
||||
ExtensionRegistry.Composer.extensions()
|
||||
|
||||
# Public: Registers a new extension with the DraftStore. DraftStore extensions
|
||||
# Public: Deprecated, use {ExtensionRegistry.Composer.register} instead.
|
||||
# Registers a new extension with the DraftStore. DraftStore extensions
|
||||
# make it possible to extend the editor experience, modify draft contents,
|
||||
# display warnings before draft are sent, and more.
|
||||
#
|
||||
# - `ext` A {DraftStoreExtension} instance.
|
||||
# - `ext` A {ComposerExtension} instance.
|
||||
#
|
||||
registerExtension: (ext) =>
|
||||
@_extensions.push(ext)
|
||||
ExtensionRegistry.Composer.register(ext)
|
||||
|
||||
# Public: Unregisters the extension provided from the DraftStore.
|
||||
# Public: Deprecated, use {ExtensionRegistry.Composer.unregister} instead.
|
||||
# Unregisters the extension provided from the DraftStore.
|
||||
#
|
||||
# - `ext` A {DraftStoreExtension} instance.
|
||||
# - `ext` A {ComposerExtension} instance.
|
||||
#
|
||||
unregisterExtension: (ext) =>
|
||||
@_extensions = _.without(@_extensions, ext)
|
||||
ExtensionRegistry.Composer.unregister(ext)
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
|
@ -228,7 +232,7 @@ class DraftStore
|
|||
|
||||
_finalizeAndPersistNewMessage: (draft) =>
|
||||
# Give extensions an opportunity to perform additional setup to the draft
|
||||
for extension in @_extensions
|
||||
for extension in @extensions()
|
||||
continue unless extension.prepareNewDraft
|
||||
extension.prepareNewDraft(draft)
|
||||
|
||||
|
@ -516,7 +520,7 @@ class DraftStore
|
|||
|
||||
# Give third-party plugins an opportunity to sanitize draft data
|
||||
_runExtensionsBeforeSend: (session) ->
|
||||
for extension in @_extensions
|
||||
for extension in @extensions()
|
||||
continue unless extension.finalizeSessionBeforeSending
|
||||
extension.finalizeSessionBeforeSending(session)
|
||||
|
||||
|
@ -537,4 +541,19 @@ class DraftStore
|
|||
NylasEnv.showErrorDialog(errorMessage)
|
||||
, 100
|
||||
|
||||
module.exports = new DraftStore()
|
||||
|
||||
# Deprecations
|
||||
store = new DraftStore()
|
||||
store.registerExtension = deprecate(
|
||||
'DraftStore.registerExtension',
|
||||
'ExtensionRegistry.Composer.register',
|
||||
store,
|
||||
store.registerExtension
|
||||
)
|
||||
store.unregisterExtension = deprecate(
|
||||
'DraftStore.unregisterExtension',
|
||||
'ExtensionRegistry.Composer.unregister',
|
||||
store,
|
||||
store.unregisterExtension
|
||||
)
|
||||
module.exports = store
|
||||
|
|
|
@ -1,35 +1,7 @@
|
|||
###
|
||||
Public: MessageStoreExtension is an abstract base class. To create MessageStoreExtension
|
||||
that customize message viewing, you should subclass {MessageStoreExtension} and
|
||||
implement the class methods your plugin needs.
|
||||
|
||||
To register your extension with the MessageStore, call {MessageStore::registerExtension}.
|
||||
When your package is being unloaded, you *must* call the corresponding
|
||||
{MessageStore::unregisterExtension} to unhook your extension.
|
||||
|
||||
```coffee
|
||||
activate: ->
|
||||
MessageStore.registerExtension(MyExtension)
|
||||
|
||||
...
|
||||
|
||||
deactivate: ->
|
||||
MessageStore.unregisterExtension(MyExtension)
|
||||
```
|
||||
|
||||
The MessageStoreExtension API does not currently expose any asynchronous or {Promise}-based APIs.
|
||||
This will likely change in the future. If you have a use-case for a Message Store extension that
|
||||
is not possible with the current API, please let us know.
|
||||
|
||||
Section: Stores
|
||||
Public: MessageStoreExtension is deprecated. Use {MessageViewExtension} instead.
|
||||
Section: Extensions
|
||||
###
|
||||
class MessageStoreExtension
|
||||
|
||||
###
|
||||
Public: Transform the message body HTML provided in `body` and return HTML
|
||||
that should be displayed for the message.
|
||||
###
|
||||
@formatMessageBody: (body) ->
|
||||
return body
|
||||
|
||||
module.exports = MessageStoreExtension
|
||||
|
|
|
@ -8,6 +8,8 @@ AccountStore = require "./account-store"
|
|||
FocusedContentStore = require "./focused-content-store"
|
||||
ChangeUnreadTask = require '../tasks/change-unread-task'
|
||||
NylasAPI = require '../nylas-api'
|
||||
ExtensionRegistry = require '../../extension-registry'
|
||||
{deprecate} = require '../../deprecate-utils'
|
||||
async = require 'async'
|
||||
_ = require 'underscore'
|
||||
|
||||
|
@ -43,25 +45,27 @@ class MessageStore extends NylasStore
|
|||
|
||||
# Public: Returns the extensions registered with the MessageStore.
|
||||
extensions: =>
|
||||
@_extensions
|
||||
ExtensionRegistry.MessageView.extensions()
|
||||
|
||||
# Public: Registers a new extension with the MessageStore. MessageStore extensions
|
||||
# Public: Deprecated, use {ExtensionRegistry.MessageView.register} instead.
|
||||
# Registers a new extension with the MessageStore. MessageStore extensions
|
||||
# make it possible to customize message body parsing, and will do more in
|
||||
# the future.
|
||||
#
|
||||
# - `ext` A {MessageStoreExtension} instance.
|
||||
# - `ext` A {MessageViewExtension} instance.
|
||||
#
|
||||
registerExtension: (ext) =>
|
||||
@_extensions.push(ext)
|
||||
MessageBodyProcessor = require './message-body-processor'
|
||||
MessageBodyProcessor.resetCache()
|
||||
ExtensionRegistry.MessageView.register(ext)
|
||||
|
||||
# Public: Unregisters the extension provided from the MessageStore.
|
||||
# Public: Deprecated, use {ExtensionRegistry.MessageView.unregister} instead.
|
||||
# Unregisters the extension provided from the MessageStore.
|
||||
#
|
||||
# - `ext` A {MessageStoreExtension} instance.
|
||||
# - `ext` A {MessageViewExtension} instance.
|
||||
#
|
||||
unregisterExtension: (ext) =>
|
||||
@_extensions = _.without(@_extensions, ext)
|
||||
ExtensionRegistry.MessageView.unregister(ext)
|
||||
|
||||
_onExtensionsChanged: (role) ->
|
||||
MessageBodyProcessor = require './message-body-processor'
|
||||
MessageBodyProcessor.resetCache()
|
||||
|
||||
|
@ -73,10 +77,10 @@ class MessageStore extends NylasStore
|
|||
@_itemsExpanded = {}
|
||||
@_itemsLoading = false
|
||||
@_thread = null
|
||||
@_extensions = []
|
||||
@_inflight = {}
|
||||
|
||||
_registerListeners: ->
|
||||
@listenTo ExtensionRegistry.MessageView, @_onExtensionsChanged
|
||||
@listenTo DatabaseStore, @_onDataChanged
|
||||
@listenTo FocusedContentStore, @_onFocusChanged
|
||||
@listenTo Actions.toggleMessageIdExpanded, @_onToggleMessageIdExpanded
|
||||
|
@ -287,4 +291,17 @@ class MessageStore extends NylasStore
|
|||
|
||||
items
|
||||
|
||||
module.exports = new MessageStore()
|
||||
store = new MessageStore()
|
||||
store.registerExtension = deprecate(
|
||||
'MessageStore.registerExtension',
|
||||
'ExtensionRegistry.MessageView.register',
|
||||
store,
|
||||
store.registerExtension
|
||||
)
|
||||
store.unregisterExtension = deprecate(
|
||||
'MessageStore.unregisterExtension',
|
||||
'ExtensionRegistry.MessageView.unregister',
|
||||
store,
|
||||
store.unregisterExtension
|
||||
)
|
||||
module.exports = store
|
||||
|
|
|
@ -33,6 +33,15 @@ class NylasExports
|
|||
NylasExports.registerSerializable(exported)
|
||||
@[prop] = exported
|
||||
|
||||
@requireDeprecated = (prop, path, {instead} = {}) ->
|
||||
{deprecate} = require '../deprecate-utils'
|
||||
Object.defineProperty @, prop,
|
||||
get: deprecate prop, instead, @, ->
|
||||
exported = require "../#{path}"
|
||||
NylasExports.registerSerializable(exported)
|
||||
return exported
|
||||
enumerable: true
|
||||
|
||||
# Actions
|
||||
@load "Actions", 'flux/actions'
|
||||
|
||||
|
@ -108,14 +117,24 @@ class NylasExports
|
|||
@require "ThreadCountsStore", 'flux/stores/thread-counts-store'
|
||||
@require "UnreadBadgeStore", 'flux/stores/unread-badge-store'
|
||||
@require "FileDownloadStore", 'flux/stores/file-download-store'
|
||||
@require "DraftStoreExtension", 'flux/stores/draft-store-extension'
|
||||
@require "FocusedContentStore", 'flux/stores/focused-content-store'
|
||||
@require "FocusedMailViewStore", 'flux/stores/focused-mail-view-store'
|
||||
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
|
||||
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
|
||||
@require "MessageStoreExtension", 'flux/stores/message-store-extension'
|
||||
@require "PreferencesUIStore", 'flux/stores/preferences-ui-store'
|
||||
|
||||
# Deprecated
|
||||
@requireDeprecated "DraftStoreExtension", 'flux/stores/draft-store-extension',
|
||||
instead: 'ComposerExtension'
|
||||
@requireDeprecated "MessageStoreExtension", 'flux/stores/message-store-extension',
|
||||
instead: 'MessageViewExtension'
|
||||
|
||||
# Extensions
|
||||
@require "ExtensionRegistry", 'extension-registry'
|
||||
@require "ContenteditableExtension", 'flux/extensions/contenteditable-extension'
|
||||
@require "ComposerExtension", 'flux/extensions/composer-extension'
|
||||
@require "MessageViewExtension", 'flux/extensions/message-view-extension'
|
||||
|
||||
# React Components
|
||||
@get "React", -> require 'react' # Our version of React for 3rd party use
|
||||
@load "ReactRemote", 'react-remote/react-remote-parent'
|
||||
|
@ -146,9 +165,6 @@ class NylasExports
|
|||
@load "BufferedProcess", 'buffered-process'
|
||||
@get "APMWrapper", -> require('../apm-wrapper')
|
||||
|
||||
# Contenteditable
|
||||
@load "ContenteditablePlugin", 'components/contenteditable/contenteditable-plugin'
|
||||
|
||||
# Testing
|
||||
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'
|
||||
|
||||
|
|
Loading…
Reference in a new issue