mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-26 01:53:13 +08:00
feat(templates): Final examples package is in - templates!
This commit is contained in:
parent
28bf9117bd
commit
94bdcc6900
16 changed files with 595 additions and 38 deletions
|
@ -0,0 +1,20 @@
|
|||
<p>
|
||||
Hi there <code class="var empty">First Name</code>,
|
||||
</p>
|
||||
<p>
|
||||
Welcome to the templates package! Templates live in the <strong>~/.nylas/templates</strong>
|
||||
directory on your computer. Each template is an HTML file - the name of the
|
||||
file is the name of the template, and it's contents are the default message body.
|
||||
</p>
|
||||
<p>
|
||||
If you include HTML <code> tags in your template, you can create
|
||||
regions that you can jump between and fill easily. Check out the source of
|
||||
the template for a <code class="var">super awesome</code> example!
|
||||
</p>
|
||||
<p>
|
||||
Give <code> tags the `var` class to mark them as template regions. Add
|
||||
the `empty` class to make them dark yellow. When you send your message, <code>
|
||||
tags are always stripped so the recipient never sees any highlighting.
|
||||
<p>
|
||||
- Nylas Team
|
||||
</p>
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
examples/N1-Composer-Templates/icon.png
Normal file
BIN
examples/N1-Composer-Templates/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
23
examples/N1-Composer-Templates/lib/main.cjsx
Normal file
23
examples/N1-Composer-Templates/lib/main.cjsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
{ComponentRegistry, DraftStore, React} = require 'nylas-exports'
|
||||
TemplatePicker = require './template-picker'
|
||||
TemplateStatusBar = require './template-status-bar'
|
||||
Extension = require './template-draft-extension'
|
||||
|
||||
module.exports =
|
||||
item: null # The DOM item the main React component renders into
|
||||
|
||||
activate: (@state={}) ->
|
||||
ComponentRegistry.register TemplatePicker,
|
||||
role: 'Composer:ActionButton'
|
||||
|
||||
ComponentRegistry.register TemplateStatusBar,
|
||||
role: 'Composer:Footer'
|
||||
|
||||
DraftStore.registerExtension(Extension)
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister(TemplatePicker)
|
||||
ComponentRegistry.unregister(TemplateStatusBar)
|
||||
DraftStore.unregisterExtension(Extension)
|
||||
|
||||
serialize: -> @state
|
|
@ -0,0 +1,92 @@
|
|||
{DraftStoreExtension} = require 'nylas-exports'
|
||||
|
||||
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) ->
|
||||
body = session.draft().body
|
||||
clean = body.replace(/<\/?code[^>]*>/g, '')
|
||||
if body != clean
|
||||
session.changes.add(body: clean)
|
||||
|
||||
@onMouseUp: (editableNode, range, event) ->
|
||||
parent = range.startContainer?.parentNode
|
||||
parentCodeNode = null
|
||||
|
||||
while parent and parent isnt editableNode
|
||||
if parent.classList?.contains('var') and parent.tagName is 'CODE'
|
||||
parentCodeNode = parent
|
||||
break
|
||||
parent = parent.parentNode
|
||||
|
||||
isSinglePoint = range.startContainer is range.endContainer and range.startOffset is range.endOffset
|
||||
|
||||
if isSinglePoint and parentCodeNode
|
||||
range.selectNode(parentCodeNode)
|
||||
selection = document.getSelection()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
|
||||
@onTabDown: (editableNode, range, event) ->
|
||||
if event.shiftKey
|
||||
@onTabSelectNextVar(editableNode, range, event, -1)
|
||||
else
|
||||
@onTabSelectNextVar(editableNode, range, event, 1)
|
||||
|
||||
@onTabSelectNextVar: (editableNode, range, event, delta) ->
|
||||
return unless range
|
||||
|
||||
# Try to find the node that the selection range is
|
||||
# currently intersecting with (inside, or around)
|
||||
parentCodeNode = null
|
||||
nodes = editableNode.querySelectorAll('code.var')
|
||||
for node in nodes
|
||||
if range.intersectsNode(node)
|
||||
parentCodeNode = node
|
||||
|
||||
if parentCodeNode
|
||||
if range.startOffset is range.endOffset and parentCodeNode.classList.contains('empty')
|
||||
# If the current node is empty and it's a single insertion point,
|
||||
# select the current node rather than advancing to the next node
|
||||
selectNode = parentCodeNode
|
||||
else
|
||||
# advance to the next code node
|
||||
matches = editableNode.querySelectorAll('code.var')
|
||||
matchIndex = -1
|
||||
for match, idx in matches
|
||||
if match is parentCodeNode
|
||||
matchIndex = idx
|
||||
break
|
||||
if matchIndex != -1 and matchIndex + delta >= 0 and matchIndex + delta < matches.length
|
||||
selectNode = matches[matchIndex+delta]
|
||||
|
||||
if selectNode
|
||||
range.selectNode(selectNode)
|
||||
selection = document.getSelection()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@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')
|
||||
|
||||
|
||||
module.exports = TemplatesDraftStoreExtension
|
86
examples/N1-Composer-Templates/lib/template-picker.cjsx
Normal file
86
examples/N1-Composer-Templates/lib/template-picker.cjsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
{Actions, Message, DatabaseStore, React} = require 'nylas-exports'
|
||||
{Popover, Menu, RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
TemplateStore = require './template-store'
|
||||
|
||||
class TemplatePicker extends React.Component
|
||||
@displayName: 'TemplatePicker'
|
||||
|
||||
@containerStyles:
|
||||
order:2
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
searchValue: ""
|
||||
templates: TemplateStore.items()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = TemplateStore.listen @_onStoreChange
|
||||
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe() if @unsubscribe
|
||||
|
||||
render: =>
|
||||
button = <button className="btn btn-toolbar narrow">
|
||||
<RetinaImg url="nylas://N1-Composer-Templates/assets/icon-composer-templates@2x.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
|
||||
<RetinaImg name="icon-composer-dropdown.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
|
||||
headerComponents = [
|
||||
<input type="text"
|
||||
tabIndex="1"
|
||||
key="textfield"
|
||||
className="search"
|
||||
value={@state.searchValue}
|
||||
onChange={@_onSearchValueChange}/>
|
||||
]
|
||||
|
||||
footerComponents = [
|
||||
<div className="item" key="new" onMouseDown={@_onNewTemplate}>Save Draft as Template...</div>
|
||||
<div className="item" key="manage" onMouseDown={@_onManageTemplates}>Open Templates Folder...</div>
|
||||
]
|
||||
|
||||
<Popover ref="popover" className="template-picker pull-right" buttonComponent={button}>
|
||||
<Menu ref="menu"
|
||||
headerComponents={headerComponents}
|
||||
footerComponents={footerComponents}
|
||||
items={@state.templates}
|
||||
itemKey={ (item) -> item.id }
|
||||
itemContent={ (item) -> item.name }
|
||||
onSelect={@_onChooseTemplate}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
|
||||
_filteredTemplates: (search) =>
|
||||
search ?= @state.searchValue
|
||||
items = TemplateStore.items()
|
||||
|
||||
return items unless search.length
|
||||
|
||||
items.filter (t) ->
|
||||
t.name.toLowerCase().indexOf(search.toLowerCase()) == 0
|
||||
|
||||
_onStoreChange: =>
|
||||
@setState
|
||||
templates: @_filteredTemplates()
|
||||
|
||||
_onSearchValueChange: =>
|
||||
newSearch = event.target.value
|
||||
@setState
|
||||
searchValue: newSearch
|
||||
templates: @_filteredTemplates(newSearch)
|
||||
|
||||
_onChooseTemplate: (template) =>
|
||||
Actions.insertTemplateId({templateId:template.id, draftClientId: @props.draftClientId})
|
||||
@refs.popover.close()
|
||||
|
||||
_onManageTemplates: =>
|
||||
Actions.showTemplates()
|
||||
|
||||
_onNewTemplate: =>
|
||||
Actions.createTemplate({draftClientId: @props.draftClientId})
|
||||
|
||||
|
||||
module.exports = TemplatePicker
|
45
examples/N1-Composer-Templates/lib/template-status-bar.cjsx
Normal file
45
examples/N1-Composer-Templates/lib/template-status-bar.cjsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
{Actions, Message, DraftStore, React} = require 'nylas-exports'
|
||||
|
||||
class TemplateStatusBar extends React.Component
|
||||
@displayName: 'TemplateStatusBar'
|
||||
|
||||
@containerStyles:
|
||||
textAlign:'center'
|
||||
width:530
|
||||
margin:'auto'
|
||||
|
||||
@propTypes:
|
||||
draftClientId: React.PropTypes.string
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = draft: null
|
||||
|
||||
componentDidMount: =>
|
||||
DraftStore.sessionForClientId(@props.draftClientId).then (_proxy) =>
|
||||
return if @_unmounted
|
||||
return unless _proxy.draftClientId is @props.draftClientId
|
||||
@_proxy = _proxy
|
||||
@unsubscribe = @_proxy.listen(@_onDraftChange, @)
|
||||
@_onDraftChange()
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_unmounted = true
|
||||
@unsubscribe() if @unsubscribe
|
||||
|
||||
render: =>
|
||||
if @_draftUsesTemplate()
|
||||
<div className="template-status-bar">
|
||||
Press "tab" to quickly fill in the blanks - highlighting will not be visible to recipients.
|
||||
</div>
|
||||
else
|
||||
<div></div>
|
||||
|
||||
_onDraftChange: =>
|
||||
@setState(draft: @_proxy.draft())
|
||||
|
||||
_draftUsesTemplate: =>
|
||||
return unless @state.draft
|
||||
@state.draft.body.search(/<code[^>]*class="var[^>]*>/i) > 0
|
||||
|
||||
|
||||
module.exports = TemplateStatusBar
|
109
examples/N1-Composer-Templates/lib/template-store.coffee
Normal file
109
examples/N1-Composer-Templates/lib/template-store.coffee
Normal file
|
@ -0,0 +1,109 @@
|
|||
{DatabaseStore, DraftStore, Actions, Message, React} = require 'nylas-exports'
|
||||
NylasStore = require 'nylas-store'
|
||||
shell = require 'shell'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
|
||||
class TemplateStore extends NylasStore
|
||||
constructor: ->
|
||||
@_setStoreDefaults()
|
||||
@_registerListeners()
|
||||
|
||||
@_templatesDir = path.join(atom.getConfigDirPath(), 'templates')
|
||||
@_welcomeName = 'Welcome to Templates.html'
|
||||
@_welcomePath = path.join(__dirname, '..', 'assets', @_welcomeName)
|
||||
|
||||
# I know this is a bit of pain but don't do anything that
|
||||
# could possibly slow down app launch
|
||||
fs.exists @_templatesDir, (exists) =>
|
||||
if exists
|
||||
@_populate()
|
||||
fs.watch @_templatesDir, => @_populate()
|
||||
else
|
||||
fs.mkdir @_templatesDir, =>
|
||||
fs.readFile @_welcomePath, (err, welcome) =>
|
||||
fs.writeFile path.join(@_templatesDir, @_welcomeName), welcome, (err) =>
|
||||
fs.watch @_templatesDir, => @_populate()
|
||||
|
||||
|
||||
########### PUBLIC #####################################################
|
||||
|
||||
items: =>
|
||||
@_items
|
||||
|
||||
templatesDirectory: =>
|
||||
@_templatesDir
|
||||
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_setStoreDefaults: =>
|
||||
@_items = []
|
||||
|
||||
_registerListeners: =>
|
||||
@listenTo Actions.insertTemplateId, @_onInsertTemplateId
|
||||
@listenTo Actions.createTemplate, @_onCreateTemplate
|
||||
@listenTo Actions.showTemplates, @_onShowTemplates
|
||||
|
||||
_populate: =>
|
||||
fs.readdir @_templatesDir, (err, filenames) =>
|
||||
@_items = []
|
||||
for filename in filenames
|
||||
continue if filename[0] is '.'
|
||||
displayname = path.basename(filename, path.extname(filename))
|
||||
@_items.push
|
||||
id: filename,
|
||||
name: displayname,
|
||||
path: path.join(@_templatesDir, filename)
|
||||
@trigger(@)
|
||||
|
||||
_onCreateTemplate: ({draftClientId, name, contents} = {}) =>
|
||||
if draftClientId
|
||||
DraftStore.sessionForClientId(draftClientId).then (session) =>
|
||||
draft = session.draft()
|
||||
name ?= draft.subject
|
||||
contents ?= draft.body
|
||||
if not name or name.length is 0
|
||||
return @_displayError("Give your draft a subject to name your template.")
|
||||
if not contents or contents.length is 0
|
||||
return @_displayError("To create a template you need to fill the body of the current draft.")
|
||||
@_writeTemplate(name, contents)
|
||||
|
||||
else
|
||||
if not name or name.length is 0
|
||||
return @_displayError("You must provide a name for your template.")
|
||||
if not contents or contents.length is 0
|
||||
return @_displayError("You must provide contents for your template.")
|
||||
@_writeTemplate(name, contents)
|
||||
|
||||
_onShowTemplates: =>
|
||||
shell.showItemInFolder(@_items[0]?.path || @_templatesDir)
|
||||
|
||||
_displayError: (message) =>
|
||||
dialog = require('remote').require('dialog')
|
||||
dialog.showErrorBox('Template Creation Error', message)
|
||||
|
||||
_writeTemplate: (name, contents) =>
|
||||
filename = "#{name}.html"
|
||||
templatePath = path.join(@_templatesDir, filename)
|
||||
fs.writeFile templatePath, contents, (err) =>
|
||||
@_displayError(err) if err
|
||||
shell.showItemInFolder(templatePath)
|
||||
@_items.push
|
||||
id: filename,
|
||||
name: name,
|
||||
path: templatePath
|
||||
@trigger(@)
|
||||
|
||||
_onInsertTemplateId: ({templateId, draftClientId} = {}) =>
|
||||
template = null
|
||||
for item in @_items
|
||||
template = item if item.id is templateId
|
||||
return unless template
|
||||
|
||||
fs.readFile template.path, (err, data) ->
|
||||
body = data.toString()
|
||||
DraftStore.sessionForClientId(draftClientId).then (session) ->
|
||||
session.changes.add(body: body)
|
||||
|
||||
module.exports = new TemplateStore()
|
22
examples/N1-Composer-Templates/package.json
Executable file
22
examples/N1-Composer-Templates/package.json
Executable file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "N1-Composer-Templates",
|
||||
"version": "0.1.0",
|
||||
"main": "./lib/main",
|
||||
|
||||
"isStarterPackage": true,
|
||||
"title": "Templates",
|
||||
"description": "Create templates you can use to pre-fill the composer - never type the same email again!",
|
||||
"icon": "./icon.png",
|
||||
|
||||
"license": "Proprietary",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"atom": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
"composer": true
|
||||
}
|
||||
}
|
139
examples/N1-Composer-Templates/spec/template-store-spec.coffee
Normal file
139
examples/N1-Composer-Templates/spec/template-store-spec.coffee
Normal file
|
@ -0,0 +1,139 @@
|
|||
{Message, Actions, DatabaseStore, DraftStore} = require 'nylas-exports'
|
||||
TemplateStore = require '../lib/template-store'
|
||||
fs = require 'fs-plus'
|
||||
shell = require 'shell'
|
||||
|
||||
stubTemplatesDir = TemplateStore.templatesDirectory()
|
||||
|
||||
stubTemplateFiles = {
|
||||
'template1.html': '<p>bla1</p>',
|
||||
'template2.html': '<p>bla2</p>'
|
||||
}
|
||||
|
||||
stubTemplates = [
|
||||
{id: 'template1.html', name: 'template1', path: "#{stubTemplatesDir}/template1.html"},
|
||||
{id: 'template2.html', name: 'template2', path: "#{stubTemplatesDir}/template2.html"},
|
||||
]
|
||||
|
||||
describe "TemplateStore", ->
|
||||
beforeEach ->
|
||||
spyOn(fs, 'mkdir')
|
||||
spyOn(shell, 'showItemInFolder').andCallFake ->
|
||||
spyOn(fs, 'writeFile').andCallFake (path, contents, callback) ->
|
||||
callback(null)
|
||||
spyOn(fs, 'readFile').andCallFake (path, callback) ->
|
||||
filename = path.split('/').pop()
|
||||
callback(null, stubTemplateFiles[filename])
|
||||
|
||||
it "should create the templates folder if it does not exist", ->
|
||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(false)
|
||||
TemplateStore.init()
|
||||
expect(fs.mkdir).toHaveBeenCalled()
|
||||
|
||||
it "should expose templates in the templates directory", ->
|
||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(true)
|
||||
spyOn(fs, 'readdir').andCallFake (path, callback) -> callback(null, Object.keys(stubTemplateFiles))
|
||||
TemplateStore.init()
|
||||
expect(TemplateStore.items()).toEqual(stubTemplates)
|
||||
|
||||
it "should watch the templates directory and reflect changes", ->
|
||||
watchCallback = null
|
||||
watchFired = false
|
||||
|
||||
spyOn(fs, 'exists').andCallFake (path, callback) -> callback(true)
|
||||
spyOn(fs, 'watch').andCallFake (path, callback) -> watchCallback = callback
|
||||
spyOn(fs, 'readdir').andCallFake (path, callback) ->
|
||||
if watchFired
|
||||
callback(null, Object.keys(stubTemplateFiles))
|
||||
else
|
||||
callback(null, [])
|
||||
|
||||
TemplateStore.init()
|
||||
expect(TemplateStore.items()).toEqual([])
|
||||
|
||||
watchFired = true
|
||||
watchCallback()
|
||||
expect(TemplateStore.items()).toEqual(stubTemplates)
|
||||
|
||||
describe "insertTemplateId", ->
|
||||
it "should insert the template with the given id into the draft with the given id", ->
|
||||
|
||||
add = jasmine.createSpy('add')
|
||||
spyOn(DraftStore, 'sessionForClientId').andCallFake ->
|
||||
Promise.resolve(changes: {add})
|
||||
|
||||
runs ->
|
||||
TemplateStore._onInsertTemplateId
|
||||
templateId: 'template1.html',
|
||||
draftClientId: 'localid-draft'
|
||||
waitsFor ->
|
||||
add.calls.length > 0
|
||||
runs ->
|
||||
expect(add).toHaveBeenCalledWith
|
||||
body: stubTemplateFiles['template1.html']
|
||||
|
||||
describe "onCreateTemplate", ->
|
||||
beforeEach ->
|
||||
TemplateStore.init()
|
||||
spyOn(DraftStore, 'sessionForClientId').andCallFake (draftClientId) ->
|
||||
if draftClientId is 'localid-nosubject'
|
||||
d = new Message(subject: '', body: '<p>Body</p>')
|
||||
else
|
||||
d = new Message(subject: 'Subject', body: '<p>Body</p>')
|
||||
session =
|
||||
draft: -> d
|
||||
Promise.resolve(session)
|
||||
|
||||
it "should create a template with the given name and contents", ->
|
||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
||||
item = TemplateStore.items()?[0]
|
||||
expect(item.id).toBe "123.html"
|
||||
expect(item.name).toBe "123"
|
||||
expect(item.path.split("/").pop()).toBe "123.html"
|
||||
|
||||
it "should display an error if no name is provided", ->
|
||||
spyOn(TemplateStore, '_displayError')
|
||||
TemplateStore._onCreateTemplate({contents: 'bla'})
|
||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
||||
|
||||
it "should display an error if no content is provided", ->
|
||||
spyOn(TemplateStore, '_displayError')
|
||||
TemplateStore._onCreateTemplate({name: 'bla'})
|
||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
||||
|
||||
it "should save the template file to the templates folder", ->
|
||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
||||
path = "#{stubTemplatesDir}/123.html"
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
expect(fs.writeFile.mostRecentCall.args[0]).toEqual(path)
|
||||
expect(fs.writeFile.mostRecentCall.args[1]).toEqual('bla')
|
||||
|
||||
it "should open the template so you can see it", ->
|
||||
TemplateStore._onCreateTemplate({name: '123', contents: 'bla'})
|
||||
path = "#{stubTemplatesDir}/123.html"
|
||||
expect(shell.showItemInFolder).toHaveBeenCalled()
|
||||
|
||||
describe "when given a draft id", ->
|
||||
it "should create a template from the name and contents of the given draft", ->
|
||||
spyOn(TemplateStore, 'trigger')
|
||||
spyOn(TemplateStore, '_populate')
|
||||
runs ->
|
||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-b'})
|
||||
waitsFor ->
|
||||
TemplateStore.trigger.callCount > 0
|
||||
runs ->
|
||||
expect(TemplateStore.items().length).toEqual(1)
|
||||
|
||||
it "should display an error if the draft has no subject", ->
|
||||
spyOn(TemplateStore, '_displayError')
|
||||
runs ->
|
||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'})
|
||||
waitsFor ->
|
||||
TemplateStore._displayError.callCount > 0
|
||||
runs ->
|
||||
expect(TemplateStore._displayError).toHaveBeenCalled()
|
||||
|
||||
describe "onShowTemplates", ->
|
||||
it "should open the templates folder in the Finder", ->
|
||||
TemplateStore._onShowTemplates()
|
||||
expect(shell.showItemInFolder).toHaveBeenCalled()
|
41
examples/N1-Composer-Templates/stylesheets/message-templates.less
Executable file
41
examples/N1-Composer-Templates/stylesheets/message-templates.less
Executable file
|
@ -0,0 +1,41 @@
|
|||
@import "ui-variables";
|
||||
@import "ui-mixins";
|
||||
|
||||
@code-bg-color: #fcf4db;
|
||||
|
||||
.template-picker {
|
||||
.menu {
|
||||
.content-container {
|
||||
height:150px;
|
||||
overflow-y:scroll;
|
||||
}
|
||||
.footer-container {
|
||||
border-top: 1px solid @border-secondary-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.template-status-bar {
|
||||
background-color: @code-bg-color;
|
||||
color: darken(@code-bg-color, 70%);
|
||||
border: 1.5px solid darken(@code-bg-color, 10%);
|
||||
border-radius: @border-radius-small;
|
||||
padding-top: @padding-small-vertical @padding-small-horizontal @padding-small-vertical @padding-small-horizontal;
|
||||
font-size: @font-size-small;
|
||||
}
|
||||
|
||||
.compose-body #contenteditable {
|
||||
code.var {
|
||||
font: inherit;
|
||||
padding:0;
|
||||
padding-left:2px;
|
||||
padding-right:2px;
|
||||
border-bottom: 1.5px solid darken(@code-bg-color, 10%);
|
||||
background-color: fade(@code-bg-color, 10%);
|
||||
&.empty {
|
||||
color:darken(@code-bg-color, 70%);
|
||||
border-bottom: 1px solid darken(@code-bg-color, 14%);
|
||||
background-color: @code-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
"isStarterPackage": true,
|
||||
"title": "Translation",
|
||||
"description": "An example package for N1 that translates drafts into other languages using the Yandex API.",
|
||||
"description": "Translate your drafts in the composer into other languages using the Yandex Translation API.",
|
||||
"icon": "./icon.png",
|
||||
|
||||
"license": "Proprietary",
|
||||
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nylas/translate"
|
||||
"url": "https://github.com/nylas/N1"
|
||||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
"isStarterPackage": true,
|
||||
"title": "Github",
|
||||
"description": "Adds Github quick actions to many emails, and allows you to see the Github profiles of the people you email.",
|
||||
"description": "Extends the contact card in the sidebar to show public repos of the people you email.",
|
||||
"icon": "./icon.png",
|
||||
|
||||
"license": "MIT",
|
||||
|
|
|
@ -42,21 +42,6 @@ class ContenteditableComponent extends React.Component
|
|||
@_editableNode().addEventListener('contextmenu', @_onShowContextualMenu)
|
||||
@_setupSelectionListeners()
|
||||
@_setupGlobalMouseListener()
|
||||
|
||||
@_disposable = atom.commands.add '.contenteditable-container *', {
|
||||
'core:focus-next': (event) =>
|
||||
editableNode = @_editableNode()
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onFocusNext(editableNode, range, event) if extension.onFocusNext
|
||||
|
||||
'core:focus-previous': (event) =>
|
||||
editableNode = @_editableNode()
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onFocusPrevious(editableNode, range, event) if extension.onFocusPrevious
|
||||
}
|
||||
|
||||
@_cleanHTML()
|
||||
|
||||
@setInnerState editableNode: @_editableNode()
|
||||
|
@ -69,7 +54,6 @@ class ContenteditableComponent extends React.Component
|
|||
@_editableNode().removeEventListener('contextmenu', @_onShowContextualMenu)
|
||||
@_teardownSelectionListeners()
|
||||
@_teardownGlobalMouseListener()
|
||||
@_disposable.dispose()
|
||||
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
@_setupServices(nextProps)
|
||||
|
@ -275,7 +259,18 @@ class ContenteditableComponent extends React.Component
|
|||
@_onInput()
|
||||
|
||||
_onTabDown: (event) ->
|
||||
editableNode = @_editableNode()
|
||||
range = DOMUtils.getRangeInScope(editableNode)
|
||||
|
||||
for extension in DraftStore.extensions()
|
||||
extension.onTabDown(editableNode, range, event) if extension.onTabDown
|
||||
|
||||
return if event.defaultPrevented
|
||||
@_onTabDownDefaultBehavior(event)
|
||||
|
||||
_onTabDownDefaultBehavior: (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
selection = document.getSelection()
|
||||
if selection?.isCollapsed
|
||||
# Only Elements (not Text nodes) have the `closest` method
|
||||
|
|
|
@ -118,29 +118,14 @@ class DraftStoreExtension
|
|||
@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
|
||||
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`
|
||||
|
@ -148,7 +133,7 @@ class DraftStoreExtension
|
|||
- `event`: The mouse up event.
|
||||
|
||||
###
|
||||
@onFocusNext: (editableNode, range, event) ->
|
||||
@onTabDown: (editableNode, range, event) ->
|
||||
return
|
||||
|
||||
###
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in a new issue