fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
# Keymaps In-Depth
|
|
|
|
|
|
|
|
## Structure of a Keymap File
|
|
|
|
|
|
|
|
Keymap files are encoded as JSON or CSON files containing nested hashes. They
|
|
|
|
work much like style sheets, but instead of applying style properties to elements
|
|
|
|
matching the selector, they specify the meaning of keystrokes on elements
|
|
|
|
matching the selector. Here is an example of some bindings that apply when
|
|
|
|
keystrokes pass through `atom-text-editor` elements:
|
|
|
|
|
|
|
|
```coffee
|
|
|
|
'atom-text-editor':
|
|
|
|
'cmd-delete': 'editor:delete-to-beginning-of-line'
|
|
|
|
'alt-backspace': 'editor:delete-to-beginning-of-word'
|
|
|
|
'ctrl-A': 'editor:select-to-first-character-of-line'
|
|
|
|
'ctrl-shift-e': 'editor:select-to-end-of-line'
|
|
|
|
'cmd-left': 'editor:move-to-first-character-of-line'
|
|
|
|
|
|
|
|
'atom-text-editor:not([mini])'
|
|
|
|
'cmd-alt-[': 'editor:fold-current-row'
|
|
|
|
'cmd-alt-]': 'editor:unfold-current-row'
|
|
|
|
```
|
|
|
|
|
|
|
|
Beneath the first selector are several bindings, mapping specific *keystroke
|
|
|
|
patterns* to *commands*. When an element with the `atom-text-editor` class is focused and
|
|
|
|
`cmd-delete` is pressed, an custom DOM event called
|
|
|
|
`editor:delete-to-beginning-of-line` is emitted on the `atom-text-editor` element.
|
|
|
|
|
|
|
|
The second selector group also targets editors, but only if they don't have the
|
|
|
|
`mini` attribute. In this example, the commands for code folding don't really
|
|
|
|
make sense on mini-editors, so the selector restricts them to regular editors.
|
|
|
|
|
|
|
|
### Keystroke Patterns
|
|
|
|
|
|
|
|
Keystroke patterns express one or more keystrokes combined with optional
|
|
|
|
modifier keys. For example: `ctrl-w v`, or `cmd-shift-up`. A keystroke is
|
|
|
|
composed of the following symbols, separated by a `-`. A multi-keystroke pattern
|
|
|
|
can be expressed as keystroke patterns separated by spaces.
|
|
|
|
|
|
|
|
|
|
|
|
| Type | Examples
|
|
|
|
| --------------------|----------------------------
|
|
|
|
| Character literals | `a` `4` `$`
|
|
|
|
| Modifier keys | `cmd` `ctrl` `alt` `shift`
|
|
|
|
| Special keys | `enter` `escape` `backspace` `delete` `tab` `home` `end` `pageup` `pagedown` `left` `right` `up` `down`
|
|
|
|
|
|
|
|
### Commands
|
|
|
|
|
|
|
|
Commands are custom DOM events that are triggered when a keystroke matches a
|
|
|
|
binding. This allows user interface code to listen for named commands without
|
|
|
|
specifying the specific keybinding that triggers it. For example, the following
|
|
|
|
code creates a command to insert the current date in an editor:
|
|
|
|
|
|
|
|
```coffee
|
|
|
|
atom.commands.add 'atom-text-editor',
|
|
|
|
'user:insert-date': (event) ->
|
|
|
|
editor = @getModel()
|
|
|
|
editor.insertText(new Date().toLocaleString())
|
|
|
|
```
|
|
|
|
|
|
|
|
`atom.commands` refers to the global {CommandRegistry} instance where all commands
|
|
|
|
are set and consequently picked up by the command palette.
|
|
|
|
|
|
|
|
When you are looking to bind new keys, it is often useful to use the command
|
|
|
|
palette (`ctrl-shift-p`) to discover what commands are being listened for in a
|
|
|
|
given focus context. Commands are "humanized" following a simple algorithm, so a
|
|
|
|
command like `editor:fold-current-row` would appear as "Editor: Fold Current
|
|
|
|
Row".
|
|
|
|
|
|
|
|
### "Composed" Commands
|
|
|
|
|
|
|
|
A common question is, "How do I make a single keybinding execute two or more
|
|
|
|
commands?" There isn't any direct support for this in Atom, but it can be
|
|
|
|
achieved by creating a custom command that performs the multiple actions
|
|
|
|
you desire and then creating a keybinding for that command. For example, let's
|
|
|
|
say I want to create a "composed" command that performs a Select Line followed
|
|
|
|
by Cut. You could add the following to your `init.coffee`:
|
|
|
|
|
|
|
|
```coffee
|
|
|
|
atom.commands.add 'atom-text-editor', 'custom:cut-line', ->
|
|
|
|
editor = atom.workspace.getActiveTextEditor()
|
|
|
|
editor.selectLinesContainingCursors()
|
|
|
|
editor.cutSelectedText()
|
|
|
|
```
|
|
|
|
|
|
|
|
Then let's say we want to map this custom command to `alt-ctrl-z`, you could
|
|
|
|
add the following to your keymap:
|
|
|
|
|
|
|
|
```coffee
|
|
|
|
'atom-text-editor':
|
|
|
|
'alt-ctrl-z': 'custom:cut-line'
|
|
|
|
```
|
|
|
|
|
|
|
|
### Specificity and Cascade Order
|
|
|
|
|
|
|
|
As is the case with CSS applying styles, when multiple bindings match for a
|
|
|
|
single element, the conflict is resolved by choosing the most *specific*
|
|
|
|
selector. If two matching selectors have the same specificity, the binding
|
|
|
|
for the selector appearing later in the cascade takes precedence.
|
|
|
|
|
|
|
|
Currently, there's no way to specify selector ordering within a single keymap,
|
|
|
|
because JSON objects do not preserve order. We eventually plan to introduce a
|
|
|
|
custom CSS-like file format for keymaps that allows for ordering within a single
|
|
|
|
file. For now, we've opted to handle cases where selector ordering is critical
|
|
|
|
by breaking the keymap into two separate files, such as `snippets-1.cson` and
|
|
|
|
`snippets-2.cson`.
|
|
|
|
|
|
|
|
## Removing Bindings
|
|
|
|
|
|
|
|
When the keymap system encounters a binding with the `unset!` directive as its
|
|
|
|
command, it will treat the current element as if it had no key bindings matching
|
|
|
|
the current keystroke sequence and continue searching from its parent. If you
|
|
|
|
want to remove a binding from a keymap you don't control, such as keymaps in
|
|
|
|
Atom core or in packages, use the `unset!` directive.
|
|
|
|
|
|
|
|
For example, the following code removes the keybinding for `a` in the Tree View,
|
|
|
|
which is normally used to trigger the `tree-view:add-file` command:
|
|
|
|
|
|
|
|
```coffee
|
|
|
|
'.tree-view':
|
|
|
|
'a': 'unset!'
|
|
|
|
```
|
|
|
|
|
|
|
|
![](https://cloud.githubusercontent.com/assets/38924/3174771/e7f6ce64-ebf4-11e3-922d-f280bffb3fc5.png)
|
|
|
|
|
|
|
|
## Forcing Chromium's Native Keystroke Handling
|
|
|
|
|
2015-02-05 10:31:41 +08:00
|
|
|
EDIT: This has been overridden in the Edgehill project. By default, Chromium's
|
|
|
|
native keystroke handling for Copy, Paste, Select-All, etc. is enabled. To except
|
|
|
|
yourself from these behaviors, apply the `.override-key-bindings` class to an element.
|
|
|
|
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
If you want to force the native browser behavior for a given keystroke, use the
|
|
|
|
`native!` directive as the command of a binding. This can be useful to enable
|
|
|
|
the correct behavior in native input elements, for example. If you apply the
|
|
|
|
`.native-key-bindings` class to an element, all the keystrokes typically handled
|
|
|
|
by the browser will be assigned the `native!` directive.
|
|
|
|
|
|
|
|
## Overloading Key Bindings
|
|
|
|
|
|
|
|
Occasionally, it makes sense to layer multiple actions on top of the same key
|
|
|
|
binding. An example of this is the snippets package. Snippets are inserted by
|
|
|
|
typing a snippet prefix such as `for` and then pressing `tab`. Every time `tab`
|
|
|
|
is pressed, we want to execute code attempting to expand a snippet if one exists
|
|
|
|
for the text preceding the cursor. If a snippet *doesn't* exist, we want `tab`
|
|
|
|
to actually insert whitespace.
|
|
|
|
|
|
|
|
To achieve this, the snippets package makes use of the `.abortKeyBinding()`
|
|
|
|
method on the event object representing the `snippets:expand` command.
|
|
|
|
|
|
|
|
```coffee-script
|
|
|
|
# pseudo-code
|
|
|
|
editor.command 'snippets:expand', (e) =>
|
|
|
|
if @cursorFollowsValidPrefix()
|
|
|
|
@expandSnippet()
|
|
|
|
else
|
|
|
|
e.abortKeyBinding()
|
|
|
|
```
|
|
|
|
|
|
|
|
When the event handler observes that the cursor does not follow a valid prefix,
|
|
|
|
it calls `e.abortKeyBinding()`, telling the keymap system to continue searching
|
|
|
|
for another matching binding.
|
|
|
|
|
|
|
|
## Step-by-Step: How Keydown Events are Mapped to Commands
|
|
|
|
|
|
|
|
* A keydown event occurs on a *focused* element.
|
|
|
|
* Starting at the focused element, the keymap walks upward towards the root of
|
|
|
|
the document, searching for the most specific CSS selector that matches the
|
|
|
|
current DOM element and also contains a keystroke pattern matching the keydown
|
|
|
|
event.
|
|
|
|
* When a matching keystroke pattern is found, the search is terminated and the
|
|
|
|
pattern's corresponding command is triggered on the current element.
|
|
|
|
* If `.abortKeyBinding()` is called on the triggered event object, the search
|
|
|
|
is resumed, triggering a binding on the next-most-specific CSS selector for
|
|
|
|
the same element or continuing upward to parent elements.
|
|
|
|
* If no bindings are found, the event is handled by Chromium normally.
|