mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-27 19:07:15 +08:00
docs(*): Additional docs for DraftStoreExtensions
This commit is contained in:
parent
ef27f30096
commit
892205f431
8 changed files with 218 additions and 39 deletions
|
@ -37,12 +37,15 @@ standardClasses = [
|
|||
'typeerror',
|
||||
'syntaxerror',
|
||||
'referenceerror',
|
||||
'rangeerror',
|
||||
'rangeerror'
|
||||
]
|
||||
|
||||
thirdPartyClasses = {
|
||||
'react.component': 'https://facebook.github.io/react/docs/component-api.html',
|
||||
'promise': 'https://github.com/petkaantonov/bluebird/blob/master/API.md'
|
||||
'promise': 'https://github.com/petkaantonov/bluebird/blob/master/API.md',
|
||||
'range': 'https://developer.mozilla.org/en-US/docs/Web/API/Range',
|
||||
'selection': 'https://developer.mozilla.org/en-US/docs/Web/API/Selection',
|
||||
'node': 'https://developer.mozilla.org/en-US/docs/Web/API/Node',
|
||||
}
|
||||
|
||||
module.exports = (grunt) ->
|
||||
|
|
|
@ -1,21 +1,40 @@
|
|||
```
|
||||
DraftStore.registerExtension(Extension)
|
||||
###Extending the Composer Experience
|
||||
|
||||
The composer lies at the heart of Nylas Mail, and many improvements to the mail experience require deep integration with the composer. To enable these sort of plugins, the DraftStore exposes an extension API.
|
||||
|
||||
This API allows your package to:
|
||||
|
||||
- Display warning messages before a draft is sent. (ie: "Are you sure you want to send this without attaching a proposal?")
|
||||
|
||||
- Intercept keyboard and mouse events to the composer's text editor.
|
||||
|
||||
- Transform the draft and make additional changes before it is sent.
|
||||
|
||||
To create a Draft Store Extension, subclass {DraftStoreExtension} and override the methods your extension needs. See {DraftStoreExtension} for a complete list of the methods your extension can implement. In the sample packages repository, [templates]() is an example of a package which uses a DraftStoreExtension to enhance the composer experience.
|
||||
|
||||
####Example:
|
||||
|
||||
This extension displays a warning before sending a draft that contains the names of competitor's products and if the user proceeds to send the draft containing the words, it appends a disclaimer.
|
||||
|
||||
```
|
||||
{DraftStoreExtension} = require 'inbox-exports'
|
||||
|
||||
class ProductsExtension extends DraftStoreExtension
|
||||
|
||||
@warningsForSending: (draft) ->
|
||||
words = ['iphone', 'ipad', 'apple', 'iwatch', 'macbook']
|
||||
body = draft.body.toLowercase()
|
||||
for word in words
|
||||
if body.indexOf(word) > 0
|
||||
return ["with the word '#{word}'?"]
|
||||
return []
|
||||
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
draft = session.draft()
|
||||
if @warningsForSending(draft)
|
||||
bodyWithWarning = draft.body += "<br>This email \
|
||||
contains competitor's product names \
|
||||
or trademarks used in context."
|
||||
session.changes.add(body: bodyWithWarning)
|
||||
```
|
||||
|
||||
module.exports =
|
||||
warningsForSending: (draft) ->
|
||||
warnings = []
|
||||
if draft.body.search(/<code[^>]*empty[^>]*>/i) > 0
|
||||
warnings.push("with an empty template area")
|
||||
warnings
|
||||
|
||||
finalizeSessionBeforeSending: (session) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
|
||||
```
|
|
@ -41,6 +41,7 @@ Exports =
|
|||
|
||||
# Stores
|
||||
DraftStore: require '../src/flux/stores/draft-store'
|
||||
DraftStoreExtension: require '../src/flux/stores/draft-store-extension'
|
||||
MessageStore: require '../src/flux/stores/message-store'
|
||||
ContactStore: require '../src/flux/stores/contact-store'
|
||||
NamespaceStore: require '../src/flux/stores/namespace-store'
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
{DraftStoreExtension} = require 'inbox-exports'
|
||||
|
||||
module.exports =
|
||||
warningsForSending: (draft) ->
|
||||
class TemplatesDraftStoreExtension extends DraftStoreExtension
|
||||
|
||||
@warningsForSending: (draft) ->
|
||||
warnings = []
|
||||
if draft.body.search(/<code[^>]*empty[^>]*>/i) > 0
|
||||
warnings.push("with an empty template area")
|
||||
warnings
|
||||
|
||||
finalizeSessionBeforeSending: (session) ->
|
||||
|
||||
@finalizeSessionBeforeSending: (session) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
|
||||
onMouseUp: (editableNode, range, event) ->
|
||||
@onMouseUp: (editableNode, range, event) ->
|
||||
parent = range.startContainer?.parentNode
|
||||
parentCodeNode = null
|
||||
|
||||
|
@ -29,14 +31,14 @@ module.exports =
|
|||
selection = document.getSelection()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
|
||||
onFocusPrevious: (editableNode, range, event) ->
|
||||
|
||||
@onFocusPrevious: (editableNode, range, event) ->
|
||||
@onFocusShift(editableNode, range, event, -1)
|
||||
|
||||
onFocusNext: (editableNode, range, event) ->
|
||||
@onFocusNext: (editableNode, range, event) ->
|
||||
@onFocusShift(editableNode, range, event, 1)
|
||||
|
||||
onFocusShift: (editableNode, range, event, delta) ->
|
||||
@onFocusShift: (editableNode, range, event, delta) ->
|
||||
return unless range
|
||||
|
||||
# Try to find the node that the selection range is
|
||||
|
@ -95,7 +97,7 @@ module.exports =
|
|||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
onInput: (editableNode, event) ->
|
||||
@onInput: (editableNode, event) ->
|
||||
selection = document.getSelection()
|
||||
|
||||
isWithinNode = (node) ->
|
||||
|
@ -109,3 +111,6 @@ module.exports =
|
|||
for codeTag in codeTags
|
||||
if selection.containsNode(codeTag) or isWithinNode(codeTag)
|
||||
codeTag.classList.remove('empty')
|
||||
|
||||
|
||||
module.exports = TemplatesDraftStoreExtension
|
|
@ -3,10 +3,13 @@
|
|||
|
||||
@code-bg-color: #fcf4db;
|
||||
|
||||
.template-picker .menu {
|
||||
.content-container {
|
||||
height:150px;
|
||||
overflow-y:scroll;
|
||||
.template-picker {
|
||||
order:2;
|
||||
.menu {
|
||||
.content-container {
|
||||
height:150px;
|
||||
overflow-y:scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +23,10 @@
|
|||
padding-left: @padding-small-horizontal;
|
||||
padding-right: @padding-small-horizontal;
|
||||
font-size: @font-size-small;
|
||||
display:inline-block;
|
||||
margin:auto;
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 530px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.compose-body #contenteditable {
|
||||
|
|
|
@ -39,7 +39,7 @@ class Popover extends React.Component
|
|||
|
||||
###
|
||||
Public: React `props` supported by Popover:
|
||||
|
||||
|
||||
- `buttonComponent` The React element that will be rendered in place of the Popover and trigger it to appear. This is typically a button or call-to-action for opening the
|
||||
popover. Popover wraps this item in a <div> with an onClick handler.
|
||||
|
||||
|
@ -75,7 +75,7 @@ class Popover extends React.Component
|
|||
wrappedButtonComponent = []
|
||||
if @props.buttonComponent
|
||||
wrappedButtonComponent = <div onClick={@_onClick}>{@props.buttonComponent}</div>
|
||||
|
||||
|
||||
popoverComponent = []
|
||||
if @state.showing
|
||||
popoverComponent = <div ref="popover" className="popover">
|
||||
|
@ -95,13 +95,13 @@ class Popover extends React.Component
|
|||
if showing
|
||||
setTimeout =>
|
||||
# Automatically focus the element inside us with the lowest tab index
|
||||
node = @refs.popover.findDOMNode()
|
||||
node = React.findDOMNode(@refs.popover)
|
||||
matches = _.sortBy node.querySelectorAll("[tabIndex]"), (a,b) -> a.tabIndex < b.tabIndex
|
||||
matches[0].focus() if matches[0]
|
||||
|
||||
_onBlur: (event) =>
|
||||
target = event.nativeEvent.relatedTarget
|
||||
if target? and @refs.container.findDOMNode().contains(target)
|
||||
if target? and React.findDOMNode(@refs.container).contains(target)
|
||||
return
|
||||
@setState
|
||||
showing:false
|
||||
|
|
146
src/flux/stores/draft-store-extension.coffee
Normal file
146
src/flux/stores/draft-store-extension.coffee
Normal file
|
@ -0,0 +1,146 @@
|
|||
###
|
||||
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.
|
||||
|
||||
```
|
||||
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.
|
||||
###
|
||||
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: Override onMouseUp 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:
|
||||
|
||||
```
|
||||
# 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 `Shift-Tab` while focused on the composer's body field.
|
||||
Override onFocusPrevious in your DraftStoreExtension to adjust the selection or perform
|
||||
other actions. If your package implements Shift-Tab behavior in a particular scenario, you
|
||||
should prevent the default behavior of Shift-Tab via `event.preventDefault()`.
|
||||
|
||||
- `editableNode` The composer's contenteditable {Node} that received the event.
|
||||
|
||||
- `range`: The currently selected {Range} in the `editableNode`
|
||||
|
||||
- `event`: The mouse up event.
|
||||
|
||||
###
|
||||
@onFocusPrevious: (editableNode, range, event) ->
|
||||
return
|
||||
|
||||
|
||||
###
|
||||
Public: Called when the user presses `Tab` while focused on the composer's body field.
|
||||
Override onFocusPrevious 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()`.
|
||||
|
||||
- `editableNode` The composer's contenteditable {Node} that received the event.
|
||||
|
||||
- `range`: The currently selected {Range} in the `editableNode`
|
||||
|
||||
- `event`: The mouse up event.
|
||||
|
||||
###
|
||||
@onFocusNext: (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.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
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
|
|
@ -73,7 +73,7 @@ button, html input[type="button"] {
|
|||
}
|
||||
|
||||
.btn-toolbar {
|
||||
min-height:34px;
|
||||
min-height:36px;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
|
|
Loading…
Reference in a new issue