mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-10 16:31:44 +08:00
feat(keymap): add new <KeymapHandlers />
Summary: Refactor keymaps to wrap components with a <KeymapHandlers /> component. This more Reactful way of declaring keyback handlers prevents us from needing to subscribe to `atom.commands` Test Plan: new tests Reviewers: bengotow, juan Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2226
This commit is contained in:
parent
0b84942051
commit
37e3fe0f45
35 changed files with 935 additions and 681 deletions
|
|
@ -1,24 +1,26 @@
|
||||||
# Your keymap
|
# Your keymap
|
||||||
#
|
#
|
||||||
# Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors
|
# N1 keymaps work in conjunction with the {KeyCommandsRegion} React
|
||||||
# to apply styles to elements, Atom keymaps use selectors to associate
|
# component.
|
||||||
# keystrokes with events in specific contexts.
|
|
||||||
#
|
#
|
||||||
# You can create a new keybinding in this file by typing "key" and then hitting
|
# A key, or sequence of keys is first mapped to a "command".
|
||||||
# tab.
|
|
||||||
#
|
#
|
||||||
# Here's an example taken from Atom's built-in keymap:
|
# The "command" is then mapped to a callback function within your React
|
||||||
|
# component or store.
|
||||||
#
|
#
|
||||||
# 'atom-text-editor':
|
# The keyboard -> command mapping is defined in this `.cson` file. Each
|
||||||
# 'enter': 'editor:newline'
|
# mapping is scoped under the component that it applies to by matching the
|
||||||
|
# root-level CSS class of that component.
|
||||||
#
|
#
|
||||||
# '.workspace':
|
# Any global, top-level mappings are scoped under the `body` selector.
|
||||||
# 'ctrl-shift-p': 'core:move-up'
|
|
||||||
# 'ctrl-p': 'core:move-down'
|
|
||||||
#
|
#
|
||||||
# You can find more information about keymaps in these guides:
|
# For example:
|
||||||
# * https://atom.io/docs/latest/customizing-atom#customizing-key-bindings
|
#
|
||||||
# * https://atom.io/docs/latest/advanced/keymaps
|
# 'body':
|
||||||
|
# 'ctrl-c': 'application:new-message'
|
||||||
|
#
|
||||||
|
# '.my-custom-package':
|
||||||
|
# 'ctrl-p': 'myPackage:customAction'
|
||||||
#
|
#
|
||||||
# This file uses CoffeeScript Object Notation (CSON).
|
# This file uses CoffeeScript Object Notation (CSON).
|
||||||
# If you are unfamiliar with CSON, you can read more about it here:
|
# If you are unfamiliar with CSON, you can read more about it here:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
shell = require 'shell'
|
shell = require 'shell'
|
||||||
GithubStore = require './github-store'
|
GithubStore = require './github-store'
|
||||||
{React} = require 'nylas-exports'
|
{React} = require 'nylas-exports'
|
||||||
{RetinaImg} = require 'nylas-component-kit'
|
{RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
|
||||||
|
|
||||||
###
|
###
|
||||||
The `ViewOnGithubButton` displays a button whenever there's a relevant
|
The `ViewOnGithubButton` displays a button whenever there's a relevant
|
||||||
|
|
@ -68,23 +68,24 @@ class ViewOnGithubButton extends React.Component
|
||||||
# are cleaned up. Every time the `GithubStore` calls its `trigger`
|
# are cleaned up. Every time the `GithubStore` calls its `trigger`
|
||||||
# method, the `_onStoreChanged` callback will be fired.
|
# method, the `_onStoreChanged` callback will be fired.
|
||||||
@_unlisten = GithubStore.listen(@_onStoreChanged)
|
@_unlisten = GithubStore.listen(@_onStoreChanged)
|
||||||
@_keymapUnlisten = atom.commands.add 'body', {
|
|
||||||
'github:open': @_openLink
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount: ->
|
componentWillUnmount: ->
|
||||||
@_unlisten?()
|
@_unlisten?()
|
||||||
@_keymapUnlisten?.dispose()
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
'github:open': @_openLink
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
return null unless @state.link
|
return null unless @state.link
|
||||||
<button className="btn btn-toolbar"
|
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||||
onClick={@_openLink}
|
<button className="btn btn-toolbar"
|
||||||
title={"Visit Thread on GitHub"}>
|
onClick={@_openLink}
|
||||||
<RetinaImg
|
title={"Visit Thread on GitHub"}>
|
||||||
mode={RetinaImg.Mode.ContentIsMask}
|
<RetinaImg
|
||||||
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
</button>
|
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
|
||||||
|
</button>
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
|
|
||||||
#### Super common N1 Component private methods ####
|
#### Super common N1 Component private methods ####
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ React = require 'react'
|
||||||
{Menu,
|
{Menu,
|
||||||
Popover,
|
Popover,
|
||||||
RetinaImg,
|
RetinaImg,
|
||||||
|
KeyCommandsRegion,
|
||||||
LabelColorizer} = require 'nylas-component-kit'
|
LabelColorizer} = require 'nylas-component-kit'
|
||||||
|
|
||||||
# This changes the category on one or more threads.
|
# This changes the category on one or more threads.
|
||||||
|
|
@ -37,9 +38,6 @@ class CategoryPicker extends React.Component
|
||||||
@unsubscribers.push CategoryStore.listen @_onStoreChanged
|
@unsubscribers.push CategoryStore.listen @_onStoreChanged
|
||||||
@unsubscribers.push AccountStore.listen @_onStoreChanged
|
@unsubscribers.push AccountStore.listen @_onStoreChanged
|
||||||
|
|
||||||
@_commandUnsubscriber = atom.commands.add 'body',
|
|
||||||
"application:change-category": @_onOpenCategoryPopover
|
|
||||||
|
|
||||||
# If the threads we're picking categories for change, (like when they
|
# If the threads we're picking categories for change, (like when they
|
||||||
# get their categories updated), we expect our parents to pass us new
|
# get their categories updated), we expect our parents to pass us new
|
||||||
# props. We don't listen to the DatabaseStore ourselves.
|
# props. We don't listen to the DatabaseStore ourselves.
|
||||||
|
|
@ -50,7 +48,9 @@ class CategoryPicker extends React.Component
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
return unless @unsubscribers
|
return unless @unsubscribers
|
||||||
unsubscribe() for unsubscribe in @unsubscribers
|
unsubscribe() for unsubscribe in @unsubscribers
|
||||||
@_commandUnsubscriber.dispose()
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
"application:change-category": @_onOpenCategoryPopover
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
return <span></span> unless @_account
|
return <span></span> unless @_account
|
||||||
|
|
@ -84,23 +84,25 @@ class CategoryPicker extends React.Component
|
||||||
onChange={@_onSearchValueChange}/>
|
onChange={@_onSearchValueChange}/>
|
||||||
]
|
]
|
||||||
|
|
||||||
<Popover className="category-picker"
|
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||||
ref="popover"
|
<Popover className="category-picker"
|
||||||
onOpened={@_onPopoverOpened}
|
ref="popover"
|
||||||
onClosed={@_onPopoverClosed}
|
onOpened={@_onPopoverOpened}
|
||||||
direction="down-align-left"
|
onClosed={@_onPopoverClosed}
|
||||||
style={order: -103}
|
direction="down-align-left"
|
||||||
buttonComponent={button}>
|
style={order: -103}
|
||||||
<Menu ref="menu"
|
buttonComponent={button}>
|
||||||
headerComponents={headerComponents}
|
<Menu ref="menu"
|
||||||
footerComponents={[]}
|
headerComponents={headerComponents}
|
||||||
items={@state.categoryData}
|
footerComponents={[]}
|
||||||
itemKey={ (item) -> item.id }
|
items={@state.categoryData}
|
||||||
itemContent={@_renderItemContent}
|
itemKey={ (item) -> item.id }
|
||||||
onSelect={@_onSelectCategory}
|
itemContent={@_renderItemContent}
|
||||||
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
|
onSelect={@_onSelectCategory}
|
||||||
/>
|
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
|
||||||
</Popover>
|
/>
|
||||||
|
</Popover>
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
_onOpenCategoryPopover: =>
|
_onOpenCategoryPopover: =>
|
||||||
return unless @_threads().length > 0
|
return unless @_threads().length > 0
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,16 @@
|
||||||
'.composer-outer-wrap, .composer-outer-wrap input, .composer-outer-wrap
|
'.composer-outer-wrap, .composer-outer-wrap input, .composer-outer-wrap
|
||||||
div[contenteditable]':
|
div[contenteditable]':
|
||||||
'cmd-B' : 'composer:show-and-focus-bcc'
|
'cmdctrl-T' : 'composer:focus-to'
|
||||||
'cmd-C' : 'composer:show-and-focus-cc'
|
'cmdctrl-C' : 'composer:show-and-focus-cc'
|
||||||
'ctrl-B' : 'composer:show-and-focus-bcc'
|
'cmdctrl-B' : 'composer:show-and-focus-bcc'
|
||||||
'ctrl-C' : 'composer:show-and-focus-cc'
|
'cmdctrl-F' : 'composer:show-and-focus-from'
|
||||||
'cmd-T' : 'composer:focus-to'
|
'cmdctrl-enter': 'composer:send-message'
|
||||||
'ctrl-T' : 'composer:focus-to'
|
'cmdctrl-z' : 'composer:undo'
|
||||||
'cmd-enter' : 'composer:send-message'
|
'cmdctrl-Z' : 'composer:redo'
|
||||||
'ctrl-enter' : 'composer:send-message'
|
'cmdctrl-y' : 'composer:redo'
|
||||||
|
|
||||||
'.composer-outer-wrap':
|
'.composer-outer-wrap':
|
||||||
'delete' : 'composer:no-op'
|
'delete': 'composer:no-op'
|
||||||
|
|
||||||
'.composer-outer-wrap, .composer-outer-wrap div[contenteditable]':
|
'.composer-outer-wrap, .composer-outer-wrap div[contenteditable]':
|
||||||
'escape' : 'composer:delete-empty-draft'
|
'escape': 'composer:delete-empty-draft'
|
||||||
|
|
||||||
'body.platform-win32 .composer-outer-wrap *[contenteditable], body.platform-win32 .composer-outer-wrap input':
|
|
||||||
'ctrl-z': 'composer:undo'
|
|
||||||
'ctrl-Z': 'composer:redo'
|
|
||||||
'ctrl-y': 'composer:redo'
|
|
||||||
|
|
||||||
'body.platform-linux .composer-outer-wrap *[contenteditable], body.platform-linux .composer-outer-wrap input':
|
|
||||||
'ctrl-z': 'composer:undo'
|
|
||||||
'ctrl-Z': 'composer:redo'
|
|
||||||
'ctrl-y': 'composer:redo'
|
|
||||||
|
|
||||||
'body.platform-darwin .composer-outer-wrap *[contenteditable], body.platform-darwin .composer-outer-wrap input':
|
|
||||||
'cmd-z': 'composer:undo'
|
|
||||||
'cmd-Z': 'composer:redo'
|
|
||||||
'cmd-y': 'composer:redo'
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class CollapsedParticipants extends React.Component
|
||||||
bcc: []
|
bcc: []
|
||||||
|
|
||||||
constructor: (@props={}) ->
|
constructor: (@props={}) ->
|
||||||
|
@_keyPrefix = Utils.generateTempId()
|
||||||
@state =
|
@state =
|
||||||
numToDisplay: 999
|
numToDisplay: 999
|
||||||
numRemaining: 0
|
numRemaining: 0
|
||||||
|
|
@ -60,15 +61,17 @@ class CollapsedParticipants extends React.Component
|
||||||
|
|
||||||
return <div className="num-remaining-wrap tokenizing-field"><div className="show-more-fade"></div><div className="num-remaining token">{str}</div></div>
|
return <div className="num-remaining-wrap tokenizing-field"><div className="show-more-fade"></div><div className="num-remaining token">{str}</div></div>
|
||||||
|
|
||||||
_collapsedContact: (contact) ->
|
_collapsedContact: (contact) =>
|
||||||
name = contact.displayName()
|
name = contact.displayName()
|
||||||
<span key={contact.id}
|
key = @_keyPrefix + contact.email + contact.name
|
||||||
|
<span key={key}
|
||||||
className="collapsed-contact regular-contact">{name}</span>
|
className="collapsed-contact regular-contact">{name}</span>
|
||||||
|
|
||||||
_collapsedBccContact: (contact, i) ->
|
_collapsedBccContact: (contact, i) =>
|
||||||
name = contact.displayName()
|
name = contact.displayName()
|
||||||
|
key = @_keyPrefix + contact.email + contact.name
|
||||||
if i is 0 then name = "Bcc: #{name}"
|
if i is 0 then name = "Bcc: #{name}"
|
||||||
<span key={contact.id}
|
<span key={key}
|
||||||
className="collapsed-contact bcc-contact">{name}</span>
|
className="collapsed-contact bcc-contact">{name}</span>
|
||||||
|
|
||||||
_setNumHiddenParticipants: ->
|
_setNumHiddenParticipants: ->
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ React = require 'react'
|
||||||
ScrollRegion,
|
ScrollRegion,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
InjectedComponent,
|
InjectedComponent,
|
||||||
|
KeyCommandsRegion,
|
||||||
FocusTrackingRegion,
|
FocusTrackingRegion,
|
||||||
InjectedComponentSet} = require 'nylas-component-kit'
|
InjectedComponentSet} = require 'nylas-component-kit'
|
||||||
|
|
||||||
|
|
@ -77,15 +78,6 @@ class ComposerView extends React.Component
|
||||||
@_usubs = []
|
@_usubs = []
|
||||||
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
|
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
|
||||||
@_usubs.push AccountStore.listen @_onAccountStoreChanged
|
@_usubs.push AccountStore.listen @_onAccountStoreChanged
|
||||||
@_keymapUnlisten = atom.commands.add '.composer-outer-wrap', {
|
|
||||||
'composer:send-message': => @_sendDraft()
|
|
||||||
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
|
|
||||||
'composer:show-and-focus-bcc': => @_onChangeEnabledFields(show: [Fields.Bcc], focus: Fields.Bcc)
|
|
||||||
'composer:show-and-focus-cc': => @_onChangeEnabledFields(show: [Fields.Cc], focus: Fields.Cc)
|
|
||||||
'composer:focus-to': => @_onChangeEnabledFields(show: [Fields.To], focus: Fields.To)
|
|
||||||
"composer:undo": @undo
|
|
||||||
"composer:redo": @redo
|
|
||||||
}
|
|
||||||
@_applyFocusedField()
|
@_applyFocusedField()
|
||||||
|
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
|
|
@ -93,7 +85,6 @@ class ComposerView extends React.Component
|
||||||
@_teardownForDraft()
|
@_teardownForDraft()
|
||||||
@_deleteDraftIfEmpty()
|
@_deleteDraftIfEmpty()
|
||||||
usub() for usub in @_usubs
|
usub() for usub in @_usubs
|
||||||
@_keymapUnlisten.dispose() if @_keymapUnlisten
|
|
||||||
|
|
||||||
componentDidUpdate: =>
|
componentDidUpdate: =>
|
||||||
# We want to use a temporary variable instead of putting this into the
|
# We want to use a temporary variable instead of putting this into the
|
||||||
|
|
@ -105,6 +96,19 @@ class ComposerView extends React.Component
|
||||||
|
|
||||||
@_applyFocusedField()
|
@_applyFocusedField()
|
||||||
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
'composer:send-message': => @_sendDraft()
|
||||||
|
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
|
||||||
|
'composer:show-and-focus-bcc': =>
|
||||||
|
@_onChangeEnabledFields(show: [Fields.Bcc], focus: Fields.Bcc)
|
||||||
|
'composer:show-and-focus-cc': =>
|
||||||
|
@_onChangeEnabledFields(show: [Fields.Cc], focus: Fields.Cc)
|
||||||
|
'composer:focus-to': =>
|
||||||
|
@_onChangeEnabledFields(show: [Fields.To], focus: Fields.To)
|
||||||
|
"composer:show-and-focus-from": => # TODO
|
||||||
|
"composer:undo": @undo
|
||||||
|
"composer:redo": @redo
|
||||||
|
|
||||||
_applyFocusedField: ->
|
_applyFocusedField: ->
|
||||||
if @state.focusedField
|
if @state.focusedField
|
||||||
return unless @refs[@state.focusedField]
|
return unless @refs[@state.focusedField]
|
||||||
|
|
@ -130,7 +134,7 @@ class ComposerView extends React.Component
|
||||||
@undoManager = new UndoManager
|
@undoManager = new UndoManager
|
||||||
DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
|
DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
|
||||||
|
|
||||||
_setupSession: (proxy) =>
|
__setupSessionsetupSession: (proxy) =>
|
||||||
return if @_unmounted
|
return if @_unmounted
|
||||||
return unless proxy.draftClientId is @props.draftClientId
|
return unless proxy.draftClientId is @props.draftClientId
|
||||||
@_proxy = proxy
|
@_proxy = proxy
|
||||||
|
|
@ -149,20 +153,26 @@ class ComposerView extends React.Component
|
||||||
if @_proxy
|
if @_proxy
|
||||||
@_proxy.changes.commit()
|
@_proxy.changes.commit()
|
||||||
|
|
||||||
render: =>
|
render: ->
|
||||||
|
<KeyCommandsRegion localHandlers={@_keymapHandlers()}
|
||||||
|
className="composer-outer-wrap">
|
||||||
|
{@_renderComposerWrap()}
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
|
_renderComposerWrap: =>
|
||||||
if @props.mode is "inline"
|
if @props.mode is "inline"
|
||||||
<FocusTrackingRegion className={@_wrapClasses()}
|
<FocusTrackingRegion className={@_wrapClasses()}
|
||||||
ref="composer"
|
ref="composerWrap"
|
||||||
tabIndex="-1">
|
tabIndex="-1">
|
||||||
{@_renderComposer()}
|
{@_renderComposer()}
|
||||||
</FocusTrackingRegion>
|
</FocusTrackingRegion>
|
||||||
else
|
else
|
||||||
<div className={@_wrapClasses()} ref="composer">
|
<div className={@_wrapClasses()} ref="composerWrap">
|
||||||
{@_renderComposer()}
|
{@_renderComposer()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_wrapClasses: =>
|
_wrapClasses: =>
|
||||||
"message-item-white-wrap composer-outer-wrap #{@props.className ? ""}"
|
"message-item-white-wrap #{@props.className ? ""}"
|
||||||
|
|
||||||
_renderComposer: =>
|
_renderComposer: =>
|
||||||
<DropZone className="composer-inner-wrap"
|
<DropZone className="composer-inner-wrap"
|
||||||
|
|
@ -320,7 +330,7 @@ class ComposerView extends React.Component
|
||||||
# component. We provide it our boundingClientRect so it can calculate
|
# component. We provide it our boundingClientRect so it can calculate
|
||||||
# this value.
|
# this value.
|
||||||
_getComposerBoundingRect: =>
|
_getComposerBoundingRect: =>
|
||||||
React.findDOMNode(@refs.composer).getBoundingClientRect()
|
React.findDOMNode(@refs.composerWrap).getBoundingClientRect()
|
||||||
|
|
||||||
_onScrollToBottom: ->
|
_onScrollToBottom: ->
|
||||||
if @props.onRequestScrollTo
|
if @props.onRequestScrollTo
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,9 @@ describe "populated composer", ->
|
||||||
@isSending = {state: false}
|
@isSending = {state: false}
|
||||||
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
DraftStore._cleanupAllSessions()
|
||||||
|
|
||||||
describe "when sending a new message", ->
|
describe "when sending a new message", ->
|
||||||
it 'makes a request with the message contents', ->
|
it 'makes a request with the message contents', ->
|
||||||
useDraft.call @
|
useDraft.call @
|
||||||
|
|
@ -528,20 +531,21 @@ describe "populated composer", ->
|
||||||
useFullDraft.apply(@)
|
useFullDraft.apply(@)
|
||||||
makeComposer.call(@)
|
makeComposer.call(@)
|
||||||
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
|
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
|
||||||
|
@$composer = @composer.refs.composerWrap
|
||||||
|
|
||||||
it "sends the draft on cmd-enter", ->
|
it "sends the draft on cmd-enter", ->
|
||||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
|
||||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||||
|
|
||||||
it "does not send the draft on enter if the button isn't in focus", ->
|
it "does not send the draft on enter if the button isn't in focus", ->
|
||||||
NylasTestUtils.keyPress("enter", React.findDOMNode(@composer))
|
NylasTestUtils.keyPress("enter", React.findDOMNode(@$composer))
|
||||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||||
|
|
||||||
it "doesn't let you send twice", ->
|
it "doesn't let you send twice", ->
|
||||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
|
||||||
@isSending.state = true
|
@isSending.state = true
|
||||||
DraftStore.trigger()
|
DraftStore.trigger()
|
||||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
|
||||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||||
expect(Actions.sendDraft.calls.length).toBe 1
|
expect(Actions.sendDraft.calls.length).toBe 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@ MessageItemContainer = require './message-item-container'
|
||||||
MessageStore,
|
MessageStore,
|
||||||
DatabaseStore,
|
DatabaseStore,
|
||||||
WorkspaceStore,
|
WorkspaceStore,
|
||||||
ComponentRegistry,
|
|
||||||
ChangeLabelsTask,
|
ChangeLabelsTask,
|
||||||
|
ComponentRegistry,
|
||||||
ChangeStarredTask} = require("nylas-exports")
|
ChangeStarredTask} = require("nylas-exports")
|
||||||
|
|
||||||
{Spinner,
|
{Spinner,
|
||||||
|
RetinaImg,
|
||||||
|
MailLabel,
|
||||||
ScrollRegion,
|
ScrollRegion,
|
||||||
ResizableRegion,
|
ResizableRegion,
|
||||||
RetinaImg,
|
|
||||||
InjectedComponentSet,
|
|
||||||
MailLabel,
|
|
||||||
MailImportantIcon,
|
MailImportantIcon,
|
||||||
InjectedComponent} = require('nylas-component-kit')
|
InjectedComponent,
|
||||||
|
KeyCommandsRegion,
|
||||||
|
InjectedComponentSet} = require('nylas-component-kit')
|
||||||
|
|
||||||
class MessageListScrollTooltip extends React.Component
|
class MessageListScrollTooltip extends React.Component
|
||||||
@displayName: 'MessageListScrollTooltip'
|
@displayName: 'MessageListScrollTooltip'
|
||||||
|
|
@ -73,18 +74,8 @@ class MessageList extends React.Component
|
||||||
@_unsubscribers = []
|
@_unsubscribers = []
|
||||||
@_unsubscribers.push MessageStore.listen @_onChange
|
@_unsubscribers.push MessageStore.listen @_onChange
|
||||||
|
|
||||||
commands = _.extend {},
|
|
||||||
'application:reply': => @_createReplyOrUpdateExistingDraft('reply')
|
|
||||||
'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all')
|
|
||||||
'application:forward': => @_onForward()
|
|
||||||
'core:messages-page-up': => @_onScrollByPage(-1)
|
|
||||||
'core:messages-page-down': => @_onScrollByPage(1)
|
|
||||||
|
|
||||||
@command_unsubscriber = atom.commands.add('body', commands)
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
unsubscribe() for unsubscribe in @_unsubscribers
|
unsubscribe() for unsubscribe in @_unsubscribers
|
||||||
@command_unsubscriber.dispose()
|
|
||||||
|
|
||||||
shouldComponentUpdate: (nextProps, nextState) =>
|
shouldComponentUpdate: (nextProps, nextState) =>
|
||||||
not Utils.isEqualReact(nextProps, @props) or
|
not Utils.isEqualReact(nextProps, @props) or
|
||||||
|
|
@ -97,6 +88,13 @@ class MessageList extends React.Component
|
||||||
if newDraftClientIds.length > 0
|
if newDraftClientIds.length > 0
|
||||||
@_focusDraft(@_getMessageContainer(newDraftClientIds[0]))
|
@_focusDraft(@_getMessageContainer(newDraftClientIds[0]))
|
||||||
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
'application:reply': => @_createReplyOrUpdateExistingDraft('reply')
|
||||||
|
'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all')
|
||||||
|
'application:forward': => @_onForward()
|
||||||
|
'core:messages-page-up': => @_onScrollByPage(-1)
|
||||||
|
'core:messages-page-down': => @_onScrollByPage(1)
|
||||||
|
|
||||||
_newDraftClientIds: (prevState) =>
|
_newDraftClientIds: (prevState) =>
|
||||||
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.clientId)
|
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.clientId)
|
||||||
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.clientId)
|
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.clientId)
|
||||||
|
|
@ -191,26 +189,28 @@ class MessageList extends React.Component
|
||||||
"messages-wrap": true
|
"messages-wrap": true
|
||||||
"ready": not @state.loading
|
"ready": not @state.loading
|
||||||
|
|
||||||
<div className="message-list" id="message-list">
|
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||||
<ScrollRegion tabIndex="-1"
|
<div className="message-list" id="message-list">
|
||||||
className={wrapClass}
|
<ScrollRegion tabIndex="-1"
|
||||||
scrollTooltipComponent={MessageListScrollTooltip}
|
className={wrapClass}
|
||||||
ref="messageWrap">
|
scrollTooltipComponent={MessageListScrollTooltip}
|
||||||
{@_renderSubject()}
|
ref="messageWrap">
|
||||||
<div className="headers" style={position:'relative'}>
|
{@_renderSubject()}
|
||||||
<InjectedComponentSet
|
<div className="headers" style={position:'relative'}>
|
||||||
className="message-list-notification-bars"
|
<InjectedComponentSet
|
||||||
matching={role:"MessageListNotificationBar"}
|
className="message-list-notification-bars"
|
||||||
exposedProps={thread: @state.currentThread}/>
|
matching={role:"MessageListNotificationBar"}
|
||||||
<InjectedComponentSet
|
exposedProps={thread: @state.currentThread}/>
|
||||||
className="message-list-headers"
|
<InjectedComponentSet
|
||||||
matching={role:"MessageListHeaders"}
|
className="message-list-headers"
|
||||||
exposedProps={thread: @state.currentThread}/>
|
matching={role:"MessageListHeaders"}
|
||||||
</div>
|
exposedProps={thread: @state.currentThread}/>
|
||||||
{@_messageElements()}
|
</div>
|
||||||
</ScrollRegion>
|
{@_messageElements()}
|
||||||
<Spinner visible={@state.loading} />
|
</ScrollRegion>
|
||||||
</div>
|
<Spinner visible={@state.loading} />
|
||||||
|
</div>
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
_renderSubject: ->
|
_renderSubject: ->
|
||||||
subject = @state.currentThread?.subject
|
subject = @state.currentThread?.subject
|
||||||
|
|
@ -300,7 +300,7 @@ class MessageList extends React.Component
|
||||||
<div className="num-messages">{bundle.messages.length} older messages</div>
|
<div className="num-messages">{bundle.messages.length} older messages</div>
|
||||||
<div className="msg-lines" style={height: h*lines.length}>
|
<div className="msg-lines" style={height: h*lines.length}>
|
||||||
{lines.map (msg, i) ->
|
{lines.map (msg, i) ->
|
||||||
<div style={height: h*2, top: -h*i} className="msg-line"></div>}
|
<div key={msg.id} style={height: h*2, top: -h*i} className="msg-line"></div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ DisplayedKeybindings = [
|
||||||
['application:focus-search', 'Search'],
|
['application:focus-search', 'Search'],
|
||||||
['application:change-category', 'Change Folder / Labels'],
|
['application:change-category', 'Change Folder / Labels'],
|
||||||
['core:select-item', 'Select Focused Item'],
|
['core:select-item', 'Select Focused Item'],
|
||||||
['core:star-item', 'Star Focused Item'],
|
['application:star-item', 'Star Focused Item'],
|
||||||
]
|
]
|
||||||
|
|
||||||
class PreferencesKeymaps extends React.Component
|
class PreferencesKeymaps extends React.Component
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
React = require 'react/addons'
|
React = require 'react/addons'
|
||||||
classNames = require 'classnames'
|
classNames = require 'classnames'
|
||||||
{Actions, WorkspaceStore} = require 'nylas-exports'
|
{Actions, WorkspaceStore} = require 'nylas-exports'
|
||||||
{Menu, RetinaImg} = require 'nylas-component-kit'
|
{Menu, RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
|
||||||
SearchSuggestionStore = require './search-suggestion-store'
|
SearchSuggestionStore = require './search-suggestion-store'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
|
|
||||||
|
|
@ -20,20 +20,16 @@ class SearchBar extends React.Component
|
||||||
@usub.push SearchSuggestionStore.listen @_onChange
|
@usub.push SearchSuggestionStore.listen @_onChange
|
||||||
@usub.push WorkspaceStore.listen =>
|
@usub.push WorkspaceStore.listen =>
|
||||||
@setState(focused: false) if @state.focused
|
@setState(focused: false) if @state.focused
|
||||||
@body_unsubscriber = atom.commands.add 'body', {
|
|
||||||
'application:focus-search': @_onFocusSearch
|
|
||||||
}
|
|
||||||
@search_unsubscriber = atom.commands.add '.search-bar', {
|
|
||||||
'search-bar:escape-search': @_clearAndBlur
|
|
||||||
}
|
|
||||||
|
|
||||||
# It's important that every React class explicitly stops listening to
|
# It's important that every React class explicitly stops listening to
|
||||||
# atom events before it unmounts. Thank you event-kit
|
# atom events before it unmounts. Thank you event-kit
|
||||||
# This can be fixed via a Reflux mixin
|
# This can be fixed via a Reflux mixin
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
usub() for usub in @usub
|
usub() for usub in @usub
|
||||||
@body_unsubscriber.dispose()
|
|
||||||
@search_unsubscriber.dispose()
|
_keymapHandlers: ->
|
||||||
|
'application:focus-search': @_onFocusSearch
|
||||||
|
'search-bar:escape-search': @_clearAndBlur
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
inputValue = @_queryToString(@state.query)
|
inputValue = @_queryToString(@state.query)
|
||||||
|
|
@ -74,16 +70,18 @@ class SearchBar extends React.Component
|
||||||
else
|
else
|
||||||
item.label
|
item.label
|
||||||
|
|
||||||
<div className="search-bar">
|
<KeyCommandsRegion className="search-bar" globalHandlers={@_keymapHandlers()}>
|
||||||
<Menu ref="menu"
|
<div>
|
||||||
className={@_containerClasses()}
|
<Menu ref="menu"
|
||||||
headerComponents={headerComponents}
|
className={@_containerClasses()}
|
||||||
items={@state.suggestions}
|
headerComponents={headerComponents}
|
||||||
itemContent={itemContentFunc}
|
items={@state.suggestions}
|
||||||
itemKey={ (item) -> item.id ? item.label }
|
itemContent={itemContentFunc}
|
||||||
onSelect={@_onSelectSuggestion}
|
itemKey={ (item) -> item.id ? item.label }
|
||||||
/>
|
onSelect={@_onSelectSuggestion}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
_onFocusSearch: =>
|
_onFocusSearch: =>
|
||||||
React.findDOMNode(@refs.searchInput).focus()
|
React.findDOMNode(@refs.searchInput).focus()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ classNames = require 'classnames'
|
||||||
MultiselectList,
|
MultiselectList,
|
||||||
RetinaImg,
|
RetinaImg,
|
||||||
MailLabel,
|
MailLabel,
|
||||||
|
KeyCommandsRegion,
|
||||||
InjectedComponentSet} = require 'nylas-component-kit'
|
InjectedComponentSet} = require 'nylas-component-kit'
|
||||||
{timestamp, subject} = require './formatting-utils'
|
{timestamp, subject} = require './formatting-utils'
|
||||||
{Actions,
|
{Actions,
|
||||||
|
|
@ -166,24 +167,6 @@ class ThreadList extends React.Component
|
||||||
|
|
||||||
@narrowColumns = [cNarrow]
|
@narrowColumns = [cNarrow]
|
||||||
|
|
||||||
_shift = ({offset, afterRunning}) =>
|
|
||||||
view = ThreadListStore.view()
|
|
||||||
focusedId = FocusedContentStore.focusedId('thread')
|
|
||||||
focusedIdx = Math.min(view.count() - 1, Math.max(0, view.indexOfId(focusedId) + offset))
|
|
||||||
item = view.get(focusedIdx)
|
|
||||||
afterRunning()
|
|
||||||
Actions.setFocus(collection: 'thread', item: item)
|
|
||||||
|
|
||||||
@commands =
|
|
||||||
'core:remove-from-view': @_onRemoveFromView
|
|
||||||
'core:archive-item': @_onArchiveItem
|
|
||||||
'core:delete-item': @_onDeleteItem
|
|
||||||
'core:star-item': @_onStarItem
|
|
||||||
'core:remove-and-previous': =>
|
|
||||||
_shift(offset: 1, afterRunning: @_onRemoveFromView)
|
|
||||||
'core:remove-and-next': =>
|
|
||||||
_shift(offset: -1, afterRunning: @_onRemoveFromView)
|
|
||||||
|
|
||||||
@itemPropsProvider = (item) ->
|
@itemPropsProvider = (item) ->
|
||||||
className: classNames
|
className: classNames
|
||||||
'unread': item.unread
|
'unread': item.unread
|
||||||
|
|
@ -196,12 +179,35 @@ class ThreadList extends React.Component
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
window.removeEventListener('resize', @_onResize, true)
|
window.removeEventListener('resize', @_onResize, true)
|
||||||
|
|
||||||
render: =>
|
_shift: ({offset, afterRunning}) =>
|
||||||
|
view = ThreadListStore.view()
|
||||||
|
focusedId = FocusedContentStore.focusedId('thread')
|
||||||
|
focusedIdx = Math.min(view.count() - 1, Math.max(0, view.indexOfId(focusedId) + offset))
|
||||||
|
item = view.get(focusedIdx)
|
||||||
|
afterRunning()
|
||||||
|
Actions.setFocus(collection: 'thread', item: item)
|
||||||
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
'core:remove-from-view': @_onRemoveFromView
|
||||||
|
'application:archive-item': @_onArchiveItem
|
||||||
|
'application:delete-item': @_onDeleteItem
|
||||||
|
'application:star-item': @_onStarItem
|
||||||
|
'application:remove-and-previous': =>
|
||||||
|
@_shift(offset: 1, afterRunning: @_onRemoveFromView)
|
||||||
|
'application:remove-and-next': =>
|
||||||
|
@_shift(offset: -1, afterRunning: @_onRemoveFromView)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}
|
||||||
|
className="thread-list-wrap">
|
||||||
|
{@_renderList()}
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
|
_renderList: =>
|
||||||
if @state.style is 'wide'
|
if @state.style is 'wide'
|
||||||
<MultiselectList
|
<MultiselectList
|
||||||
dataStore={ThreadListStore}
|
dataStore={ThreadListStore}
|
||||||
columns={@wideColumns}
|
columns={@wideColumns}
|
||||||
commands={@commands}
|
|
||||||
itemPropsProvider={@itemPropsProvider}
|
itemPropsProvider={@itemPropsProvider}
|
||||||
itemHeight={39}
|
itemHeight={39}
|
||||||
className="thread-list"
|
className="thread-list"
|
||||||
|
|
@ -215,7 +221,6 @@ class ThreadList extends React.Component
|
||||||
<MultiselectList
|
<MultiselectList
|
||||||
dataStore={ThreadListStore}
|
dataStore={ThreadListStore}
|
||||||
columns={@narrowColumns}
|
columns={@narrowColumns}
|
||||||
commands={@commands}
|
|
||||||
itemPropsProvider={@itemPropsProvider}
|
itemPropsProvider={@itemPropsProvider}
|
||||||
itemHeight={90}
|
itemHeight={90}
|
||||||
className="thread-list thread-list-narrow"
|
className="thread-list thread-list-narrow"
|
||||||
|
|
@ -260,8 +265,6 @@ class ThreadList extends React.Component
|
||||||
if current isnt desired
|
if current isnt desired
|
||||||
@setState(style: desired)
|
@setState(style: desired)
|
||||||
|
|
||||||
# Additional Commands
|
|
||||||
|
|
||||||
_threadsForKeyboardAction: ->
|
_threadsForKeyboardAction: ->
|
||||||
return null unless ThreadListStore.view()
|
return null unless ThreadListStore.view()
|
||||||
focused = FocusedContentStore.focused('thread')
|
focused = FocusedContentStore.focused('thread')
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,13 @@
|
||||||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
# Note: Only put keymaps in here that apply to Mac ONLY
|
||||||
# to be listed in this file.
|
#
|
||||||
|
# Most cross-platform issues can be resolved by using the special N1
|
||||||
|
# `cmdctrl` extension before you keymap. This will automatically default
|
||||||
|
# to using the `cmd` key on Mac and `ctrl` key on Windows.
|
||||||
|
|
||||||
'body':
|
'body':
|
||||||
# Mac application keys
|
'cmd-m': 'application:minimize'
|
||||||
'cmd-q': 'application:quit'
|
|
||||||
'cmd-h': 'application:hide'
|
'cmd-h': 'application:hide'
|
||||||
'cmd-1': 'application:show-main-window'
|
'cmd-1': 'application:show-main-window'
|
||||||
'cmd-m': 'application:minimize'
|
|
||||||
'cmd-alt-w': 'application:show-work-window'
|
|
||||||
'cmd-alt-h': 'application:hide-other-applications'
|
'cmd-alt-h': 'application:hide-other-applications'
|
||||||
'alt-cmd-ctrl-m': 'application:zoom'
|
|
||||||
'cmd-alt-ctrl-s': 'application:run-all-specs'
|
|
||||||
|
|
||||||
# Mac core keys
|
|
||||||
'cmd-z': 'core:undo'
|
|
||||||
'cmd-Z': 'core:redo'
|
|
||||||
'cmd-y': 'core:redo'
|
|
||||||
'cmd-x': 'core:cut'
|
|
||||||
'cmd-c': 'core:copy'
|
|
||||||
'cmd-a': 'core:select-all'
|
|
||||||
'cmd-v': 'core:paste'
|
|
||||||
|
|
||||||
# Mac window keys
|
|
||||||
'cmd-w': 'window:close'
|
|
||||||
'cmd-=': 'window:increase-font-size'
|
|
||||||
'cmd-+': 'window:increase-font-size'
|
|
||||||
'cmd--': 'window:decrease-font-size'
|
|
||||||
'cmd-_': 'window:decrease-font-size'
|
|
||||||
'cmd-0': 'window:reset-font-size'
|
|
||||||
'alt-cmd-i': 'window:toggle-dev-tools'
|
|
||||||
'cmd-ctrl-f': 'window:toggle-full-screen'
|
'cmd-ctrl-f': 'window:toggle-full-screen'
|
||||||
'ctrl-alt-cmd-l': 'window:reload'
|
'alt-cmd-ctrl-m': 'application:zoom'
|
||||||
'cmd-alt-ctrl-p': 'application:run-package-specs'
|
|
||||||
|
|
||||||
'body *[contenteditable]':
|
|
||||||
'cmd-z': 'native!'
|
|
||||||
'cmd-Z': 'native!'
|
|
||||||
'cmd-y': 'native!'
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,11 @@
|
||||||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
# Note: Only put keymaps in here that apply to Linux ONLY
|
||||||
# to be listed in this file.
|
#
|
||||||
|
# Most cross-platform issues can be resolved by using the special N1
|
||||||
|
# `cmdctrl` extension before you keymap. This will automatically default
|
||||||
|
# to using the `cmd` key on Mac and `ctrl` key on Windows.
|
||||||
|
|
||||||
# Linux email-specific menu items
|
# Linux email-specific menu items
|
||||||
'body':
|
'body':
|
||||||
# Linux application keys
|
|
||||||
'ctrl-q': 'application:quit'
|
|
||||||
|
|
||||||
# Linux core keys
|
|
||||||
'ctrl-z': 'core:undo'
|
|
||||||
'ctrl-Z': 'core:redo'
|
|
||||||
'ctrl-y': 'core:redo'
|
|
||||||
'ctrl-x': 'core:cut'
|
|
||||||
'ctrl-c': 'core:copy'
|
|
||||||
'ctrl-a': 'core:select-all'
|
|
||||||
'ctrl-v': 'core:paste'
|
|
||||||
'ctrl-insert': 'core:copy'
|
'ctrl-insert': 'core:copy'
|
||||||
'shift-insert': 'core:paste'
|
'shift-insert': 'core:paste'
|
||||||
|
|
||||||
# Linux window keys
|
|
||||||
'ctrl-w': 'core:close'
|
|
||||||
'ctrl-=': 'window:increase-font-size'
|
|
||||||
'ctrl-+': 'window:increase-font-size'
|
|
||||||
'ctrl--': 'window:decrease-font-size'
|
|
||||||
'ctrl-_': 'window:decrease-font-size'
|
|
||||||
'ctrl-0': 'window:reset-font-size'
|
|
||||||
'ctrl-alt-r': 'window:reload'
|
|
||||||
'ctrl-shift-i': 'window:toggle-dev-tools'
|
|
||||||
'ctrl-alt-p': 'application:run-package-specs'
|
|
||||||
'ctrl-alt-s': 'application:run-all-specs'
|
|
||||||
'F11': 'window:toggle-full-screen'
|
'F11': 'window:toggle-full-screen'
|
||||||
|
|
||||||
'body *[contenteditable]':
|
|
||||||
'ctrl-z': 'native!'
|
|
||||||
'ctrl-Z': 'native!'
|
|
||||||
'ctrl-y': 'native!'
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,11 @@
|
||||||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
# Note: Only put keymaps in here that apply to Windows ONLY
|
||||||
# to be listed in this file.
|
#
|
||||||
|
# Most cross-platform issues can be resolved by using the special N1
|
||||||
|
# `cmdctrl` extension before you keymap. This will automatically default
|
||||||
|
# to using the `cmd` key on Mac and `ctrl` key on Windows.
|
||||||
|
|
||||||
'body':
|
'body':
|
||||||
# Windows email-specific menu items
|
'cmd-alt-l': 'window:reload'
|
||||||
'ctrl-n': 'application:new-message' # Outlook
|
'cmd-alt-i': 'window:toggle-dev-tools'
|
||||||
'ctrl-r': 'application:reply' # Outlook
|
'cmd-alt-w': 'application:show-work-window'
|
||||||
'ctrl-R': 'application:reply-all' # Outlook
|
|
||||||
'ctrl-F': 'application:forward' # Outlook
|
|
||||||
'ctrl-shift-v': 'application:change-category' # Outlook
|
|
||||||
|
|
||||||
# Windows application keys
|
|
||||||
'ctrl-q': 'application:quit'
|
|
||||||
'ctrl-alt-s': 'application:run-all-specs'
|
|
||||||
|
|
||||||
# Windows core keys
|
|
||||||
'ctrl-z': 'core:undo'
|
|
||||||
'ctrl-Z': 'core:redo'
|
|
||||||
'ctrl-y': 'core:redo'
|
|
||||||
'ctrl-x': 'core:cut'
|
|
||||||
'ctrl-c': 'core:copy'
|
|
||||||
'ctrl-a': 'core:select-all'
|
|
||||||
'ctrl-v': 'core:paste'
|
|
||||||
'ctrl-insert': 'core:copy'
|
|
||||||
'shift-insert': 'core:paste'
|
|
||||||
|
|
||||||
# Windows window keys
|
|
||||||
'ctrl-w': 'window:close'
|
|
||||||
'ctrl-=': 'window:increase-font-size'
|
|
||||||
'ctrl-+': 'window:increase-font-size'
|
|
||||||
'ctrl--': 'window:decrease-font-size'
|
|
||||||
'ctrl-_': 'window:decrease-font-size'
|
|
||||||
'ctrl-0': 'window:reset-font-size'
|
|
||||||
'ctrl-alt-r': 'window:reload'
|
|
||||||
'ctrl-alt-i': 'window:toggle-dev-tools'
|
|
||||||
'ctrl-alt-p': 'application:run-package-specs'
|
|
||||||
'ctrl-alt-s': 'application:run-all-specs'
|
|
||||||
'F11': 'window:toggle-full-screen'
|
'F11': 'window:toggle-full-screen'
|
||||||
|
|
||||||
'body *[contenteditable]':
|
|
||||||
'ctrl-z': 'native!'
|
|
||||||
'ctrl-Z': 'native!'
|
|
||||||
'ctrl-y': 'native!'
|
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,97 @@
|
||||||
# Email-specific core key-mappings
|
# This is the core set of universal, cross-platform keymaps. This is
|
||||||
|
# extended in the following places:
|
||||||
#
|
#
|
||||||
# There are additional mappings in <platform>.cson files that bind
|
# 1. keymaps/base.cson - (This file) Core, universal keymaps across all platforms
|
||||||
# menu items. In the future, we should break these into files like:
|
# 2. keymaps/base-darwin.cson - Any universal mac-only keymaps
|
||||||
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
|
# 3. keymaps/base-win32.cson - Any universal windows-only keymaps
|
||||||
|
# 4. keymaps/base-darwin.cson - Any universal linux-only keymaps
|
||||||
|
# 5. keymaps/templates/Gmail.cson - Gmail key bindings for all platforms
|
||||||
|
# 6. keymaps/templates/Outlook.cson - Outlook key bindings for all platforms
|
||||||
|
# 7. keymaps/templates/Apple Mail.cson - Mac Mail key bindings for all platforms
|
||||||
|
# 8. some/package/keymaps/package.cson - Keymaps for a specific package
|
||||||
|
# 9. ~/.nylas/keymap.cson - Custom user-specific overrides
|
||||||
|
#
|
||||||
|
# NOTE: We have a special N1 extension called `cmdctrl` that automatically
|
||||||
|
# uses `cmd` on mac and `ctrl` on windows and linux. This covers most
|
||||||
|
# cross-platform cases. For truely platform-specific features, use the
|
||||||
|
# platform keymap extensions.
|
||||||
|
|
||||||
'body':
|
'body':
|
||||||
'escape' : 'application:pop-sheet'
|
### Core system commands. ###
|
||||||
'cmd-,' : 'application:open-preferences'
|
# These have their default effects, but map to
|
||||||
'up' : 'core:previous-item'
|
# commands to allow for custom interactions.
|
||||||
'down' : 'core:next-item'
|
'cmdctrl-z': 'core:undo'
|
||||||
'enter' : 'core:focus-item'
|
'cmdctrl-Z': 'core:redo'
|
||||||
'delete' : 'core:remove-from-view'
|
'cmdctrl-y': 'core:redo'
|
||||||
'backspace': 'core:remove-from-view'
|
'cmdctrl-x': 'core:cut'
|
||||||
|
'cmdctrl-c': 'core:copy'
|
||||||
'pageup' : 'core:messages-page-up'
|
'cmdctrl-v': 'core:paste'
|
||||||
'pagedown' : 'core:messages-page-down'
|
'cmdctrl-a': 'core:select-all'
|
||||||
'shift-pageup' : 'core:list-page-up'
|
|
||||||
'shift-pagedown' : 'core:list-page-down'
|
|
||||||
|
|
||||||
# Default cross-platform core behaviors
|
|
||||||
'left': 'core:move-left'
|
|
||||||
'right': 'core:move-right'
|
|
||||||
'shift-up': 'core:select-up'
|
|
||||||
'shift-down': 'core:select-down'
|
|
||||||
'shift-left': 'core:select-left'
|
|
||||||
'shift-right': 'core:select-right'
|
|
||||||
'shift-delete': 'core:cut'
|
'shift-delete': 'core:cut'
|
||||||
|
|
||||||
# Inputs are native by default.
|
'up' : 'core:previous-item'
|
||||||
# Also make sure not to catch anything intended for a webview
|
'down' : 'core:next-item'
|
||||||
'body input, body textarea, body *[contenteditable], body webview':
|
'left' : 'core:move-left'
|
||||||
'up': 'native!'
|
'right' : 'core:move-right'
|
||||||
'left': 'native!'
|
'shift-up' : 'core:select-up'
|
||||||
'down': 'native!'
|
'shift-down' : 'core:select-down'
|
||||||
'right': 'native!'
|
'shift-left' : 'core:select-left'
|
||||||
'cmd-up': 'native!'
|
'shift-right': 'core:select-right'
|
||||||
'cmd-left': 'native!'
|
|
||||||
'cmd-down': 'native!'
|
|
||||||
'cmd-right': 'native!'
|
|
||||||
'ctrl-up': 'native!'
|
|
||||||
'ctrl-left': 'native!'
|
|
||||||
'ctrl-down': 'native!'
|
|
||||||
'ctrl-right': 'native!'
|
|
||||||
'shift-up': 'native!'
|
|
||||||
'shift-left': 'native!'
|
|
||||||
'shift-down': 'native!'
|
|
||||||
'shift-right': 'native!'
|
|
||||||
'escape': 'native!'
|
|
||||||
'pageup': 'native!'
|
|
||||||
'pagedown': 'native!'
|
|
||||||
'shift-pageup': 'native!'
|
|
||||||
'shift-pagedown': 'native!'
|
|
||||||
'enter': 'native!'
|
|
||||||
'cmd-enter': 'native!'
|
|
||||||
'ctrl-enter': 'native!'
|
|
||||||
'shift-enter': 'native!'
|
|
||||||
'backspace': 'native!'
|
|
||||||
'shift-backspace': 'native!'
|
|
||||||
'delete': 'native!'
|
|
||||||
'shift-delete': 'native!'
|
|
||||||
'cmd-y': 'native!'
|
|
||||||
'cmd-z': 'native!'
|
|
||||||
'cmd-Z': 'native!'
|
|
||||||
'cmd-x': 'native!'
|
|
||||||
'cmd-X': 'native!'
|
|
||||||
'cmd-c': 'native!'
|
|
||||||
'cmd-C': 'native!'
|
|
||||||
'cmd-v': 'native!'
|
|
||||||
'cmd-V': 'native!'
|
|
||||||
'cmd-a': 'native!'
|
|
||||||
'cmd-A': 'native!'
|
|
||||||
'cmd-b': 'native!'
|
|
||||||
'cmd-i': 'native!'
|
|
||||||
'cmd-u': 'native!'
|
|
||||||
'ctrl-y': 'native!'
|
|
||||||
'ctrl-z': 'native!'
|
|
||||||
'ctrl-Z': 'native!'
|
|
||||||
'ctrl-x': 'native!'
|
|
||||||
'ctrl-X': 'native!'
|
|
||||||
'ctrl-c': 'native!'
|
|
||||||
'ctrl-C': 'native!'
|
|
||||||
'ctrl-v': 'native!'
|
|
||||||
'ctrl-V': 'native!'
|
|
||||||
'ctrl-a': 'native!'
|
|
||||||
'ctrl-A': 'native!'
|
|
||||||
'ctrl-b': 'native!'
|
|
||||||
'ctrl-i': 'native!'
|
|
||||||
'ctrl-u': 'native!'
|
|
||||||
'a': 'native!'
|
|
||||||
'b': 'native!'
|
|
||||||
'c': 'native!'
|
|
||||||
'd': 'native!'
|
|
||||||
'e': 'native!'
|
|
||||||
'f': 'native!'
|
|
||||||
'g': 'native!'
|
|
||||||
'h': 'native!'
|
|
||||||
'i': 'native!'
|
|
||||||
'j': 'native!'
|
|
||||||
'k': 'native!'
|
|
||||||
'l': 'native!'
|
|
||||||
'm': 'native!'
|
|
||||||
'n': 'native!'
|
|
||||||
'o': 'native!'
|
|
||||||
'p': 'native!'
|
|
||||||
'q': 'native!'
|
|
||||||
'r': 'native!'
|
|
||||||
's': 'native!'
|
|
||||||
't': 'native!'
|
|
||||||
'u': 'native!'
|
|
||||||
'v': 'native!'
|
|
||||||
'w': 'native!'
|
|
||||||
'x': 'native!'
|
|
||||||
'y': 'native!'
|
|
||||||
'z': 'native!'
|
|
||||||
'A': 'native!'
|
|
||||||
'B': 'native!'
|
|
||||||
'C': 'native!'
|
|
||||||
'D': 'native!'
|
|
||||||
'E': 'native!'
|
|
||||||
'F': 'native!'
|
|
||||||
'G': 'native!'
|
|
||||||
'H': 'native!'
|
|
||||||
'I': 'native!'
|
|
||||||
'J': 'native!'
|
|
||||||
'K': 'native!'
|
|
||||||
'L': 'native!'
|
|
||||||
'M': 'native!'
|
|
||||||
'N': 'native!'
|
|
||||||
'O': 'native!'
|
|
||||||
'P': 'native!'
|
|
||||||
'Q': 'native!'
|
|
||||||
'R': 'native!'
|
|
||||||
'S': 'native!'
|
|
||||||
'T': 'native!'
|
|
||||||
'U': 'native!'
|
|
||||||
'V': 'native!'
|
|
||||||
'W': 'native!'
|
|
||||||
'X': 'native!'
|
|
||||||
'Y': 'native!'
|
|
||||||
'Z': 'native!'
|
|
||||||
'1': 'native!'
|
|
||||||
'2': 'native!'
|
|
||||||
'3': 'native!'
|
|
||||||
'4': 'native!'
|
|
||||||
'5': 'native!'
|
|
||||||
'6': 'native!'
|
|
||||||
'7': 'native!'
|
|
||||||
'8': 'native!'
|
|
||||||
'9': 'native!'
|
|
||||||
'0': 'native!'
|
|
||||||
'~': 'native!'
|
|
||||||
'!': 'native!'
|
|
||||||
'@': 'native!'
|
|
||||||
'#': 'native!'
|
|
||||||
'$': 'native!'
|
|
||||||
'%': 'native!'
|
|
||||||
'^': 'native!'
|
|
||||||
'&': 'native!'
|
|
||||||
'*': 'native!'
|
|
||||||
'(': 'native!'
|
|
||||||
')': 'native!'
|
|
||||||
'-': 'native!'
|
|
||||||
'_': 'native!'
|
|
||||||
'=': 'native!'
|
|
||||||
'+': 'native!'
|
|
||||||
'[': 'native!'
|
|
||||||
'{': 'native!'
|
|
||||||
']': 'native!'
|
|
||||||
'}': 'native!'
|
|
||||||
'\\': 'native!'
|
|
||||||
'|': 'native!'
|
|
||||||
';': 'native!'
|
|
||||||
':': 'native!'
|
|
||||||
'\'': 'native!'
|
|
||||||
'"': 'native!'
|
|
||||||
'<': 'native!'
|
|
||||||
',': 'native!'
|
|
||||||
'>': 'native!'
|
|
||||||
'.': 'native!'
|
|
||||||
'?': 'native!'
|
|
||||||
'/': 'native!'
|
|
||||||
|
|
||||||
'body input, body textarea':
|
### Core application commands. ###
|
||||||
'tab': 'core:focus-next'
|
'cmdctrl-q' : 'application:quit'
|
||||||
'shift-tab': 'core:focus-previous'
|
'cmdctrl-w' : 'window:close'
|
||||||
|
|
||||||
'body webview':
|
### Universal N1 commands. ###
|
||||||
'tab': 'native!'
|
'enter' : 'core:focus-item'
|
||||||
'shift-tab': 'native!'
|
'delete' : 'core:remove-from-view'
|
||||||
|
'escape' : 'application:pop-sheet'
|
||||||
|
'backspace': 'core:remove-from-view'
|
||||||
|
'cmdctrl-,': 'application:open-preferences'
|
||||||
|
|
||||||
# So our contenteditable control can do its own thing
|
'pageup' : 'core:messages-page-up'
|
||||||
'body *[contenteditable]':
|
'pagedown' : 'core:messages-page-down'
|
||||||
'tab': 'native!'
|
'shift-pageup' : 'core:list-page-up'
|
||||||
'shift-tab': 'native!'
|
'shift-pagedown': 'core:list-page-down'
|
||||||
|
|
||||||
# For menus
|
### N1 developer commands. ###
|
||||||
'body .menu, body .menu, body .menu input':
|
'cmdctrl-alt-l': 'window:reload'
|
||||||
# and by "native!" I actually mean for it to just let React deal with
|
'cmdctrl-alt-i': 'window:toggle-dev-tools'
|
||||||
# it.
|
'cmdctrl-alt-w': 'application:show-work-window'
|
||||||
'tab': 'native!'
|
'cmdctrl-alt-s': 'application:run-all-specs'
|
||||||
'shift-tab': 'native!'
|
'cmdctrl-alt-p': 'application:run-package-specs'
|
||||||
|
|
||||||
|
'body *[contenteditable].contenteditable':
|
||||||
|
### Basic formatting commands ###
|
||||||
|
'cmdctrl-u': 'contenteditable:underline'
|
||||||
|
'cmdctrl-b': 'contenteditable:bold'
|
||||||
|
'cmdctrl-i': 'contenteditable:italic'
|
||||||
|
'cmdctrl-k': 'contenteditable:insert-link'
|
||||||
|
|
||||||
|
### Advanced formatting commands ###
|
||||||
|
'cmdctrl-&': 'contenteditable:numbered-list'
|
||||||
|
'cmdctrl-#': 'contenteditable:numbered-list'
|
||||||
|
'cmdctrl-*': 'contenteditable:bulleted-list'
|
||||||
|
'cmdctrl-(': 'contenteditable:quote'
|
||||||
|
|
||||||
|
'cmdctrl-[': 'contenteditable:outdent'
|
||||||
|
'cmdctrl-]': 'contenteditable:indent'
|
||||||
|
|
||||||
|
'cmdctrl-L': 'contenteditable:align-left'
|
||||||
|
'cmdctrl-E': 'contenteditable:align-center'
|
||||||
|
'cmdctrl-R': 'contenteditable:align-right'
|
||||||
|
|
||||||
|
'cmdctrl-,': 'contenteditable:set-right-to-left'
|
||||||
|
'cmdctrl-.': 'contenteditable:set-left-to-right'
|
||||||
|
|
||||||
|
'cmdctrl-\\': 'contenteditable:remove-formatting'
|
||||||
|
|
||||||
|
'cmdctrl-%': 'contenteditable:previous-font'
|
||||||
|
'cmdctrl-^': 'contenteditable:next-font'
|
||||||
|
'cmdctrl-+': 'contenteditable:increase-text-size'
|
||||||
|
'cmdctrl--': 'contenteditable:decrease-text-size'
|
||||||
|
|
||||||
|
### Custom Property Navigating ###
|
||||||
|
'cmdctrl-;': 'contenteditable:previous-selection'
|
||||||
|
"cmdctrl-'": 'contenteditable:next-selection'
|
||||||
|
'cmdctrl-m': 'contenteditable:open-spelling-suggestions'
|
||||||
|
|
|
||||||
202
keymaps/input-reset.cson
Normal file
202
keymaps/input-reset.cson
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# We need to explicitly ensure all commands sent to inputs are handled as
|
||||||
|
# native events.
|
||||||
|
#
|
||||||
|
# When users type a key in an input, that event bubbles up. (We
|
||||||
|
# intentionally don't `stopPropagation` from inputs to allow for cases
|
||||||
|
# where you do want to catch key events.)
|
||||||
|
#
|
||||||
|
# When that keypress bubbles up to the root level, we may capture it
|
||||||
|
# thinking it's a hot key. While we could attach the special
|
||||||
|
# `.native-key-bindings` class to the input, we can't guarantee that this
|
||||||
|
# will be upheld. Furthermore, in some cases we may want to actually
|
||||||
|
# capture the input.
|
||||||
|
#
|
||||||
|
# Once captured, the event's default will be prevented.
|
||||||
|
#
|
||||||
|
# We give a higher CSS specificity to the inputs, textareas, and
|
||||||
|
# contentedtiables to ensure that the `native!` behavior takes precedent.
|
||||||
|
'body input, body textarea, body *[contenteditable], body webview':
|
||||||
|
'up': 'native!'
|
||||||
|
'left': 'native!'
|
||||||
|
'down': 'native!'
|
||||||
|
'right': 'native!'
|
||||||
|
'cmd-up': 'native!'
|
||||||
|
'cmd-left': 'native!'
|
||||||
|
'cmd-down': 'native!'
|
||||||
|
'cmd-right': 'native!'
|
||||||
|
'ctrl-up': 'native!'
|
||||||
|
'ctrl-left': 'native!'
|
||||||
|
'ctrl-down': 'native!'
|
||||||
|
'ctrl-right': 'native!'
|
||||||
|
'shift-up': 'native!'
|
||||||
|
'shift-left': 'native!'
|
||||||
|
'shift-down': 'native!'
|
||||||
|
'shift-right': 'native!'
|
||||||
|
'escape': 'native!'
|
||||||
|
'pageup': 'native!'
|
||||||
|
'pagedown': 'native!'
|
||||||
|
'shift-pageup': 'native!'
|
||||||
|
'shift-pagedown': 'native!'
|
||||||
|
'enter': 'native!'
|
||||||
|
'cmd-enter': 'native!'
|
||||||
|
'ctrl-enter': 'native!'
|
||||||
|
'shift-enter': 'native!'
|
||||||
|
'backspace': 'native!'
|
||||||
|
'shift-backspace': 'native!'
|
||||||
|
'delete': 'native!'
|
||||||
|
'shift-delete': 'native!'
|
||||||
|
'cmd-y': 'native!'
|
||||||
|
'cmd-z': 'native!'
|
||||||
|
'cmd-Z': 'native!'
|
||||||
|
'cmd-x': 'native!'
|
||||||
|
'cmd-X': 'native!'
|
||||||
|
'cmd-c': 'native!'
|
||||||
|
'cmd-C': 'native!'
|
||||||
|
'cmd-v': 'native!'
|
||||||
|
'cmd-V': 'native!'
|
||||||
|
'cmd-a': 'native!'
|
||||||
|
'cmd-A': 'native!'
|
||||||
|
'cmd-b': 'native!'
|
||||||
|
'cmd-i': 'native!'
|
||||||
|
'cmd-u': 'native!'
|
||||||
|
'ctrl-y': 'native!'
|
||||||
|
'ctrl-z': 'native!'
|
||||||
|
'ctrl-Z': 'native!'
|
||||||
|
'ctrl-x': 'native!'
|
||||||
|
'ctrl-X': 'native!'
|
||||||
|
'ctrl-c': 'native!'
|
||||||
|
'ctrl-C': 'native!'
|
||||||
|
'ctrl-v': 'native!'
|
||||||
|
'ctrl-V': 'native!'
|
||||||
|
'ctrl-a': 'native!'
|
||||||
|
'ctrl-A': 'native!'
|
||||||
|
'ctrl-b': 'native!'
|
||||||
|
'ctrl-i': 'native!'
|
||||||
|
'ctrl-u': 'native!'
|
||||||
|
'a': 'native!'
|
||||||
|
'b': 'native!'
|
||||||
|
'c': 'native!'
|
||||||
|
'd': 'native!'
|
||||||
|
'e': 'native!'
|
||||||
|
'f': 'native!'
|
||||||
|
'g': 'native!'
|
||||||
|
'h': 'native!'
|
||||||
|
'i': 'native!'
|
||||||
|
'j': 'native!'
|
||||||
|
'k': 'native!'
|
||||||
|
'l': 'native!'
|
||||||
|
'm': 'native!'
|
||||||
|
'n': 'native!'
|
||||||
|
'o': 'native!'
|
||||||
|
'p': 'native!'
|
||||||
|
'q': 'native!'
|
||||||
|
'r': 'native!'
|
||||||
|
's': 'native!'
|
||||||
|
't': 'native!'
|
||||||
|
'u': 'native!'
|
||||||
|
'v': 'native!'
|
||||||
|
'w': 'native!'
|
||||||
|
'x': 'native!'
|
||||||
|
'y': 'native!'
|
||||||
|
'z': 'native!'
|
||||||
|
'A': 'native!'
|
||||||
|
'B': 'native!'
|
||||||
|
'C': 'native!'
|
||||||
|
'D': 'native!'
|
||||||
|
'E': 'native!'
|
||||||
|
'F': 'native!'
|
||||||
|
'G': 'native!'
|
||||||
|
'H': 'native!'
|
||||||
|
'I': 'native!'
|
||||||
|
'J': 'native!'
|
||||||
|
'K': 'native!'
|
||||||
|
'L': 'native!'
|
||||||
|
'M': 'native!'
|
||||||
|
'N': 'native!'
|
||||||
|
'O': 'native!'
|
||||||
|
'P': 'native!'
|
||||||
|
'Q': 'native!'
|
||||||
|
'R': 'native!'
|
||||||
|
'S': 'native!'
|
||||||
|
'T': 'native!'
|
||||||
|
'U': 'native!'
|
||||||
|
'V': 'native!'
|
||||||
|
'W': 'native!'
|
||||||
|
'X': 'native!'
|
||||||
|
'Y': 'native!'
|
||||||
|
'Z': 'native!'
|
||||||
|
'1': 'native!'
|
||||||
|
'2': 'native!'
|
||||||
|
'3': 'native!'
|
||||||
|
'4': 'native!'
|
||||||
|
'5': 'native!'
|
||||||
|
'6': 'native!'
|
||||||
|
'7': 'native!'
|
||||||
|
'8': 'native!'
|
||||||
|
'9': 'native!'
|
||||||
|
'0': 'native!'
|
||||||
|
'~': 'native!'
|
||||||
|
'`': 'native!'
|
||||||
|
'!': 'native!'
|
||||||
|
'@': 'native!'
|
||||||
|
'#': 'native!'
|
||||||
|
'$': 'native!'
|
||||||
|
'%': 'native!'
|
||||||
|
'^': 'native!'
|
||||||
|
'&': 'native!'
|
||||||
|
'*': 'native!'
|
||||||
|
'(': 'native!'
|
||||||
|
')': 'native!'
|
||||||
|
'-': 'native!'
|
||||||
|
'_': 'native!'
|
||||||
|
'=': 'native!'
|
||||||
|
'+': 'native!'
|
||||||
|
'[': 'native!'
|
||||||
|
'{': 'native!'
|
||||||
|
']': 'native!'
|
||||||
|
'}': 'native!'
|
||||||
|
'\\': 'native!'
|
||||||
|
'|': 'native!'
|
||||||
|
';': 'native!'
|
||||||
|
':': 'native!'
|
||||||
|
'\'': 'native!'
|
||||||
|
'"': 'native!'
|
||||||
|
'<': 'native!'
|
||||||
|
',': 'native!'
|
||||||
|
'>': 'native!'
|
||||||
|
'.': 'native!'
|
||||||
|
'?': 'native!'
|
||||||
|
'/': 'native!'
|
||||||
|
'g i': 'native!'
|
||||||
|
'g s': 'native!'
|
||||||
|
'g t': 'native!'
|
||||||
|
'g d': 'native!'
|
||||||
|
'g a': 'native!'
|
||||||
|
'g c': 'native!'
|
||||||
|
'g k': 'native!'
|
||||||
|
'g l': 'native!'
|
||||||
|
'* a': 'native!'
|
||||||
|
'* n': 'native!'
|
||||||
|
'* r': 'native!'
|
||||||
|
'* u': 'native!'
|
||||||
|
'* s': 'native!'
|
||||||
|
'* t': 'native!'
|
||||||
|
|
||||||
|
# Tabs are a bit different because simple elements (like text inputs) we
|
||||||
|
# want to use our custom `core:focus-next`. Other more complex ones, like
|
||||||
|
# `contenteditable`, we want to have a more controlled effect over.
|
||||||
|
'body input, body textarea':
|
||||||
|
'tab': 'core:focus-next'
|
||||||
|
'shift-tab': 'core:focus-previous'
|
||||||
|
|
||||||
|
# So our contenteditable control can do its own thing
|
||||||
|
'body webview, body *[contenteditable]':
|
||||||
|
'tab': 'native!'
|
||||||
|
'shift-tab': 'native!'
|
||||||
|
|
||||||
|
# For menus
|
||||||
|
'body .menu, body .menu, body .menu input':
|
||||||
|
# and by "native!" I actually mean for it to just let React deal with
|
||||||
|
# it.
|
||||||
|
'tab': 'native!'
|
||||||
|
'shift-tab': 'native!'
|
||||||
|
|
@ -1,22 +1,12 @@
|
||||||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
# Note: For a menu item to have a keyboard equiavalent, it needs
|
||||||
# to be listed in this file.
|
# to be listed in this file.
|
||||||
|
|
||||||
'body.platform-darwin':
|
'body':
|
||||||
'cmd-n' : 'application:new-message'
|
'cmdctrl-n' : 'application:new-message'
|
||||||
'cmd-r' : 'application:reply'
|
'cmdctrl-r' : 'application:reply'
|
||||||
'cmd-R' : 'application:reply-all'
|
'cmdctrl-R' : 'application:reply-all'
|
||||||
'cmd-F' : 'application:forward'
|
'cmdctrl-F' : 'application:forward'
|
||||||
'cmd-alt-f': 'application:focus-search'
|
'cmdctrl-alt-f': 'application:focus-search'
|
||||||
'cmd-D': 'application:send-message'
|
'cmdctrl-D': 'application:send-message'
|
||||||
'cmd-V': 'application:change-category'
|
'cmdctrl-V': 'application:change-category'
|
||||||
'cmd-e' : 'core:archive-item'
|
'cmdctrl-e' : 'application:archive-item'
|
||||||
|
|
||||||
'body.platform-linux, body.platform-win32':
|
|
||||||
'ctrl-n' : 'application:new-message'
|
|
||||||
'ctrl-r' : 'application:reply'
|
|
||||||
'ctrl-R' : 'application:reply-all'
|
|
||||||
'ctrl-F' : 'application:forward'
|
|
||||||
'ctrl-alt-f': 'application:focus-search'
|
|
||||||
'ctrl-D': 'application:send-message'
|
|
||||||
'ctrl-shift-v': 'application:change-category'
|
|
||||||
'ctrl-e' : 'core:archive-item'
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,83 @@
|
||||||
# Email-specific core key-mappings
|
# Gmail-specific core key-mappings
|
||||||
#
|
|
||||||
# There are additional mappings in <platform>.cson files that bind
|
|
||||||
# menu items. In the future, we should break these into files like:
|
|
||||||
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
|
|
||||||
|
|
||||||
'body':
|
'body':
|
||||||
'c' : 'application:new-message'
|
|
||||||
'/' : 'application:focus-search'
|
|
||||||
|
|
||||||
'r' : 'application:reply'
|
### Jumping ###
|
||||||
'a' : 'application:reply-all'
|
'g i' : 'navigation:go-to-inbox'
|
||||||
'f' : 'application:forward'
|
'g s' : 'navigation:go-to-starred'
|
||||||
'l' : 'application:change-category'
|
'g t' : 'navigation:go-to-sent'
|
||||||
'u' : 'application:pop-sheet'
|
'g d' : 'navigation:go-to-drafts'
|
||||||
|
'g a' : 'navigation:go-to-all'
|
||||||
|
'g c' : 'navigation:go-to-contacts'
|
||||||
|
'g k' : 'navigation:go-to-tasks'
|
||||||
|
'g l' : 'navigation:go-to-label'
|
||||||
|
|
||||||
'k' : 'core:previous-item'
|
### Threadlist selection ###
|
||||||
'j' : 'core:next-item'
|
'* a': 'thread-list:select-all'
|
||||||
']' : 'core:remove-and-previous'
|
'* n': 'thread-list:deselect-all'
|
||||||
'[' : 'core:remove-and-next'
|
'* r': 'thread-list:select-read'
|
||||||
'#' : 'core:delete-item'
|
'* u': 'thread-list:select-unread'
|
||||||
'e' : 'core:archive-item'
|
'* s': 'thread-list:select-starred'
|
||||||
's' : 'core:star-item'
|
'* t': 'thread-list:select-unstarred'
|
||||||
'x' : 'core:select-item'
|
|
||||||
|
|
||||||
# Gmail also includes some more basic ones that users expect from desktop software.
|
### Navigation ###
|
||||||
|
'u': 'application:pop-sheet'
|
||||||
|
'k': 'core:previous-item'
|
||||||
|
'j': 'core:next-item'
|
||||||
|
'o': 'core:focus-item'
|
||||||
|
'p': 'message-list:previous-message'
|
||||||
|
'n': 'message-list:next-message'
|
||||||
|
'`': 'thread-list:next-inbox'
|
||||||
|
'~': 'thread-list:previous-inbox'
|
||||||
|
|
||||||
'body.platform-darwin':
|
### Application ###
|
||||||
'cmd-n' : 'application:new-message'
|
'c': 'application:new-message'
|
||||||
'cmd-r' : 'application:reply'
|
'd': 'application:new-message'
|
||||||
'cmd-R' : 'application:reply-all'
|
'/': 'application:focus-search'
|
||||||
'cmd-F' : 'application:forward'
|
'.': 'application:more-actions'
|
||||||
|
'l': 'application:change-category'
|
||||||
|
'v': 'application:change-category'
|
||||||
|
'?': 'application:open-help'
|
||||||
|
|
||||||
'body.platform-linux, body.platform-win32':
|
### Actions ###
|
||||||
'ctrl-n' : 'application:new-message'
|
',': 'application:focus-toolbar'
|
||||||
'ctrl-r' : 'application:reply'
|
'x': 'core:select-item'
|
||||||
'ctrl-R' : 'application:reply-all'
|
's': 'application:star-item'
|
||||||
'ctrl-F' : 'application:forward'
|
'y': 'core:remove-from-view' # Remove label
|
||||||
|
'e': 'application:archive-item'
|
||||||
|
'm': 'application:mute-conversation'
|
||||||
|
'!': 'application:report-as-spam'
|
||||||
|
'#': 'application:delete-item'
|
||||||
|
|
||||||
|
'r': 'application:reply'
|
||||||
|
'R': 'application:reply-new-window'
|
||||||
|
'a': 'application:reply-all'
|
||||||
|
'A': 'application:reply-all-new-window'
|
||||||
|
'f': 'application:forward'
|
||||||
|
'F': 'application:forward-new-window'
|
||||||
|
|
||||||
|
'N': 'application:update-conversation'
|
||||||
|
|
||||||
|
']': 'application:remove-and-previous'
|
||||||
|
'}': 'application:remove-and-previous'
|
||||||
|
'[': 'application:remove-and-next'
|
||||||
|
'{': 'application:remove-and-next'
|
||||||
|
|
||||||
|
'z': 'core:undo'
|
||||||
|
|
||||||
|
'I': 'application:mark-as-read'
|
||||||
|
'U': 'application:mark-as-unread'
|
||||||
|
'_': 'application:mark-as-unread' # Mark unread from the selected message
|
||||||
|
'+': 'application:mark-important'
|
||||||
|
'=': 'application:mark-important'
|
||||||
|
'-': 'application:mark-unimportant'
|
||||||
|
|
||||||
|
';': 'message-list:expand-all'
|
||||||
|
':': 'message-list:collapse-all'
|
||||||
|
|
||||||
|
# While not stock Gmail, we add the following standard keymaps that
|
||||||
|
# desktop application users would expect:
|
||||||
|
'cmdctrl-n' : 'application:new-message'
|
||||||
|
'cmdctrl-r' : 'application:reply'
|
||||||
|
'cmdctrl-R' : 'application:reply-all'
|
||||||
|
'cmdctrl-F' : 'application:forward'
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,18 @@
|
||||||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
# Note: For a menu item to have a keyboard equiavalent, it needs
|
||||||
# to be listed in this file.
|
# to be listed in this file.
|
||||||
|
|
||||||
'body.platform-darwin':
|
'body':
|
||||||
# Windows email-specific menu items
|
# Windows email-specific menu items
|
||||||
'cmd-shift-v': 'application:change-category' # Outlook
|
'cmdctrl-shift-v': 'application:change-category' # Outlook
|
||||||
'F3': 'application:focus-search'
|
'F3': 'application:focus-search'
|
||||||
'cmd-e': 'application:focus-search'
|
'cmdctrl-e': 'application:focus-search'
|
||||||
'cmd-f': 'application:forward'
|
'cmdctrl-f': 'application:forward'
|
||||||
'cmd-shift-v': 'application:change-category'
|
'cmdctrl-shift-v': 'application:change-category'
|
||||||
'cmd-d': 'core:delete-item'
|
'cmdctrl-d': 'application:delete-item'
|
||||||
'alt-backspace':'core:undo'
|
'alt-backspace':'core:undo'
|
||||||
'alt-s': 'application:send-message'
|
'alt-s': 'application:send-message'
|
||||||
'cmd-r': 'application:reply'
|
'cmdctrl-r': 'application:reply'
|
||||||
'cmd-shift-r': 'application:reply-all'
|
'cmdctrl-shift-r': 'application:reply-all'
|
||||||
'cmd-n' : 'application:new-message'
|
'cmdctrl-n' : 'application:new-message'
|
||||||
'cmd-shift-m': 'application:new-message'
|
'cmdctrl-shift-m': 'application:new-message'
|
||||||
'cmd-enter': 'send'
|
'cmdctrl-enter': 'send'
|
||||||
|
|
||||||
'body.platform-linux, body.platform-win32':
|
|
||||||
# Windows email-specific menu items
|
|
||||||
'ctrl-shift-v': 'application:change-category' # Outlook
|
|
||||||
'F3': 'application:focus-search'
|
|
||||||
'ctrl-e': 'application:focus-search'
|
|
||||||
'ctrl-f': 'application:forward'
|
|
||||||
'ctrl-shift-v': 'application:change-category'
|
|
||||||
'ctrl-d': 'core:delete-item'
|
|
||||||
'alt-backspace':'core:undo'
|
|
||||||
'alt-s': 'application:send-message'
|
|
||||||
'ctrl-r': 'application:reply'
|
|
||||||
'ctrl-shift-r': 'application:reply-all'
|
|
||||||
'ctrl-n' : 'application:new-message'
|
|
||||||
'ctrl-shift-m': 'application:new-message'
|
|
||||||
'ctrl-enter': 'send'
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asar": "^0.5.0",
|
"asar": "^0.5.0",
|
||||||
"async": "^0.9",
|
"async": "^0.9",
|
||||||
"atom-keymap": "^5.1",
|
"atom-keymap": "6.1.0",
|
||||||
"babel-core": "^6.0.20",
|
"babel-core": "^6.0.20",
|
||||||
"babel-preset-es2015": "^6.0.15",
|
"babel-preset-es2015": "^6.0.15",
|
||||||
"babel-preset-react": "^6.0.15",
|
"babel-preset-react": "^6.0.15",
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,14 @@ NylasTestUtils =
|
||||||
loadKeymap: (keymapPath) ->
|
loadKeymap: (keymapPath) ->
|
||||||
{resourcePath} = atom.getLoadSettings()
|
{resourcePath} = atom.getLoadSettings()
|
||||||
basePath = CSON.resolve("#{resourcePath}/keymaps/base")
|
basePath = CSON.resolve("#{resourcePath}/keymaps/base")
|
||||||
baseKeymaps = CSON.readFileSync(basePath)
|
atom.keymaps.loadKeymap(basePath)
|
||||||
atom.keymaps.add(basePath, baseKeymaps)
|
|
||||||
|
|
||||||
if keymapPath?
|
if keymapPath?
|
||||||
keymapPath = CSON.resolve("#{resourcePath}/#{keymapPath}")
|
keymapPath = CSON.resolve("#{resourcePath}/#{keymapPath}")
|
||||||
keymapFile = CSON.readFileSync(keymapPath)
|
atom.keymaps.loadKeymap(keymapPath)
|
||||||
atom.keymaps.add(keymapPath, keymapFile)
|
|
||||||
|
|
||||||
keyPress: (key, target) ->
|
keyPress: (key, target) ->
|
||||||
event = KeymapManager.buildKeydownEvent(key, target: target)
|
event = KeymapManager.buildKeydownEvent(key, target: target)
|
||||||
document.dispatchEvent(event)
|
atom.keymaps.handleKeyboardEvent(event)
|
||||||
|
|
||||||
module.exports = NylasTestUtils
|
module.exports = NylasTestUtils
|
||||||
|
|
@ -9,7 +9,7 @@ _ = require 'underscore'
|
||||||
_str = require 'underscore.string'
|
_str = require 'underscore.string'
|
||||||
fs = require 'fs-plus'
|
fs = require 'fs-plus'
|
||||||
Grim = require 'grim'
|
Grim = require 'grim'
|
||||||
KeymapManager = require '../src/keymap-extensions'
|
KeymapManager = require '../src/keymap-manager'
|
||||||
|
|
||||||
# FIXME: Remove jquery from this
|
# FIXME: Remove jquery from this
|
||||||
{$} = require '../src/space-pen-extensions'
|
{$} = require '../src/space-pen-extensions'
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ class Atom extends Model
|
||||||
@loadTime = null
|
@loadTime = null
|
||||||
|
|
||||||
Config = require './config'
|
Config = require './config'
|
||||||
KeymapManager = require './keymap-extensions'
|
KeymapManager = require './keymap-manager'
|
||||||
CommandRegistry = require './command-registry'
|
CommandRegistry = require './command-registry'
|
||||||
PackageManager = require './package-manager'
|
PackageManager = require './package-manager'
|
||||||
Clipboard = require './clipboard'
|
Clipboard = require './clipboard'
|
||||||
|
|
@ -183,11 +183,11 @@ class Atom extends Model
|
||||||
@config = new Config({configDirPath, resourcePath})
|
@config = new Config({configDirPath, resourcePath})
|
||||||
|
|
||||||
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
||||||
@keymaps.subscribeToFileReadFailure()
|
|
||||||
@keymaps.onDidMatchBinding (event) ->
|
@keymaps.onDidMatchBinding (event) ->
|
||||||
# If the user fired a command with the application: prefix bound to the body, re-fire it
|
# If the user fired a command with the application: prefix bound to
|
||||||
# up into the browser process. This prevents us from needing this crap, which has to be
|
# the body, re-fire it up into the browser process. This prevents us
|
||||||
# updated every time a new application: command is added:
|
# from needing this crap, which has to be updated every time a new
|
||||||
|
# application: command is added:
|
||||||
# https://github.com/atom/atom/blob/master/src/workspace-element.coffee#L119
|
# https://github.com/atom/atom/blob/master/src/workspace-element.coffee#L119
|
||||||
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
|
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
|
||||||
ipc.send('command', event.binding.command)
|
ipc.send('command', event.binding.command)
|
||||||
|
|
|
||||||
137
src/components/key-commands-region.cjsx
Normal file
137
src/components/key-commands-region.cjsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
_ = require 'underscore'
|
||||||
|
React = require 'react'
|
||||||
|
|
||||||
|
###
|
||||||
|
Public: Easily respond to keyboard shortcuts
|
||||||
|
|
||||||
|
A keyboard shortcut has two parts to it:
|
||||||
|
|
||||||
|
1. A mapping between keyboard actions and a command
|
||||||
|
2. A mapping between a command and a callback handler
|
||||||
|
|
||||||
|
|
||||||
|
## Mapping keys to commands (not handled by this component)
|
||||||
|
|
||||||
|
The **keyboard -> command** mapping is defined in a separate `.cson` file.
|
||||||
|
A majority of the commands your component would want to listen to you have
|
||||||
|
already been defined by core N1 defaults, as well as custom user
|
||||||
|
overrides. See 'keymaps/base.cson' for more information.
|
||||||
|
|
||||||
|
You can define additional, custom keyboard -> command mappings in your own
|
||||||
|
package-specific keymap `.cson` file. The file can be named anything but
|
||||||
|
must exist in a folder called `keymaps` in the root of your package's
|
||||||
|
directory.
|
||||||
|
|
||||||
|
|
||||||
|
## Mapping commands to callbacks (handled by this component)
|
||||||
|
|
||||||
|
When a keystroke sequence matches a binding in a given context, a custom
|
||||||
|
DOM event with a type based on the command is dispatched on the target of
|
||||||
|
the keyboard event.
|
||||||
|
|
||||||
|
That custom DOM event (whose type is the command you want to listen to)
|
||||||
|
will propagate up from its original target. That original target may or
|
||||||
|
may not be a descendent of your <KeyCommandsRegion> component.
|
||||||
|
|
||||||
|
Frequently components will want to listen to a keyboard command regardless
|
||||||
|
of where it was fired from. For those, use the `globalHandlers` prop. The
|
||||||
|
DOM event will NOT be passed to `globalHandlers` callbacks.
|
||||||
|
|
||||||
|
Components may also want to listen to keyboard commands that originate
|
||||||
|
within one of their descendents. For those use the `localHandlers` prop.
|
||||||
|
The DOM event WILL be passed to `localHandlers` callback because it is
|
||||||
|
sometimes valuable to call `stopPropagataion` on the custom command event.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
|
||||||
|
- `localHandlers` A mapping between key commands and callbacks for key command events that originate within a descendent of this component.
|
||||||
|
- `globalHandlers` A mapping between key commands and callbacks for key
|
||||||
|
commands that originate from anywhere and are global in scope.
|
||||||
|
- `className` The unique class name that shows up in your keymap.cson
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
In `my-package/lib/my-component.cjsx`:
|
||||||
|
|
||||||
|
```coffee
|
||||||
|
class MyComponent extends React.Component
|
||||||
|
render: ->
|
||||||
|
<KeyCommandsRegion globalHandlers={@globalHandlers()} className="my-component">
|
||||||
|
<div>... sweet component ...</div>
|
||||||
|
</KeyCommandsRegion>
|
||||||
|
|
||||||
|
globalHandlers: ->
|
||||||
|
"core:moveDown": @onMoveDown
|
||||||
|
"core:selectItem": @onSelectItem
|
||||||
|
|
||||||
|
localHandlers: ->
|
||||||
|
"custom:send": (event) => @onSelectItem(); event.stopPropagation()
|
||||||
|
"custom:move": @onCustomMove
|
||||||
|
```
|
||||||
|
|
||||||
|
In `my-package/keymaps/my-package.cson`:
|
||||||
|
|
||||||
|
```coffee
|
||||||
|
".my-component":
|
||||||
|
"cmd-t": "selectItem"
|
||||||
|
"cmd-enter": "sendMessage"
|
||||||
|
```
|
||||||
|
|
||||||
|
###
|
||||||
|
class KeyCommandsRegion extends React.Component
|
||||||
|
@displayName: "KeyCommandsRegion"
|
||||||
|
|
||||||
|
@propTypes:
|
||||||
|
className: React.PropTypes.string
|
||||||
|
localHandlers: React.PropTypes.object
|
||||||
|
globalHandlers: React.PropTypes.object
|
||||||
|
|
||||||
|
@defaultProps:
|
||||||
|
className: ""
|
||||||
|
localHandlers: {}
|
||||||
|
globalHandlers: {}
|
||||||
|
|
||||||
|
componentWillReceiveProps: (newProps) ->
|
||||||
|
@_unmountListeners()
|
||||||
|
@_setupListeners(newProps)
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
@_mounted = true
|
||||||
|
@_setupListeners(@props)
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
@_unmountListeners()
|
||||||
|
|
||||||
|
# When the {KeymapManager} finds a valid keymap in a `.cson` file, it
|
||||||
|
# will create a CustomEvent with the command name as its type. That
|
||||||
|
# custom event will be fired at the originating target and propogate
|
||||||
|
# updwards until it reaches the root window level.
|
||||||
|
#
|
||||||
|
# An event is scoped in the `.cson` files. Since we use that to
|
||||||
|
# determine which keymappings can fire a particular command in a
|
||||||
|
# particular scope, we simply need to listen at the root window level
|
||||||
|
# here for all commands coming in.
|
||||||
|
_setupListeners: (props) ->
|
||||||
|
_.each props.globalHandlers, (callback, handler) ->
|
||||||
|
window.addEventListener(handler, callback)
|
||||||
|
|
||||||
|
return unless @_mounted
|
||||||
|
$el = React.findDOMNode(@)
|
||||||
|
_.each props.localHandlers, (callback, handler) ->
|
||||||
|
$el.addEventListener(handler, callback)
|
||||||
|
|
||||||
|
_unmountListeners: ->
|
||||||
|
_.each @props.globalHandlers, (callback, handler) ->
|
||||||
|
window.removeEventListener(handler, callback)
|
||||||
|
|
||||||
|
return unless @_mounted
|
||||||
|
$el = React.findDOMNode(@)
|
||||||
|
_.each @props.localHandlers, (callback, handler) ->
|
||||||
|
$el.removeEventListener(handler, callback)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
<div className="key-commands-region #{@props.className}">
|
||||||
|
{@props.children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
module.exports = KeyCommandsRegion
|
||||||
|
|
@ -8,6 +8,7 @@ Spinner = require './spinner'
|
||||||
WorkspaceStore,
|
WorkspaceStore,
|
||||||
FocusedContentStore,
|
FocusedContentStore,
|
||||||
AccountStore} = require 'nylas-exports'
|
AccountStore} = require 'nylas-exports'
|
||||||
|
{KeyCommandsRegion} = require 'nylas-component-kit'
|
||||||
EventEmitter = require('events').EventEmitter
|
EventEmitter = require('events').EventEmitter
|
||||||
|
|
||||||
MultiselectListInteractionHandler = require './multiselect-list-interaction-handler'
|
MultiselectListInteractionHandler = require './multiselect-list-interaction-handler'
|
||||||
|
|
@ -30,7 +31,6 @@ class MultiselectList extends React.Component
|
||||||
@propTypes =
|
@propTypes =
|
||||||
className: React.PropTypes.string.isRequired
|
className: React.PropTypes.string.isRequired
|
||||||
collection: React.PropTypes.string.isRequired
|
collection: React.PropTypes.string.isRequired
|
||||||
commands: React.PropTypes.object.isRequired
|
|
||||||
columns: React.PropTypes.array.isRequired
|
columns: React.PropTypes.array.isRequired
|
||||||
dataStore: React.PropTypes.object.isRequired
|
dataStore: React.PropTypes.object.isRequired
|
||||||
itemPropsProvider: React.PropTypes.func.isRequired
|
itemPropsProvider: React.PropTypes.func.isRequired
|
||||||
|
|
@ -66,30 +66,23 @@ class MultiselectList extends React.Component
|
||||||
teardownForProps: =>
|
teardownForProps: =>
|
||||||
return unless @unsubscribers
|
return unless @unsubscribers
|
||||||
unsubscribe() for unsubscribe in @unsubscribers
|
unsubscribe() for unsubscribe in @unsubscribers
|
||||||
@command_unsubscriber.dispose()
|
|
||||||
|
|
||||||
setupForProps: (props) =>
|
setupForProps: (props) =>
|
||||||
commands = _.extend {},
|
|
||||||
'core:focus-item': => @_onEnter()
|
|
||||||
'core:select-item': => @_onSelect()
|
|
||||||
'core:next-item': => @_onShift(1)
|
|
||||||
'core:previous-item': => @_onShift(-1)
|
|
||||||
'core:select-down': => @_onShift(1, {select: true})
|
|
||||||
'core:select-up': => @_onShift(-1, {select: true})
|
|
||||||
'core:list-page-up': => @_onScrollByPage(-1)
|
|
||||||
'core:list-page-down': => @_onScrollByPage(1)
|
|
||||||
'application:pop-sheet': => @_onDeselect()
|
|
||||||
|
|
||||||
Object.keys(props.commands).forEach (key) =>
|
|
||||||
commands[key] = =>
|
|
||||||
context = {focusedId: @state.focusedId}
|
|
||||||
props.commands[key](context)
|
|
||||||
|
|
||||||
@unsubscribers = []
|
@unsubscribers = []
|
||||||
@unsubscribers.push props.dataStore.listen @_onChange
|
@unsubscribers.push props.dataStore.listen @_onChange
|
||||||
@unsubscribers.push WorkspaceStore.listen @_onChange
|
@unsubscribers.push WorkspaceStore.listen @_onChange
|
||||||
@unsubscribers.push FocusedContentStore.listen @_onChange
|
@unsubscribers.push FocusedContentStore.listen @_onChange
|
||||||
@command_unsubscriber = atom.commands.add('body', commands)
|
|
||||||
|
_keymapHandlers: ->
|
||||||
|
'core:focus-item': => @_onEnter()
|
||||||
|
'core:select-item': => @_onSelect()
|
||||||
|
'core:next-item': => @_onShift(1)
|
||||||
|
'core:previous-item': => @_onShift(-1)
|
||||||
|
'core:select-down': => @_onShift(1, {select: true})
|
||||||
|
'core:select-up': => @_onShift(-1, {select: true})
|
||||||
|
'core:list-page-up': => @_onScrollByPage(-1)
|
||||||
|
'core:list-page-down': => @_onScrollByPage(1)
|
||||||
|
'application:pop-sheet': => @_onDeselect()
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
# IMPORTANT: DO NOT pass inline functions as props. _.isEqual thinks these
|
# IMPORTANT: DO NOT pass inline functions as props. _.isEqual thinks these
|
||||||
|
|
@ -122,19 +115,21 @@ class MultiselectList extends React.Component
|
||||||
|
|
||||||
spinnerElement = <Spinner visible={!@state.loaded and @state.empty} />
|
spinnerElement = <Spinner visible={!@state.loaded and @state.empty} />
|
||||||
|
|
||||||
<div className={className} {...otherProps}>
|
<KeyCommandsRegion globalHandlers={@_keymapHandlers()} className="multiselect-list">
|
||||||
<ListTabular
|
<div className={className} {...otherProps}>
|
||||||
ref="list"
|
<ListTabular
|
||||||
columns={@state.computedColumns}
|
ref="list"
|
||||||
scrollTooltipComponent={@props.scrollTooltipComponent}
|
columns={@state.computedColumns}
|
||||||
dataView={@state.dataView}
|
scrollTooltipComponent={@props.scrollTooltipComponent}
|
||||||
itemPropsProvider={@itemPropsProvider}
|
dataView={@state.dataView}
|
||||||
itemHeight={@props.itemHeight}
|
itemPropsProvider={@itemPropsProvider}
|
||||||
onSelect={@_onClickItem}
|
itemHeight={@props.itemHeight}
|
||||||
onDoubleClick={@props.onDoubleClick} />
|
onSelect={@_onClickItem}
|
||||||
{spinnerElement}
|
onDoubleClick={@props.onDoubleClick} />
|
||||||
{emptyElement}
|
{spinnerElement}
|
||||||
</div>
|
{emptyElement}
|
||||||
|
</div>
|
||||||
|
</KeyCommandsRegion>
|
||||||
else
|
else
|
||||||
<div className={className} {...otherProps}>
|
<div className={className} {...otherProps}>
|
||||||
<Spinner visible={true} />
|
<Spinner visible={true} />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
NylasStore = require 'nylas-store'
|
NylasStore = require 'nylas-store'
|
||||||
|
WorkspaceStore = require './workspace-store'
|
||||||
MailViewFilter = require '../../mail-view-filter'
|
MailViewFilter = require '../../mail-view-filter'
|
||||||
CategoryStore = require './category-store'
|
CategoryStore = require './category-store'
|
||||||
AccountStore = require './account-store'
|
AccountStore = require './account-store'
|
||||||
|
|
@ -18,8 +19,9 @@ class FocusedMailViewStore extends NylasStore
|
||||||
else if not CategoryStore.byId(@_mailView.categoryId())
|
else if not CategoryStore.byId(@_mailView.categoryId())
|
||||||
@_setMailView(@_defaultMailView())
|
@_setMailView(@_defaultMailView())
|
||||||
|
|
||||||
_onFocusMailView: (filter) ->
|
_onFocusMailView: (filter) =>
|
||||||
return if filter.isEqual(@_mailView)
|
return if filter.isEqual(@_mailView)
|
||||||
|
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
|
||||||
Actions.searchQueryCommitted('')
|
Actions.searchQueryCommitted('')
|
||||||
@_setMailView(filter)
|
@_setMailView(filter)
|
||||||
|
|
||||||
|
|
@ -36,7 +38,7 @@ class FocusedMailViewStore extends NylasStore
|
||||||
@_mailViewBeforeSearch = null
|
@_mailViewBeforeSearch = null
|
||||||
|
|
||||||
_defaultMailView: ->
|
_defaultMailView: ->
|
||||||
category = CategoryStore.getStandardCategory('inbox')
|
category = CategoryStore.getStandardCategory("inbox")
|
||||||
return null unless category
|
return null unless category
|
||||||
MailViewFilter.forCategory(category)
|
MailViewFilter.forCategory(category)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
Actions = require '../actions'
|
Actions = require '../actions'
|
||||||
AccountStore = require './account-store'
|
AccountStore = require './account-store'
|
||||||
|
CategoryStore = require './category-store'
|
||||||
|
MailViewFilter = require '../../mail-view-filter'
|
||||||
NylasStore = require 'nylas-store'
|
NylasStore = require 'nylas-store'
|
||||||
|
|
||||||
Sheet = {}
|
Sheet = {}
|
||||||
|
|
@ -41,8 +43,38 @@ class WorkspaceStore extends NylasStore
|
||||||
@popToRootSheet()
|
@popToRootSheet()
|
||||||
@trigger()
|
@trigger()
|
||||||
|
|
||||||
atom.commands.add 'body',
|
atom.commands.add 'body', @_navigationCommands()
|
||||||
'application:pop-sheet': => @popSheet()
|
|
||||||
|
_navigationCommands: ->
|
||||||
|
'application:pop-sheet' : => @popSheet()
|
||||||
|
'navigation:go-to-inbox' : => @_setMailViewByName("inbox")
|
||||||
|
'navigation:go-to-starred' : => @_selectStarredView()
|
||||||
|
'navigation:go-to-sent' : => @_setMailViewByName("sent")
|
||||||
|
'navigation:go-to-drafts' : => @_selectDraftsSheet()
|
||||||
|
'navigation:go-to-all' : => @_selectAllView()
|
||||||
|
'navigation:go-to-contacts': => ## TODO
|
||||||
|
'navigation:go-to-tasks' : => ## TODO
|
||||||
|
'navigation:go-to-label' : => ## TODO
|
||||||
|
|
||||||
|
_setMailViewByName: (categoryName) ->
|
||||||
|
category = CategoryStore.getStandardCategory(categoryName)
|
||||||
|
return unless category
|
||||||
|
view = MailViewFilter.forCategory(category)
|
||||||
|
return unless view
|
||||||
|
Actions.focusMailView(view)
|
||||||
|
|
||||||
|
_selectDraftsSheet: ->
|
||||||
|
Actions.selectRootSheet(@Sheet.Drafts)
|
||||||
|
|
||||||
|
_selectAllView: ->
|
||||||
|
category = CategoryStore.getArchiveCategory()
|
||||||
|
return unless category
|
||||||
|
view = MailViewFilter.forCategory(category)
|
||||||
|
return unless view
|
||||||
|
Actions.focusMailView(view)
|
||||||
|
|
||||||
|
_selectStarredView: ->
|
||||||
|
Actions.focusMailView MailViewFilter.forStarred()
|
||||||
|
|
||||||
_resetInstanceVars: =>
|
_resetInstanceVars: =>
|
||||||
@Location = Location = {}
|
@Location = Location = {}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class NylasComponentKit
|
||||||
@load "ButtonDropdown", 'button-dropdown'
|
@load "ButtonDropdown", 'button-dropdown'
|
||||||
@load "Contenteditable", 'contenteditable/contenteditable'
|
@load "Contenteditable", 'contenteditable/contenteditable'
|
||||||
@load "MultiselectList", 'multiselect-list'
|
@load "MultiselectList", 'multiselect-list'
|
||||||
|
@load "KeyCommandsRegion", 'key-commands-region'
|
||||||
@load "InjectedComponent", 'injected-component'
|
@load "InjectedComponent", 'injected-component'
|
||||||
@load "TokenizingTextField", 'tokenizing-text-field'
|
@load "TokenizingTextField", 'tokenizing-text-field'
|
||||||
@load "MultiselectActionBar", 'multiselect-action-bar'
|
@load "MultiselectActionBar", 'multiselect-action-bar'
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,6 @@ class NylasExports
|
||||||
@get "APMWrapper", -> require('../apm-wrapper')
|
@get "APMWrapper", -> require('../apm-wrapper')
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
@get "NylasTestUtils", -> require '../../spec/test_utils'
|
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'
|
||||||
|
|
||||||
module.exports = NylasExports
|
module.exports = NylasExports
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
fs = require 'fs-plus'
|
|
||||||
path = require 'path'
|
|
||||||
KeymapManager = require 'atom-keymap'
|
|
||||||
CSON = require 'season'
|
|
||||||
{jQuery} = require 'space-pen'
|
|
||||||
Grim = require 'grim'
|
|
||||||
|
|
||||||
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
|
|
||||||
@emitter.on 'did-load-bundled-keymaps', callback
|
|
||||||
|
|
||||||
KeymapManager::loadBundledKeymaps = ->
|
|
||||||
# Load the base keymap and the base.platform keymap
|
|
||||||
baseKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'base', ['cson', 'json'])
|
|
||||||
basePlatformKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), "base-#{process.platform}", ['cson', 'json'])
|
|
||||||
@loadKeymap(baseKeymap)
|
|
||||||
@loadKeymap(basePlatformKeymap)
|
|
||||||
|
|
||||||
# Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
|
|
||||||
templateConfigKey = 'core.keymapTemplate'
|
|
||||||
templateKeymapPath = null
|
|
||||||
reloadTemplateKeymap = =>
|
|
||||||
@removeBindingsFromSource(templateKeymapPath) if templateKeymapPath
|
|
||||||
templateFile = atom.config.get(templateConfigKey)
|
|
||||||
if templateFile
|
|
||||||
templateKeymapPath = fs.resolve(path.join(@resourcePath, 'keymaps', 'templates'), templateFile, ['cson', 'json'])
|
|
||||||
if fs.existsSync(templateKeymapPath)
|
|
||||||
@loadKeymap(templateKeymapPath)
|
|
||||||
@emitter.emit('did-reload-keymap', {path: templateKeymapPath})
|
|
||||||
else
|
|
||||||
console.warn("Could not find #{templateKeymapPath}")
|
|
||||||
|
|
||||||
atom.config.observe(templateConfigKey, reloadTemplateKeymap)
|
|
||||||
reloadTemplateKeymap()
|
|
||||||
|
|
||||||
@emit 'bundled-keymaps-loaded' if Grim.includeDeprecatedAPIs
|
|
||||||
@emitter.emit 'did-load-bundled-keymaps'
|
|
||||||
|
|
||||||
KeymapManager::getUserKeymapPath = ->
|
|
||||||
if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
|
|
||||||
userKeymapPath
|
|
||||||
else
|
|
||||||
path.join(@configDirPath, 'keymap.cson')
|
|
||||||
|
|
||||||
KeymapManager::loadUserKeymap = ->
|
|
||||||
userKeymapPath = @getUserKeymapPath()
|
|
||||||
return unless fs.isFileSync(userKeymapPath)
|
|
||||||
|
|
||||||
try
|
|
||||||
@loadKeymap(userKeymapPath, watch: true, suppressErrors: true)
|
|
||||||
catch error
|
|
||||||
if error.message.indexOf('Unable to watch path') > -1
|
|
||||||
message = """
|
|
||||||
Unable to watch path: `#{path.basename(userKeymapPath)}`. Make sure you
|
|
||||||
have permission to read `#{userKeymapPath}`.
|
|
||||||
|
|
||||||
On linux there are currently problems with watch sizes. See
|
|
||||||
[this document][watches] for more info.
|
|
||||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
|
|
||||||
"""
|
|
||||||
console.error(message, {dismissable: true})
|
|
||||||
else
|
|
||||||
detail = error.path
|
|
||||||
stack = error.stack
|
|
||||||
atom.notifications.addFatalError(error.message, {detail, stack, dismissable: true})
|
|
||||||
|
|
||||||
KeymapManager::subscribeToFileReadFailure = ->
|
|
||||||
@onDidFailToReadFile (error) =>
|
|
||||||
userKeymapPath = @getUserKeymapPath()
|
|
||||||
message = "Failed to load `#{userKeymapPath}`"
|
|
||||||
|
|
||||||
detail = if error.location?
|
|
||||||
error.stack
|
|
||||||
else
|
|
||||||
error.message
|
|
||||||
|
|
||||||
console.error(message, {detail: detail, dismissable: true})
|
|
||||||
|
|
||||||
# This enables command handlers registered via jQuery to call
|
|
||||||
# `.abortKeyBinding()` on the `jQuery.Event` object passed to the handler.
|
|
||||||
jQuery.Event::abortKeyBinding = ->
|
|
||||||
@originalEvent?.abortKeyBinding?()
|
|
||||||
|
|
||||||
module.exports = KeymapManager
|
|
||||||
97
src/keymap-manager.coffee
Normal file
97
src/keymap-manager.coffee
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
fs = require 'fs-plus'
|
||||||
|
path = require 'path'
|
||||||
|
CSON = require 'season'
|
||||||
|
AtomKeymap = require 'atom-keymap'
|
||||||
|
|
||||||
|
class KeymapManager extends AtomKeymap
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@subscribeToFileReadFailure()
|
||||||
|
|
||||||
|
onDidLoadBundledKeymaps: (callback) ->
|
||||||
|
@emitter.on 'did-load-bundled-keymaps', callback
|
||||||
|
|
||||||
|
# N1 adds the `cmdctrl` extension. This will use `cmd` or `ctrl` on a
|
||||||
|
# mac, and `ctrl` only on windows and linux.
|
||||||
|
readKeymap: (args...) ->
|
||||||
|
re = /(cmdctrl|ctrlcmd)/i
|
||||||
|
keymap = super(args...)
|
||||||
|
for selector, keyBindings of keymap
|
||||||
|
normalizedBindings = {}
|
||||||
|
for keystrokes, command of keyBindings
|
||||||
|
if re.test keystrokes
|
||||||
|
if process.platform is "darwin"
|
||||||
|
newKeystrokes1= keystrokes.replace(re, "ctrl")
|
||||||
|
newKeystrokes2= keystrokes.replace(re, "cmd")
|
||||||
|
normalizedBindings[newKeystrokes1] = command
|
||||||
|
normalizedBindings[newKeystrokes2] = command
|
||||||
|
else
|
||||||
|
newKeystrokes = keystrokes.replace(re, "ctrl")
|
||||||
|
normalizedBindings[newKeystrokes] = command
|
||||||
|
else
|
||||||
|
normalizedBindings[keystrokes] = command
|
||||||
|
keymap[selector] = normalizedBindings
|
||||||
|
|
||||||
|
return keymap
|
||||||
|
|
||||||
|
loadBundledKeymaps: ->
|
||||||
|
# Load the base keymap and the base.platform keymap
|
||||||
|
baseKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'base', ['cson', 'json'])
|
||||||
|
inputResetKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'input-reset', ['cson', 'json'])
|
||||||
|
basePlatformKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), "base-#{process.platform}", ['cson', 'json'])
|
||||||
|
@loadKeymap(baseKeymap)
|
||||||
|
@loadKeymap(inputResetKeymap)
|
||||||
|
@loadKeymap(basePlatformKeymap)
|
||||||
|
|
||||||
|
# Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
|
||||||
|
templateConfigKey = 'core.keymapTemplate'
|
||||||
|
templateKeymapPath = null
|
||||||
|
reloadTemplateKeymap = =>
|
||||||
|
@removeBindingsFromSource(templateKeymapPath) if templateKeymapPath
|
||||||
|
templateFile = atom.config.get(templateConfigKey)
|
||||||
|
if templateFile
|
||||||
|
templateKeymapPath = fs.resolve(path.join(@resourcePath, 'keymaps', 'templates'), templateFile, ['cson', 'json'])
|
||||||
|
if fs.existsSync(templateKeymapPath)
|
||||||
|
@loadKeymap(templateKeymapPath)
|
||||||
|
@emitter.emit('did-reload-keymap', {path: templateKeymapPath})
|
||||||
|
else
|
||||||
|
console.warn("Could not find #{templateKeymapPath}")
|
||||||
|
|
||||||
|
atom.config.observe(templateConfigKey, reloadTemplateKeymap)
|
||||||
|
reloadTemplateKeymap()
|
||||||
|
|
||||||
|
@emitter.emit 'did-load-bundled-keymaps'
|
||||||
|
|
||||||
|
getUserKeymapPath: ->
|
||||||
|
if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
|
||||||
|
userKeymapPath
|
||||||
|
else
|
||||||
|
path.join(@configDirPath, 'keymap.cson')
|
||||||
|
|
||||||
|
loadUserKeymap: ->
|
||||||
|
userKeymapPath = @getUserKeymapPath()
|
||||||
|
return unless fs.isFileSync(userKeymapPath)
|
||||||
|
|
||||||
|
try
|
||||||
|
@loadKeymap(userKeymapPath, watch: true, suppressErrors: true)
|
||||||
|
catch error
|
||||||
|
message = """
|
||||||
|
Unable to watch path: `#{path.basename(userKeymapPath)}`. Make sure you
|
||||||
|
have permission to read `#{userKeymapPath}`.
|
||||||
|
"""
|
||||||
|
console.error(message, {dismissable: true})
|
||||||
|
|
||||||
|
subscribeToFileReadFailure: ->
|
||||||
|
@onDidFailToReadFile (error) =>
|
||||||
|
userKeymapPath = @getUserKeymapPath()
|
||||||
|
message = "Failed to load `#{userKeymapPath}`"
|
||||||
|
|
||||||
|
detail = if error.location?
|
||||||
|
error.stack
|
||||||
|
else
|
||||||
|
error.message
|
||||||
|
|
||||||
|
console.error(message, {detail: detail, dismissable: true})
|
||||||
|
|
||||||
|
module.exports = KeymapManager
|
||||||
|
|
@ -21,6 +21,9 @@ class MailViewFilter
|
||||||
@forSearch: (query) ->
|
@forSearch: (query) ->
|
||||||
new SearchMailViewFilter(query)
|
new SearchMailViewFilter(query)
|
||||||
|
|
||||||
|
@forAll: ->
|
||||||
|
new AllMailViewFilter()
|
||||||
|
|
||||||
# Instance Methods
|
# Instance Methods
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
|
|
@ -83,6 +86,31 @@ class SearchMailViewFilter extends MailViewFilter
|
||||||
categoryId: ->
|
categoryId: ->
|
||||||
null
|
null
|
||||||
|
|
||||||
|
class AllMailViewFilter extends MailViewFilter
|
||||||
|
constructor: ->
|
||||||
|
@name = "All"
|
||||||
|
@iconName = "all-mail.png"
|
||||||
|
@
|
||||||
|
|
||||||
|
isEqual: (other) ->
|
||||||
|
super(other) and other.searchQuery is @searchQuery
|
||||||
|
|
||||||
|
matchers: ->
|
||||||
|
account = AccountStore.current()
|
||||||
|
[Thread.attributes.accountId.equal(account.id)]
|
||||||
|
|
||||||
|
canApplyToThreads: ->
|
||||||
|
true
|
||||||
|
|
||||||
|
canArchiveThreads: ->
|
||||||
|
false
|
||||||
|
|
||||||
|
canTrashThreads: ->
|
||||||
|
false
|
||||||
|
|
||||||
|
categoryId: ->
|
||||||
|
CategoryStore.getStandardCategory("all")?.id
|
||||||
|
|
||||||
|
|
||||||
class StarredMailViewFilter extends MailViewFilter
|
class StarredMailViewFilter extends MailViewFilter
|
||||||
constructor: ->
|
constructor: ->
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ class Package
|
||||||
if @bundledPackage and packagesCache[@name]?
|
if @bundledPackage and packagesCache[@name]?
|
||||||
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
|
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
|
||||||
else
|
else
|
||||||
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
|
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, atom.keymaps.readKeymap(keymapPath) ? {}]
|
||||||
|
|
||||||
loadMenus: ->
|
loadMenus: ->
|
||||||
if @bundledPackage and packagesCache[@name]?
|
if @bundledPackage and packagesCache[@name]?
|
||||||
|
|
|
||||||
5
static/components/key-commands-region.less
Normal file
5
static/components/key-commands-region.less
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.key-commands-region {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
@ -25,3 +25,4 @@
|
||||||
@import "components/spinner";
|
@import "components/spinner";
|
||||||
@import "components/generated-form";
|
@import "components/generated-form";
|
||||||
@import "components/unsafe";
|
@import "components/unsafe";
|
||||||
|
@import "components/key-commands-region";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue