mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 15:26:06 +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
118761d79e
commit
455b418d6f
|
@ -1,24 +1,26 @@
|
|||
# Your keymap
|
||||
#
|
||||
# Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors
|
||||
# to apply styles to elements, Atom keymaps use selectors to associate
|
||||
# keystrokes with events in specific contexts.
|
||||
# N1 keymaps work in conjunction with the {KeyCommandsRegion} React
|
||||
# component.
|
||||
#
|
||||
# You can create a new keybinding in this file by typing "key" and then hitting
|
||||
# tab.
|
||||
# A key, or sequence of keys is first mapped to a "command".
|
||||
#
|
||||
# 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':
|
||||
# 'enter': 'editor:newline'
|
||||
# The keyboard -> command mapping is defined in this `.cson` file. Each
|
||||
# mapping is scoped under the component that it applies to by matching the
|
||||
# root-level CSS class of that component.
|
||||
#
|
||||
# '.workspace':
|
||||
# 'ctrl-shift-p': 'core:move-up'
|
||||
# 'ctrl-p': 'core:move-down'
|
||||
# Any global, top-level mappings are scoped under the `body` selector.
|
||||
#
|
||||
# You can find more information about keymaps in these guides:
|
||||
# * https://atom.io/docs/latest/customizing-atom#customizing-key-bindings
|
||||
# * https://atom.io/docs/latest/advanced/keymaps
|
||||
# For example:
|
||||
#
|
||||
# 'body':
|
||||
# 'ctrl-c': 'application:new-message'
|
||||
#
|
||||
# '.my-custom-package':
|
||||
# 'ctrl-p': 'myPackage:customAction'
|
||||
#
|
||||
# This file uses CoffeeScript Object Notation (CSON).
|
||||
# If you are unfamiliar with CSON, you can read more about it here:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
shell = require 'shell'
|
||||
GithubStore = require './github-store'
|
||||
{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
|
||||
|
@ -68,23 +68,24 @@ class ViewOnGithubButton extends React.Component
|
|||
# are cleaned up. Every time the `GithubStore` calls its `trigger`
|
||||
# method, the `_onStoreChanged` callback will be fired.
|
||||
@_unlisten = GithubStore.listen(@_onStoreChanged)
|
||||
@_keymapUnlisten = atom.commands.add 'body', {
|
||||
'github:open': @_openLink
|
||||
}
|
||||
|
||||
componentWillUnmount: ->
|
||||
@_unlisten?()
|
||||
@_keymapUnlisten?.dispose()
|
||||
|
||||
_keymapHandlers: ->
|
||||
'github:open': @_openLink
|
||||
|
||||
render: ->
|
||||
return null unless @state.link
|
||||
<button className="btn btn-toolbar"
|
||||
onClick={@_openLink}
|
||||
title={"Visit Thread on GitHub"}>
|
||||
<RetinaImg
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
|
||||
</button>
|
||||
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||
<button className="btn btn-toolbar"
|
||||
onClick={@_openLink}
|
||||
title={"Visit Thread on GitHub"}>
|
||||
<RetinaImg
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
|
||||
</button>
|
||||
</KeyCommandsRegion>
|
||||
|
||||
|
||||
#### Super common N1 Component private methods ####
|
||||
|
|
|
@ -19,6 +19,7 @@ React = require 'react'
|
|||
{Menu,
|
||||
Popover,
|
||||
RetinaImg,
|
||||
KeyCommandsRegion,
|
||||
LabelColorizer} = require 'nylas-component-kit'
|
||||
|
||||
# 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 AccountStore.listen @_onStoreChanged
|
||||
|
||||
@_commandUnsubscriber = atom.commands.add 'body',
|
||||
"application:change-category": @_onOpenCategoryPopover
|
||||
|
||||
# If the threads we're picking categories for change, (like when they
|
||||
# get their categories updated), we expect our parents to pass us new
|
||||
# props. We don't listen to the DatabaseStore ourselves.
|
||||
|
@ -50,7 +48,9 @@ class CategoryPicker extends React.Component
|
|||
componentWillUnmount: =>
|
||||
return unless @unsubscribers
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
@_commandUnsubscriber.dispose()
|
||||
|
||||
_keymapHandlers: ->
|
||||
"application:change-category": @_onOpenCategoryPopover
|
||||
|
||||
render: =>
|
||||
return <span></span> unless @_account
|
||||
|
@ -84,23 +84,25 @@ class CategoryPicker extends React.Component
|
|||
onChange={@_onSearchValueChange}/>
|
||||
]
|
||||
|
||||
<Popover className="category-picker"
|
||||
ref="popover"
|
||||
onOpened={@_onPopoverOpened}
|
||||
onClosed={@_onPopoverClosed}
|
||||
direction="down-align-left"
|
||||
style={order: -103}
|
||||
buttonComponent={button}>
|
||||
<Menu ref="menu"
|
||||
headerComponents={headerComponents}
|
||||
footerComponents={[]}
|
||||
items={@state.categoryData}
|
||||
itemKey={ (item) -> item.id }
|
||||
itemContent={@_renderItemContent}
|
||||
onSelect={@_onSelectCategory}
|
||||
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
|
||||
/>
|
||||
</Popover>
|
||||
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||
<Popover className="category-picker"
|
||||
ref="popover"
|
||||
onOpened={@_onPopoverOpened}
|
||||
onClosed={@_onPopoverClosed}
|
||||
direction="down-align-left"
|
||||
style={order: -103}
|
||||
buttonComponent={button}>
|
||||
<Menu ref="menu"
|
||||
headerComponents={headerComponents}
|
||||
footerComponents={[]}
|
||||
items={@state.categoryData}
|
||||
itemKey={ (item) -> item.id }
|
||||
itemContent={@_renderItemContent}
|
||||
onSelect={@_onSelectCategory}
|
||||
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
|
||||
/>
|
||||
</Popover>
|
||||
</KeyCommandsRegion>
|
||||
|
||||
_onOpenCategoryPopover: =>
|
||||
return unless @_threads().length > 0
|
||||
|
|
|
@ -1,31 +1,16 @@
|
|||
'.composer-outer-wrap, .composer-outer-wrap input, .composer-outer-wrap
|
||||
div[contenteditable]':
|
||||
'cmd-B' : 'composer:show-and-focus-bcc'
|
||||
'cmd-C' : 'composer:show-and-focus-cc'
|
||||
'ctrl-B' : 'composer:show-and-focus-bcc'
|
||||
'ctrl-C' : 'composer:show-and-focus-cc'
|
||||
'cmd-T' : 'composer:focus-to'
|
||||
'ctrl-T' : 'composer:focus-to'
|
||||
'cmd-enter' : 'composer:send-message'
|
||||
'ctrl-enter' : 'composer:send-message'
|
||||
'cmdctrl-T' : 'composer:focus-to'
|
||||
'cmdctrl-C' : 'composer:show-and-focus-cc'
|
||||
'cmdctrl-B' : 'composer:show-and-focus-bcc'
|
||||
'cmdctrl-F' : 'composer:show-and-focus-from'
|
||||
'cmdctrl-enter': 'composer:send-message'
|
||||
'cmdctrl-z' : 'composer:undo'
|
||||
'cmdctrl-Z' : 'composer:redo'
|
||||
'cmdctrl-y' : 'composer:redo'
|
||||
|
||||
'.composer-outer-wrap':
|
||||
'delete' : 'composer:no-op'
|
||||
'delete': 'composer:no-op'
|
||||
|
||||
'.composer-outer-wrap, .composer-outer-wrap div[contenteditable]':
|
||||
'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'
|
||||
'escape': 'composer:delete-empty-draft'
|
||||
|
|
|
@ -20,6 +20,7 @@ class CollapsedParticipants extends React.Component
|
|||
bcc: []
|
||||
|
||||
constructor: (@props={}) ->
|
||||
@_keyPrefix = Utils.generateTempId()
|
||||
@state =
|
||||
numToDisplay: 999
|
||||
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>
|
||||
|
||||
_collapsedContact: (contact) ->
|
||||
_collapsedContact: (contact) =>
|
||||
name = contact.displayName()
|
||||
<span key={contact.id}
|
||||
key = @_keyPrefix + contact.email + contact.name
|
||||
<span key={key}
|
||||
className="collapsed-contact regular-contact">{name}</span>
|
||||
|
||||
_collapsedBccContact: (contact, i) ->
|
||||
_collapsedBccContact: (contact, i) =>
|
||||
name = contact.displayName()
|
||||
key = @_keyPrefix + contact.email + contact.name
|
||||
if i is 0 then name = "Bcc: #{name}"
|
||||
<span key={contact.id}
|
||||
<span key={key}
|
||||
className="collapsed-contact bcc-contact">{name}</span>
|
||||
|
||||
_setNumHiddenParticipants: ->
|
||||
|
|
|
@ -17,6 +17,7 @@ React = require 'react'
|
|||
ScrollRegion,
|
||||
Contenteditable,
|
||||
InjectedComponent,
|
||||
KeyCommandsRegion,
|
||||
FocusTrackingRegion,
|
||||
InjectedComponentSet} = require 'nylas-component-kit'
|
||||
|
||||
|
@ -77,15 +78,6 @@ class ComposerView extends React.Component
|
|||
@_usubs = []
|
||||
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
|
||||
@_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()
|
||||
|
||||
componentWillUnmount: =>
|
||||
|
@ -93,7 +85,6 @@ class ComposerView extends React.Component
|
|||
@_teardownForDraft()
|
||||
@_deleteDraftIfEmpty()
|
||||
usub() for usub in @_usubs
|
||||
@_keymapUnlisten.dispose() if @_keymapUnlisten
|
||||
|
||||
componentDidUpdate: =>
|
||||
# We want to use a temporary variable instead of putting this into the
|
||||
|
@ -105,6 +96,19 @@ class ComposerView extends React.Component
|
|||
|
||||
@_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: ->
|
||||
if @state.focusedField
|
||||
return unless @refs[@state.focusedField]
|
||||
|
@ -130,7 +134,7 @@ class ComposerView extends React.Component
|
|||
@undoManager = new UndoManager
|
||||
DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
|
||||
|
||||
_setupSession: (proxy) =>
|
||||
__setupSessionsetupSession: (proxy) =>
|
||||
return if @_unmounted
|
||||
return unless proxy.draftClientId is @props.draftClientId
|
||||
@_proxy = proxy
|
||||
|
@ -149,20 +153,26 @@ class ComposerView extends React.Component
|
|||
if @_proxy
|
||||
@_proxy.changes.commit()
|
||||
|
||||
render: =>
|
||||
render: ->
|
||||
<KeyCommandsRegion localHandlers={@_keymapHandlers()}
|
||||
className="composer-outer-wrap">
|
||||
{@_renderComposerWrap()}
|
||||
</KeyCommandsRegion>
|
||||
|
||||
_renderComposerWrap: =>
|
||||
if @props.mode is "inline"
|
||||
<FocusTrackingRegion className={@_wrapClasses()}
|
||||
ref="composer"
|
||||
ref="composerWrap"
|
||||
tabIndex="-1">
|
||||
{@_renderComposer()}
|
||||
</FocusTrackingRegion>
|
||||
else
|
||||
<div className={@_wrapClasses()} ref="composer">
|
||||
<div className={@_wrapClasses()} ref="composerWrap">
|
||||
{@_renderComposer()}
|
||||
</div>
|
||||
|
||||
_wrapClasses: =>
|
||||
"message-item-white-wrap composer-outer-wrap #{@props.className ? ""}"
|
||||
"message-item-white-wrap #{@props.className ? ""}"
|
||||
|
||||
_renderComposer: =>
|
||||
<DropZone className="composer-inner-wrap"
|
||||
|
@ -320,7 +330,7 @@ class ComposerView extends React.Component
|
|||
# component. We provide it our boundingClientRect so it can calculate
|
||||
# this value.
|
||||
_getComposerBoundingRect: =>
|
||||
React.findDOMNode(@refs.composer).getBoundingClientRect()
|
||||
React.findDOMNode(@refs.composerWrap).getBoundingClientRect()
|
||||
|
||||
_onScrollToBottom: ->
|
||||
if @props.onRequestScrollTo
|
||||
|
|
|
@ -133,6 +133,9 @@ describe "populated composer", ->
|
|||
@isSending = {state: false}
|
||||
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
||||
|
||||
afterEach ->
|
||||
DraftStore._cleanupAllSessions()
|
||||
|
||||
describe "when sending a new message", ->
|
||||
it 'makes a request with the message contents', ->
|
||||
useDraft.call @
|
||||
|
@ -528,20 +531,21 @@ describe "populated composer", ->
|
|||
useFullDraft.apply(@)
|
||||
makeComposer.call(@)
|
||||
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
|
||||
@$composer = @composer.refs.composerWrap
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
it "doesn't let you send twice", ->
|
||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
|
||||
@isSending.state = true
|
||||
DraftStore.trigger()
|
||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
||||
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
|
|
|
@ -10,18 +10,19 @@ MessageItemContainer = require './message-item-container'
|
|||
MessageStore,
|
||||
DatabaseStore,
|
||||
WorkspaceStore,
|
||||
ComponentRegistry,
|
||||
ChangeLabelsTask,
|
||||
ComponentRegistry,
|
||||
ChangeStarredTask} = require("nylas-exports")
|
||||
|
||||
{Spinner,
|
||||
RetinaImg,
|
||||
MailLabel,
|
||||
ScrollRegion,
|
||||
ResizableRegion,
|
||||
RetinaImg,
|
||||
InjectedComponentSet,
|
||||
MailLabel,
|
||||
MailImportantIcon,
|
||||
InjectedComponent} = require('nylas-component-kit')
|
||||
InjectedComponent,
|
||||
KeyCommandsRegion,
|
||||
InjectedComponentSet} = require('nylas-component-kit')
|
||||
|
||||
class MessageListScrollTooltip extends React.Component
|
||||
@displayName: 'MessageListScrollTooltip'
|
||||
|
@ -73,18 +74,8 @@ class MessageList extends React.Component
|
|||
@_unsubscribers = []
|
||||
@_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: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
@command_unsubscriber.dispose()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
|
@ -97,6 +88,13 @@ class MessageList extends React.Component
|
|||
if newDraftClientIds.length > 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) =>
|
||||
oldDraftIds = _.map(_.filter((prevState.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
|
||||
"ready": not @state.loading
|
||||
|
||||
<div className="message-list" id="message-list">
|
||||
<ScrollRegion tabIndex="-1"
|
||||
className={wrapClass}
|
||||
scrollTooltipComponent={MessageListScrollTooltip}
|
||||
ref="messageWrap">
|
||||
{@_renderSubject()}
|
||||
<div className="headers" style={position:'relative'}>
|
||||
<InjectedComponentSet
|
||||
className="message-list-notification-bars"
|
||||
matching={role:"MessageListNotificationBar"}
|
||||
exposedProps={thread: @state.currentThread}/>
|
||||
<InjectedComponentSet
|
||||
className="message-list-headers"
|
||||
matching={role:"MessageListHeaders"}
|
||||
exposedProps={thread: @state.currentThread}/>
|
||||
</div>
|
||||
{@_messageElements()}
|
||||
</ScrollRegion>
|
||||
<Spinner visible={@state.loading} />
|
||||
</div>
|
||||
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
|
||||
<div className="message-list" id="message-list">
|
||||
<ScrollRegion tabIndex="-1"
|
||||
className={wrapClass}
|
||||
scrollTooltipComponent={MessageListScrollTooltip}
|
||||
ref="messageWrap">
|
||||
{@_renderSubject()}
|
||||
<div className="headers" style={position:'relative'}>
|
||||
<InjectedComponentSet
|
||||
className="message-list-notification-bars"
|
||||
matching={role:"MessageListNotificationBar"}
|
||||
exposedProps={thread: @state.currentThread}/>
|
||||
<InjectedComponentSet
|
||||
className="message-list-headers"
|
||||
matching={role:"MessageListHeaders"}
|
||||
exposedProps={thread: @state.currentThread}/>
|
||||
</div>
|
||||
{@_messageElements()}
|
||||
</ScrollRegion>
|
||||
<Spinner visible={@state.loading} />
|
||||
</div>
|
||||
</KeyCommandsRegion>
|
||||
|
||||
_renderSubject: ->
|
||||
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="msg-lines" style={height: h*lines.length}>
|
||||
{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>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ DisplayedKeybindings = [
|
|||
['application:focus-search', 'Search'],
|
||||
['application:change-category', 'Change Folder / Labels'],
|
||||
['core:select-item', 'Select Focused Item'],
|
||||
['core:star-item', 'Star Focused Item'],
|
||||
['application:star-item', 'Star Focused Item'],
|
||||
]
|
||||
|
||||
class PreferencesKeymaps extends React.Component
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{Actions, WorkspaceStore} = require 'nylas-exports'
|
||||
{Menu, RetinaImg} = require 'nylas-component-kit'
|
||||
{Menu, RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
|
||||
SearchSuggestionStore = require './search-suggestion-store'
|
||||
_ = require 'underscore'
|
||||
|
||||
|
@ -20,20 +20,16 @@ class SearchBar extends React.Component
|
|||
@usub.push SearchSuggestionStore.listen @_onChange
|
||||
@usub.push WorkspaceStore.listen =>
|
||||
@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
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: =>
|
||||
usub() for usub in @usub
|
||||
@body_unsubscriber.dispose()
|
||||
@search_unsubscriber.dispose()
|
||||
|
||||
_keymapHandlers: ->
|
||||
'application:focus-search': @_onFocusSearch
|
||||
'search-bar:escape-search': @_clearAndBlur
|
||||
|
||||
render: =>
|
||||
inputValue = @_queryToString(@state.query)
|
||||
|
@ -74,16 +70,18 @@ class SearchBar extends React.Component
|
|||
else
|
||||
item.label
|
||||
|
||||
<div className="search-bar">
|
||||
<Menu ref="menu"
|
||||
className={@_containerClasses()}
|
||||
headerComponents={headerComponents}
|
||||
items={@state.suggestions}
|
||||
itemContent={itemContentFunc}
|
||||
itemKey={ (item) -> item.id ? item.label }
|
||||
onSelect={@_onSelectSuggestion}
|
||||
/>
|
||||
</div>
|
||||
<KeyCommandsRegion className="search-bar" globalHandlers={@_keymapHandlers()}>
|
||||
<div>
|
||||
<Menu ref="menu"
|
||||
className={@_containerClasses()}
|
||||
headerComponents={headerComponents}
|
||||
items={@state.suggestions}
|
||||
itemContent={itemContentFunc}
|
||||
itemKey={ (item) -> item.id ? item.label }
|
||||
onSelect={@_onSelectSuggestion}
|
||||
/>
|
||||
</div>
|
||||
</KeyCommandsRegion>
|
||||
|
||||
_onFocusSearch: =>
|
||||
React.findDOMNode(@refs.searchInput).focus()
|
||||
|
|
|
@ -5,6 +5,7 @@ classNames = require 'classnames'
|
|||
MultiselectList,
|
||||
RetinaImg,
|
||||
MailLabel,
|
||||
KeyCommandsRegion,
|
||||
InjectedComponentSet} = require 'nylas-component-kit'
|
||||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
|
@ -166,24 +167,6 @@ class ThreadList extends React.Component
|
|||
|
||||
@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) ->
|
||||
className: classNames
|
||||
'unread': item.unread
|
||||
|
@ -196,12 +179,35 @@ class ThreadList extends React.Component
|
|||
componentWillUnmount: =>
|
||||
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'
|
||||
<MultiselectList
|
||||
dataStore={ThreadListStore}
|
||||
columns={@wideColumns}
|
||||
commands={@commands}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={39}
|
||||
className="thread-list"
|
||||
|
@ -215,7 +221,6 @@ class ThreadList extends React.Component
|
|||
<MultiselectList
|
||||
dataStore={ThreadListStore}
|
||||
columns={@narrowColumns}
|
||||
commands={@commands}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={90}
|
||||
className="thread-list thread-list-narrow"
|
||||
|
@ -260,8 +265,6 @@ class ThreadList extends React.Component
|
|||
if current isnt desired
|
||||
@setState(style: desired)
|
||||
|
||||
# Additional Commands
|
||||
|
||||
_threadsForKeyboardAction: ->
|
||||
return null unless ThreadListStore.view()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
|
|
|
@ -1,39 +1,13 @@
|
|||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
||||
# to be listed in this file.
|
||||
# Note: Only put keymaps in here that apply to Mac ONLY
|
||||
#
|
||||
# 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':
|
||||
# Mac application keys
|
||||
'cmd-q': 'application:quit'
|
||||
'cmd-m': 'application:minimize'
|
||||
'cmd-h': 'application:hide'
|
||||
'cmd-1': 'application:show-main-window'
|
||||
'cmd-m': 'application:minimize'
|
||||
'cmd-alt-w': 'application:show-work-window'
|
||||
'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'
|
||||
'ctrl-alt-cmd-l': 'window:reload'
|
||||
'cmd-alt-ctrl-p': 'application:run-package-specs'
|
||||
|
||||
'body *[contenteditable]':
|
||||
'cmd-z': 'native!'
|
||||
'cmd-Z': 'native!'
|
||||
'cmd-y': 'native!'
|
||||
'alt-cmd-ctrl-m': 'application:zoom'
|
||||
|
|
|
@ -1,36 +1,11 @@
|
|||
# Note: For a menu item to have a keyboard equiavalent, it needs
|
||||
# to be listed in this file.
|
||||
# Note: Only put keymaps in here that apply to Linux ONLY
|
||||
#
|
||||
# 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
|
||||
'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'
|
||||
'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'
|
||||
|
||||
'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
|
||||
# to be listed in this file.
|
||||
# Note: Only put keymaps in here that apply to Windows ONLY
|
||||
#
|
||||
# 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':
|
||||
# Windows email-specific menu items
|
||||
'ctrl-n': 'application:new-message' # Outlook
|
||||
'ctrl-r': 'application:reply' # Outlook
|
||||
'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'
|
||||
'cmd-alt-l': 'window:reload'
|
||||
'cmd-alt-i': 'window:toggle-dev-tools'
|
||||
'cmd-alt-w': 'application:show-work-window'
|
||||
'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
|
||||
# menu items. In the future, we should break these into files like:
|
||||
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
|
||||
# 1. keymaps/base.cson - (This file) Core, universal keymaps across all platforms
|
||||
# 2. keymaps/base-darwin.cson - Any universal mac-only keymaps
|
||||
# 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':
|
||||
'escape' : 'application:pop-sheet'
|
||||
'cmd-,' : 'application:open-preferences'
|
||||
'up' : 'core:previous-item'
|
||||
'down' : 'core:next-item'
|
||||
'enter' : 'core:focus-item'
|
||||
'delete' : 'core:remove-from-view'
|
||||
'backspace': 'core:remove-from-view'
|
||||
|
||||
'pageup' : 'core:messages-page-up'
|
||||
'pagedown' : 'core:messages-page-down'
|
||||
'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'
|
||||
### Core system commands. ###
|
||||
# These have their default effects, but map to
|
||||
# commands to allow for custom interactions.
|
||||
'cmdctrl-z': 'core:undo'
|
||||
'cmdctrl-Z': 'core:redo'
|
||||
'cmdctrl-y': 'core:redo'
|
||||
'cmdctrl-x': 'core:cut'
|
||||
'cmdctrl-c': 'core:copy'
|
||||
'cmdctrl-v': 'core:paste'
|
||||
'cmdctrl-a': 'core:select-all'
|
||||
'shift-delete': 'core:cut'
|
||||
|
||||
# Inputs are native by default.
|
||||
# Also make sure not to catch anything intended for a webview
|
||||
'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!'
|
||||
'up' : 'core:previous-item'
|
||||
'down' : 'core:next-item'
|
||||
'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'
|
||||
|
||||
'body input, body textarea':
|
||||
'tab': 'core:focus-next'
|
||||
'shift-tab': 'core:focus-previous'
|
||||
### Core application commands. ###
|
||||
'cmdctrl-q' : 'application:quit'
|
||||
'cmdctrl-w' : 'window:close'
|
||||
|
||||
'body webview':
|
||||
'tab': 'native!'
|
||||
'shift-tab': 'native!'
|
||||
### Universal N1 commands. ###
|
||||
'enter' : 'core:focus-item'
|
||||
'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
|
||||
'body *[contenteditable]':
|
||||
'tab': 'native!'
|
||||
'shift-tab': 'native!'
|
||||
'pageup' : 'core:messages-page-up'
|
||||
'pagedown' : 'core:messages-page-down'
|
||||
'shift-pageup' : 'core:list-page-up'
|
||||
'shift-pagedown': 'core:list-page-down'
|
||||
|
||||
# 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!'
|
||||
### N1 developer commands. ###
|
||||
'cmdctrl-alt-l': 'window:reload'
|
||||
'cmdctrl-alt-i': 'window:toggle-dev-tools'
|
||||
'cmdctrl-alt-w': 'application:show-work-window'
|
||||
'cmdctrl-alt-s': 'application:run-all-specs'
|
||||
'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
|
||||
# to be listed in this file.
|
||||
|
||||
'body.platform-darwin':
|
||||
'cmd-n' : 'application:new-message'
|
||||
'cmd-r' : 'application:reply'
|
||||
'cmd-R' : 'application:reply-all'
|
||||
'cmd-F' : 'application:forward'
|
||||
'cmd-alt-f': 'application:focus-search'
|
||||
'cmd-D': 'application:send-message'
|
||||
'cmd-V': 'application:change-category'
|
||||
'cmd-e' : 'core: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'
|
||||
'body':
|
||||
'cmdctrl-n' : 'application:new-message'
|
||||
'cmdctrl-r' : 'application:reply'
|
||||
'cmdctrl-R' : 'application:reply-all'
|
||||
'cmdctrl-F' : 'application:forward'
|
||||
'cmdctrl-alt-f': 'application:focus-search'
|
||||
'cmdctrl-D': 'application:send-message'
|
||||
'cmdctrl-V': 'application:change-category'
|
||||
'cmdctrl-e' : 'application:archive-item'
|
||||
|
|
|
@ -1,38 +1,83 @@
|
|||
# Email-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...
|
||||
# Gmail-specific core key-mappings
|
||||
|
||||
'body':
|
||||
'c' : 'application:new-message'
|
||||
'/' : 'application:focus-search'
|
||||
|
||||
'r' : 'application:reply'
|
||||
'a' : 'application:reply-all'
|
||||
'f' : 'application:forward'
|
||||
'l' : 'application:change-category'
|
||||
'u' : 'application:pop-sheet'
|
||||
### Jumping ###
|
||||
'g i' : 'navigation:go-to-inbox'
|
||||
'g s' : 'navigation:go-to-starred'
|
||||
'g t' : 'navigation:go-to-sent'
|
||||
'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'
|
||||
'j' : 'core:next-item'
|
||||
']' : 'core:remove-and-previous'
|
||||
'[' : 'core:remove-and-next'
|
||||
'#' : 'core:delete-item'
|
||||
'e' : 'core:archive-item'
|
||||
's' : 'core:star-item'
|
||||
'x' : 'core:select-item'
|
||||
### Threadlist selection ###
|
||||
'* a': 'thread-list:select-all'
|
||||
'* n': 'thread-list:deselect-all'
|
||||
'* r': 'thread-list:select-read'
|
||||
'* u': 'thread-list:select-unread'
|
||||
'* s': 'thread-list:select-starred'
|
||||
'* t': 'thread-list:select-unstarred'
|
||||
|
||||
# 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':
|
||||
'cmd-n' : 'application:new-message'
|
||||
'cmd-r' : 'application:reply'
|
||||
'cmd-R' : 'application:reply-all'
|
||||
'cmd-F' : 'application:forward'
|
||||
### Application ###
|
||||
'c': 'application:new-message'
|
||||
'd': 'application:new-message'
|
||||
'/': 'application:focus-search'
|
||||
'.': 'application:more-actions'
|
||||
'l': 'application:change-category'
|
||||
'v': 'application:change-category'
|
||||
'?': 'application:open-help'
|
||||
|
||||
'body.platform-linux, body.platform-win32':
|
||||
'ctrl-n' : 'application:new-message'
|
||||
'ctrl-r' : 'application:reply'
|
||||
'ctrl-R' : 'application:reply-all'
|
||||
'ctrl-F' : 'application:forward'
|
||||
### Actions ###
|
||||
',': 'application:focus-toolbar'
|
||||
'x': 'core:select-item'
|
||||
's': 'application:star-item'
|
||||
'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
|
||||
# to be listed in this file.
|
||||
|
||||
'body.platform-darwin':
|
||||
'body':
|
||||
# Windows email-specific menu items
|
||||
'cmd-shift-v': 'application:change-category' # Outlook
|
||||
'cmdctrl-shift-v': 'application:change-category' # Outlook
|
||||
'F3': 'application:focus-search'
|
||||
'cmd-e': 'application:focus-search'
|
||||
'cmd-f': 'application:forward'
|
||||
'cmd-shift-v': 'application:change-category'
|
||||
'cmd-d': 'core:delete-item'
|
||||
'cmdctrl-e': 'application:focus-search'
|
||||
'cmdctrl-f': 'application:forward'
|
||||
'cmdctrl-shift-v': 'application:change-category'
|
||||
'cmdctrl-d': 'application:delete-item'
|
||||
'alt-backspace':'core:undo'
|
||||
'alt-s': 'application:send-message'
|
||||
'cmd-r': 'application:reply'
|
||||
'cmd-shift-r': 'application:reply-all'
|
||||
'cmd-n' : 'application:new-message'
|
||||
'cmd-shift-m': 'application:new-message'
|
||||
'cmd-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'
|
||||
'cmdctrl-r': 'application:reply'
|
||||
'cmdctrl-shift-r': 'application:reply-all'
|
||||
'cmdctrl-n' : 'application:new-message'
|
||||
'cmdctrl-shift-m': 'application:new-message'
|
||||
'cmdctrl-enter': 'send'
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"dependencies": {
|
||||
"asar": "^0.5.0",
|
||||
"async": "^0.9",
|
||||
"atom-keymap": "^5.1",
|
||||
"atom-keymap": "6.1.0",
|
||||
"babel-core": "^6.0.20",
|
||||
"babel-preset-es2015": "^6.0.15",
|
||||
"babel-preset-react": "^6.0.15",
|
||||
|
|
|
@ -6,16 +6,14 @@ NylasTestUtils =
|
|||
loadKeymap: (keymapPath) ->
|
||||
{resourcePath} = atom.getLoadSettings()
|
||||
basePath = CSON.resolve("#{resourcePath}/keymaps/base")
|
||||
baseKeymaps = CSON.readFileSync(basePath)
|
||||
atom.keymaps.add(basePath, baseKeymaps)
|
||||
atom.keymaps.loadKeymap(basePath)
|
||||
|
||||
if keymapPath?
|
||||
keymapPath = CSON.resolve("#{resourcePath}/#{keymapPath}")
|
||||
keymapFile = CSON.readFileSync(keymapPath)
|
||||
atom.keymaps.add(keymapPath, keymapFile)
|
||||
atom.keymaps.loadKeymap(keymapPath)
|
||||
|
||||
keyPress: (key, target) ->
|
||||
event = KeymapManager.buildKeydownEvent(key, target: target)
|
||||
document.dispatchEvent(event)
|
||||
atom.keymaps.handleKeyboardEvent(event)
|
||||
|
||||
module.exports = NylasTestUtils
|
|
@ -9,7 +9,7 @@ _ = require 'underscore'
|
|||
_str = require 'underscore.string'
|
||||
fs = require 'fs-plus'
|
||||
Grim = require 'grim'
|
||||
KeymapManager = require '../src/keymap-extensions'
|
||||
KeymapManager = require '../src/keymap-manager'
|
||||
|
||||
# FIXME: Remove jquery from this
|
||||
{$} = require '../src/space-pen-extensions'
|
||||
|
|
|
@ -151,7 +151,7 @@ class Atom extends Model
|
|||
@loadTime = null
|
||||
|
||||
Config = require './config'
|
||||
KeymapManager = require './keymap-extensions'
|
||||
KeymapManager = require './keymap-manager'
|
||||
CommandRegistry = require './command-registry'
|
||||
PackageManager = require './package-manager'
|
||||
Clipboard = require './clipboard'
|
||||
|
@ -183,11 +183,11 @@ class Atom extends Model
|
|||
@config = new Config({configDirPath, resourcePath})
|
||||
|
||||
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
||||
@keymaps.subscribeToFileReadFailure()
|
||||
@keymaps.onDidMatchBinding (event) ->
|
||||
# If the user fired a command with the application: prefix bound to the body, re-fire it
|
||||
# up into the browser process. This prevents us from needing this crap, which has to be
|
||||
# updated every time a new application: command is added:
|
||||
# If the user fired a command with the application: prefix bound to
|
||||
# the body, re-fire it up into the browser process. This prevents us
|
||||
# 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
|
||||
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
|
||||
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,
|
||||
FocusedContentStore,
|
||||
AccountStore} = require 'nylas-exports'
|
||||
{KeyCommandsRegion} = require 'nylas-component-kit'
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
MultiselectListInteractionHandler = require './multiselect-list-interaction-handler'
|
||||
|
@ -30,7 +31,6 @@ class MultiselectList extends React.Component
|
|||
@propTypes =
|
||||
className: React.PropTypes.string.isRequired
|
||||
collection: React.PropTypes.string.isRequired
|
||||
commands: React.PropTypes.object.isRequired
|
||||
columns: React.PropTypes.array.isRequired
|
||||
dataStore: React.PropTypes.object.isRequired
|
||||
itemPropsProvider: React.PropTypes.func.isRequired
|
||||
|
@ -66,30 +66,23 @@ class MultiselectList extends React.Component
|
|||
teardownForProps: =>
|
||||
return unless @unsubscribers
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
@command_unsubscriber.dispose()
|
||||
|
||||
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.push props.dataStore.listen @_onChange
|
||||
@unsubscribers.push WorkspaceStore.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: =>
|
||||
# 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} />
|
||||
|
||||
<div className={className} {...otherProps}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
columns={@state.computedColumns}
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}
|
||||
dataView={@state.dataView}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={@props.itemHeight}
|
||||
onSelect={@_onClickItem}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
{spinnerElement}
|
||||
{emptyElement}
|
||||
</div>
|
||||
<KeyCommandsRegion globalHandlers={@_keymapHandlers()} className="multiselect-list">
|
||||
<div className={className} {...otherProps}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
columns={@state.computedColumns}
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}
|
||||
dataView={@state.dataView}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
itemHeight={@props.itemHeight}
|
||||
onSelect={@_onClickItem}
|
||||
onDoubleClick={@props.onDoubleClick} />
|
||||
{spinnerElement}
|
||||
{emptyElement}
|
||||
</div>
|
||||
</KeyCommandsRegion>
|
||||
else
|
||||
<div className={className} {...otherProps}>
|
||||
<Spinner visible={true} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
NylasStore = require 'nylas-store'
|
||||
WorkspaceStore = require './workspace-store'
|
||||
MailViewFilter = require '../../mail-view-filter'
|
||||
CategoryStore = require './category-store'
|
||||
AccountStore = require './account-store'
|
||||
|
@ -18,8 +19,9 @@ class FocusedMailViewStore extends NylasStore
|
|||
else if not CategoryStore.byId(@_mailView.categoryId())
|
||||
@_setMailView(@_defaultMailView())
|
||||
|
||||
_onFocusMailView: (filter) ->
|
||||
_onFocusMailView: (filter) =>
|
||||
return if filter.isEqual(@_mailView)
|
||||
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
|
||||
Actions.searchQueryCommitted('')
|
||||
@_setMailView(filter)
|
||||
|
||||
|
@ -36,7 +38,7 @@ class FocusedMailViewStore extends NylasStore
|
|||
@_mailViewBeforeSearch = null
|
||||
|
||||
_defaultMailView: ->
|
||||
category = CategoryStore.getStandardCategory('inbox')
|
||||
category = CategoryStore.getStandardCategory("inbox")
|
||||
return null unless category
|
||||
MailViewFilter.forCategory(category)
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
_ = require 'underscore'
|
||||
Actions = require '../actions'
|
||||
AccountStore = require './account-store'
|
||||
CategoryStore = require './category-store'
|
||||
MailViewFilter = require '../../mail-view-filter'
|
||||
NylasStore = require 'nylas-store'
|
||||
|
||||
Sheet = {}
|
||||
|
@ -41,8 +43,38 @@ class WorkspaceStore extends NylasStore
|
|||
@popToRootSheet()
|
||||
@trigger()
|
||||
|
||||
atom.commands.add 'body',
|
||||
'application:pop-sheet': => @popSheet()
|
||||
atom.commands.add 'body', @_navigationCommands()
|
||||
|
||||
_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: =>
|
||||
@Location = Location = {}
|
||||
|
|
|
@ -23,6 +23,7 @@ class NylasComponentKit
|
|||
@load "ButtonDropdown", 'button-dropdown'
|
||||
@load "Contenteditable", 'contenteditable/contenteditable'
|
||||
@load "MultiselectList", 'multiselect-list'
|
||||
@load "KeyCommandsRegion", 'key-commands-region'
|
||||
@load "InjectedComponent", 'injected-component'
|
||||
@load "TokenizingTextField", 'tokenizing-text-field'
|
||||
@load "MultiselectActionBar", 'multiselect-action-bar'
|
||||
|
|
|
@ -146,6 +146,6 @@ class NylasExports
|
|||
@get "APMWrapper", -> require('../apm-wrapper')
|
||||
|
||||
# Testing
|
||||
@get "NylasTestUtils", -> require '../../spec/test_utils'
|
||||
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'
|
||||
|
||||
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) ->
|
||||
new SearchMailViewFilter(query)
|
||||
|
||||
@forAll: ->
|
||||
new AllMailViewFilter()
|
||||
|
||||
# Instance Methods
|
||||
|
||||
constructor: ->
|
||||
|
@ -83,6 +86,31 @@ class SearchMailViewFilter extends MailViewFilter
|
|||
categoryId: ->
|
||||
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
|
||||
constructor: ->
|
||||
|
|
|
@ -245,7 +245,7 @@ class Package
|
|||
if @bundledPackage and packagesCache[@name]?
|
||||
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
|
||||
else
|
||||
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
|
||||
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, atom.keymaps.readKeymap(keymapPath) ? {}]
|
||||
|
||||
loadMenus: ->
|
||||
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/generated-form";
|
||||
@import "components/unsafe";
|
||||
@import "components/key-commands-region";
|
||||
|
|
Loading…
Reference in a new issue